A Java library for programmatically generating Minecraft-themed images: item tooltips, inventories, individual items, and player heads, as static PNGs or animated GIFs.
Everything below was rendered by the library itself. Each snippet is the actual code that produces the image next to it.
Full Minecraft formatting: colors, bold, italic, strikethrough, underline, obfuscation, text wrapping, rarity coloring, and configurable padding and borders.
GeneratedObject tooltip = new GeneratorImageBuilder()
.addGenerator(new MinecraftTooltipGenerator.Builder()
.withName("Aegis of the Fallen Star")
.withRarity(Rarity.byName("LEGENDARY"))
.withType("SWORD")
.withItemLore("""
&7A blade forged in the heart of a dying star.
&6⚔ Damage: &c+340
&6✦ Attack Speed: &aVery Fast
&f&lBold &7&mStrike&r &b&nUnderline &d&oItalic
&e&lRIGHT-CLICK &7to unleash &6Starfall""")
.withMaxLineLength(48)
.withRenderBorder(true)
.build())
.build();Use %%gradient:#start:#end%%...%%/gradient%% for smooth per-character gradients, and &#RRGGBB for inline hex colors.
new MinecraftTooltipGenerator.Builder()
.withName("Prism of Eternity")
.withRarity(Rarity.byName("MYTHIC"))
.withType("RELIC")
.withItemLore("""
%%gradient:#ff3cac:#784ba0%%A shard of the very first dawn.%%/gradient%%
%%gradient:#2af598:#009efd%%Its light bends reality itself.%%/gradient%%
&#ffd700✦ Radiance: &#ffec8b+500
fdbff❉ Clarity: fdbff+250""")
.withRenderBorder(true)
.build();Render text in the Standard Galactic Alphabet (&g) or Illageralt (&h).
new MinecraftTooltipGenerator.Builder()
.withName("Tome of Forbidden Runes")
.withRarity(Rarity.byName("EPIC"))
.withType("ARTIFACT")
.withItemLore("&b&gThe stars remember your name\n&d&gSpeak the word and be unmade")
.build();Render any vanilla item from its id, optionally with an animated enchantment glint. When a generator is animated, getGifData() returns the encoded GIF.
// Static item
new MinecraftItemGenerator.Builder()
.withItem("diamond_sword")
.build();
// Animated enchant glint
GeneratedObject glint = new GeneratorImageBuilder()
.addGenerator(new MinecraftItemGenerator.Builder()
.withItem("netherite_sword")
.isEnchanted(true)
.build())
.build();
Files.write(Path.of("glint.gif"), glint.getGifData());Complete inventory grids with item placement, stack counts, titles, and configurable rows and columns. The inventory string is material:slot per item ({slot:amount} sets a stack count), joined with %%.
new MinecraftInventoryGenerator.Builder()
.withRows(3)
.withSlotsPerRow(9)
.withContainerTitle("Legendary Loot")
.withInventoryString(
"diamond_sword:1%%nether_star:5%%elytra:6%%"
+ "emerald:{11:64}%%diamond:{12:64}%%dragon_head:27")
.build();Render a 3D player head from a player name, texture URL, base64 texture data, or a texture hash.
new MinecraftPlayerHeadGenerator.Builder()
.withSkin("Aerh") // name, texture URL, base64, or hash
.withScale(-2) // positive upscales, negative downscales
.build();Compose one or more generators with GeneratorImageBuilder and write the result to disk. Static output is a BufferedImage, and animated output is a GIF byte array.
GeneratedObject result = new GeneratorImageBuilder()
.addGenerator(new MinecraftTooltipGenerator.Builder()
.withName("Hello World")
.withRarity(Rarity.byName("RARE"))
.withItemLore("&7Your first &brendered &7tooltip.")
.build())
.build();
if (result.isAnimated()) {
Files.write(Path.of("output.gif"), result.getGifData());
} else {
ImageIO.write(result.getImage(), "png", new File("output.png"));
}By default everything renders with vanilla textures. You can also load a resource pack and render any generator with its item textures, tooltip styles, custom glyphs, and color palette. This is what drives Hypixel SkyBlock item rendering, for example.
Register a pack, from a directory or a zip, then pass its id to any generator:
// Load a pack from disk (directory or .zip). Stays registered until unregistered.
PackId skyblock = PackRepository.global().register(
"hypixel:skyblock",
PackSource.directory(Path.of("packs/hypixel-skyblock"), PackLimits.fromSystemProperties()));
// A tooltip in one of the pack's rarity styles, with a gradient and hex colors
new MinecraftTooltipGenerator.Builder()
.withName("&#c77dffMaelstrom")
.withRarity(Rarity.byName("MYTHIC"))
.withType("STAFF")
.withItemLore("""
%%gradient:#f857a6:#ff5858%%The sky answers only to its bearer.%%/gradient%%
&7Intelligence: &b+900
&7Ability Damage: &d+65%""")
.withPack("hypixel:skyblock")
.withTooltipStyle("hypixel_skyblock:mythic")
.build();
// A custom item texture from the pack
new MinecraftItemGenerator.Builder()
.withItem("hypixel_skyblock:item/uncategorized/aurora_staff")
.withPack("hypixel:skyblock")
.build();The items below are invented, rendered with this pack's real textures and rarity tooltip styles (common through mythic, plus special, supreme, ultimate, and more):
![]() ![]() |
![]() ![]() |
![]() ![]() |
![]() ![]() |
![]() ![]() |
![]() ![]() |
![]() ![]() |
- Vanilla (
minecraft:minecraft) is always available and is the default, sowithPackis opt-in. - Pack ids are
namespace:nameat the library level (e.g.hypixel:skyblock), separate from the asset namespace a pack declares internally (e.g.hypixel_skyblock). - Item refs use the pack's asset namespace and item path (e.g.
hypixel_skyblock:item/uncategorized/aurora_staff). withTooltipStyle(...)selects the pack'sminecraft:tooltip_style(e.g.hypixel_skyblock:mythic).- The item and inventory generators accept
withPack(...)too, so a whole inventory can render in a pack's style. PackRepository.global().unregister("hypixel:skyblock")releases a pack and frees its id. There is no atomic replace: to swap a pack's content under the same id, unregister it and register again - a render racing the swap fails with the ordinary pack-not-registered error until the new registration lands.
Beyond item textures and tooltip styles, the library renders the interface techniques that large server resource packs are built on: menus whose entire background is glyph art drawn by the title through a custom font, per-rarity tooltip frames, elements-based item models dispatched on custom model data, and HUD text pushed through bossbar names. Packs like these ship on many large servers; every example below uses a fictional pack (emberveil) - swap in your own pack's ids.
Pack reads are guarded by PackLimits. The defaults (read from generator.pack.* system properties) comfortably fit vanilla-scale packs, but large server packs can far exceed the 20,000-entry default. Construct one PackLimits and pass the same instance to both the source factory and register so read-time and index-time limits agree:
PackLimits limits = new PackLimits(
60_000, // maxEntries
8L * 1024 * 1024, // maxEntryBytes
1_024, // maxTextureDim (item and general GUI textures)
256L * 1024 * 1024, // textureCacheMaxBytes
8_192); // sheetTextureMaxDim (font glyph sheets, sprite strips)
PackId emberveil = PackRepository.global().register(
"emberveil:main",
PackSource.zip(Path.of("packs/emberveil.zip"), limits),
limits);- What
maxEntriescounts depends on the source type: a zip counts every central-directory record (regular files and directory entries), a directory source counts only regular files underassets/. Size the limit comfortably above the pack's record count. - Sheet-shaped textures (font glyph sheets, animated tooltip sprite strips) decode under the separate
sheetTextureMaxDimcap (default 8192); item and general GUI textures keep the strictmaxTextureDimimage-bomb guard.
Placeholder icons like %%zone%% swap to a pack's own glyph through packOverrides entries keyed by pack id (for example "hypixel:skyblock": ""). A second build of the same pack registered under its own id, such as an alpha build, matches none of those keys. Prepare it as a variant of the original so it uses them too:
PackId alpha = PackRepository.global().register(PackRepository.prepare(
"hypixel:alpha", "hypixel:skyblock",
PackSource.zip(Path.of("packs/hypixel-alpha.zip"), limits),
limits));- Overrides are looked up under the pack's own id first, then under its
variantOfid, so a glyph that differs in the variant can still get its own key. - The variant also inherits pack-id conventions such as the emissive alpha normalization.
- The
variantOfpack does not have to be registered; only one level is followed.ResourcePackServiceusers setvariantOfon thePackDefinition.
Each text segment resolves against a font id: its explicit pack font id when set, otherwise the resource location of its built-in font (minecraft:default for ordinary text, minecraft:alt for &g, minecraft:illageralt for &h). A pack that overrides minecraft:default therefore restyles ordinary tooltip text automatically - just render with withPack(...):
// The pack's minecraft:default override restyles all of this text
new MinecraftTooltipGenerator.Builder()
.withName("Emberveil Compass")
.withRarity(Rarity.byName("RARE"))
.withItemLore("&7Points to the nearest ember rift.")
.withPack("emberveil:main")
.build();Explicit font ids go on individual runs and segments: the font key of a menu recipe title run, TitleRun.of(text, fontId) for container titles and HUD lines, and withPackFontId(...) when composing a MinecraftTooltip from segments directly:
MinecraftTooltip tooltip = MinecraftTooltip.builder()
.withSegments(ColorSegment.builder()
.withText("\uE0A0 Ember Rush \uE0A1")
.withPackFontId("emberveil:icons")
.build())
.withPackFontSource(fontId -> PackRepository.global().resolveFont(emberveil, fontId))
.build()
.render();- Codepoints the pack font does not supply fall back to the built-in fonts; a missing font id falls back entirely.
- The full provider model is honored:
bitmapsheets at any declared height and ascent,spaceproviders with negative or fractional advances (how packs kern glyph art together),referenceexpansion, andfilterobjects. - One deliberate deviation from vanilla: a
bitmapprovider whose sheet texture is absent from the pack is skipped with a warning instead of failing the whole font. Real packs overrideminecraft:defaultwith providers referencing vanilla client sheets (e.g.minecraft:font/ascii.png) that this library does not bundle; skipping just those providers keeps every glyph the pack actually ships renderable. Present-but-broken sheets still fail loudly, and a font whose providers all skip resolves as an empty font. - Obfuscated (
&k) text substitutes glyphs of equal advance from the same pack font, deterministically per frame. PackRepository.global().fontIds(packId)lists every font a pack defines.
withTooltipStyle(...) (shown above) selects a pack's minecraft:tooltip_style sprite pair - <style>_background and <style>_frame - honoring their nine-slice gui.scaling mcmeta. Three details worth knowing:
- Missing styles fail loudly: requesting a style the pack does not define throws instead of rendering the vanilla missing texture.
- Without an explicit style, a pack override of the default tooltip sprites (
minecraft:tooltip/background+minecraft:tooltip/frame) still themes every tooltip; animated sprite strips play their full animation (see Animated textures). PackRepository.global().tooltipStyles(packId)lists every style a pack defines.
MinecraftContainerGenerator renders a vanilla-geometry generic chest screen, including the title-glyph background technique: a menu whose whole background is glyph art drawn by the title through a custom font, over a fully transparent generic_54 container texture.
new MinecraftContainerGenerator.Builder()
.withRows(6)
.withTitle(
TitleRun.of("\uE000", "emberveil:menu"), // full-bleed menu art glyph
TitleRun.of("Vault of Embers"))
.withSlot(22, "emberveil:item/ember_blade,enchant")
.withSlot(31, "nether_star:5")
.withPack("emberveil:main")
.withScaleFactor(2)
.build();- Geometry is vanilla-exact: the GUI rect is 176 x
114 + 18 * rowsGUI px, the top-left slot interior sits at (8, 18) with an 18 px pitch, and the title draws unclipped at (8, 6) in#404040with no shadow. - The canvas expands to cover the measured title-line extents, so full-bleed menu art reached through glyph ascents and negative advances is never clipped; slot positions stay anchored to the GUI rect.
- With a pack override of
minecraft:textures/gui/container/generic_54.png, the texture is stitched exactly like the client (chest section above the player-inventory section); without one, procedural vanilla-style chrome is drawn. - Layers follow the vanilla z-order: background, then title art, then items, then stack count badges.
- Slot items use the inventory item spec (modifiers like
enchantandhoverincluded) plus an optional trailing:amount; pack items needing custom model data usewithSlot(int, String, CustomModelData).
MinecraftContainerGenerator.fromRecipe(String) parses a JSON transcription of a menu and returns a preconfigured builder. Pack selection and scale factor are not part of the document - set them on the returned builder:
MinecraftContainerGenerator menu = MinecraftContainerGenerator.fromRecipe("""
{
"rows": 3,
"title": [
{"text": "\\uE001", "font": "emberveil:menu"},
{"text": "Reliquary", "color": "#3F3F3F", "bold": true}
],
"slots": {
"11": "emberveil:item/ember_blade",
"13": "diamond_sword,enchant",
"15": "emberveil:item/ashen_idol:16"
}
}""")
.withPack("emberveil:main")
.build();The full schema:
rows(required): chest rows, an integer 1-6.title(optional): an array of run objects concatenated into one line. Each run takestext(required),font(a resource location, e.g. a pack font),color(strict#RRGGBB), and booleanbold/italic(absent means false).slots(optional): an object mapping 1-based row-major slot indices ("1"is the top-left slot,"rows * 9"the bottom-right) to item spec strings: a vanilla material or pack item ref, optional modifiers (enchant,hover, ...), optional durability, and an optional trailing:amount(1-64).- Parsing is strict by design, so a bad transcription fails loudly instead of rendering a silently wrong menu: unknown keys, duplicate keys anywhere, non-canonical slot keys (
"01","+1"), out-of-range slot indices, malformed colors or font ids, and trailing content are all rejected.
Items can be addressed by their minecraft:item_model component value, and item model definitions that dispatch on minecraft:custom_model_data evaluate against caller-supplied data:
// Address an item by its item model definition
new MinecraftItemGenerator.Builder()
.withItemModel("emberveil:item/ember_blade")
.withPack("emberveil:main")
.build();
// Evaluate custom_model_data dispatch nodes and tint sources
new MinecraftItemGenerator.Builder()
.withItemModel("emberveil:item/soul_lantern")
.withCustomModelData(new CustomModelData(
List.of(3.0f), // floats: range_dispatch
List.of(true), // flags: condition
List.of("lit"), // strings: select
List.of(0xFF7A1F))) // colors: custom_model_data tint sources
.withPack("emberveil:main")
.build();
// Damage-driven dispatch (range_dispatch on property minecraft:damage)
new MinecraftItemGenerator.Builder()
.withItemModel("emberveil:item/ember_blade")
.withItemDamage(1_200, 1_561) // damage, max damage
.withPack("emberveil:main")
.build();- Flat generated models render as sprites exactly like retextured vanilla items: contiguous
layer0..layer4textures stack bottom-to-top with tinticoloring layeri, vanilla's item model baking. Elements-based models rasterize directly at the target resolution, so sub-pixel geometry survives at any scale. Identity and mirroreddisplay.guirotations use exact flat projections;withFullGuiRotations(true)renders every other rotation as the true orthographic projection of the 3D model (see Limitations). - Per-element
rotationentries (any angle about one axis - the modern 1.21.6+ rule;originandrescaleincluded) render with vanilla semantics - the geometry decorative medals and tilted props are built from. gui_light: sidemodels (the vanilla default when the key is absent) shade each face by the vanilla per-orientation constants (up 1.0, north/south 0.8, east/west 0.6, down 0.5) in the orthographic pipeline;gui_light: frontandshade: falseelements render unshaded. Note that any active element rotation routes its whole model through the orthographic pipeline even when the gui rotation classifies as flat, so a side-lit model gains this shading the moment it carries a rotated element - the pinned flat fast paths themselves never shade.oversized_in_guiitems skip slot clipping: in container renders their art anchors on the slot center and spans neighboring slots, exactly like the in-game client.condition,select,range_dispatchandcompositenodes are supported, along withconstant,custom_model_dataanddye(rendered at its requireddefaultcolor) tint sources.withItemDamage(damage, maxDamage)feedsrange_dispatchnodes withproperty: minecraft:damage:normalize: true(the vanilla default) evaluates the 0..1 damage fraction,normalize: falsethe raw damage value. When no damage is set, the property evaluates at 0.- Model parent chains may end at the vanilla flat templates
minecraft:item/generatedorminecraft:item/handheldwithout the pack shipping them - the ordinary flat-item shape real packs rely on. Any other parent missing from a pack-claimed namespace still fails loudly. MinecraftNbtParserpicks the component up straight from 1.20.5+ NBT: an item whosecomponentscarryminecraft:item_modelis addressed by that model instead of by itsid, and the value is exposed onParsedNbt.getParsedItemModel(). The model wins over the player head render even whenminecraft:profileis also present (as it does in vanilla), while the resolved head texture stays on the result so a caller whose pack cannot resolve the model can fall back to the head. A blank or non-string component is ignored rather than failing the parse.
Servers render HUD text through bossbar names with the bar sprites blanked by the pack. MinecraftHudLineGenerator renders such a stack of centered text lines at the exact vanilla bossbar anchors - line k's top edge at GUI y = 3 + 19 * k on a canvas whose top is the screen top:
new MinecraftHudLineGenerator.Builder()
.withLine(TitleRun.of("\uE010", "emberveil:hud")) // glyph art line
.withLine(TitleRun.of("Ember Rush "), TitleRun.of("02:41", "emberveil:timer"))
.withLine() // blank spacer line
.withLine(TitleRun.of("Collect 30 embers"))
.withPack("emberveil:main")
.build();- Lines center on the canvas middle in whole GUI px - vanilla's exact
width / 2 - textWidth / 2integer math - with extent-aware measurement, so trailing negative advances do not skew the centering. The default canvas width is 320 GUI px (a 1280 px screen at GUI scale 4); override it withwithGuiWidth(...). - Runs without an explicit color draw in white with a drop shadow, matching vanilla bossbar names.
TitleRunis the same styled-run type the container title uses, so pack fonts, colors, bold and italic behave identically in both.
Animated pack textures (vertical flipbooks with an animation mcmeta) always render as the real flipbook - there is no opt-in and no toggle. When the resolved visual uses at least one animated texture, generate() returns the GIF form of GeneratedObject, with getFrameDelaysMs() carrying the per-frame delays (frames legitimately hold for different times - a shiny-strip frames list like [0..16, {"index": 0, "time": 100}] produces one long-hold frame). A caller that wants a single still frame takes one from getAnimationFrames() itself:
// An item whose layer0 sprite (or elements-model face texture) is animated
new MinecraftItemGenerator.Builder()
.withItemModel("emberveil:item/ember_blade")
.withPack("emberveil:main")
.build();
// A "shiny" tooltip style whose background sprite is a 17-frame strip
new MinecraftTooltipGenerator.Builder()
.withName("&6Soulrend")
.withItemLore("&7A blade of &kliving&7 flame")
.withPack("emberveil:main")
.withTooltipStyle("emberveil:mythic")
.build();- The full vanilla animation model is honored:
frametime(default 1 tick = 50 ms), frames lists with int and{index, time}entries (a per-frametimeoverridesframetime), default top-to-bottom frame order when the list is absent, andwidth/heightframe-size overrides.interpolate: truecross-fades like the vanilla client: each keyframe expands into one blended frame per tick, linearly interpolating the previous keyframe toward the next bysubTick / time(R/G/B mixed with vanilla's truncating math, alpha kept from the current keyframe so opaque animations stay opaque). Because interpolated playback samples every tick, its cycle reaches the shared 120-step / 1200-tick timeline cap sooner and truncates deterministically past it. - Multiple animated sources in one scene (several slot items, an animated container background, chrome plus obfuscated text) share one timeline: the least common multiple of the cycles, sampled wherever any source changes frame. Timelines cap at
AnimationTimeline.MAX_ANIMATION_FRAMES(120) steps andAnimationTimeline.MAX_CYCLE_TICKS(1200) ticks; longer cycles truncate deterministically with a warning and loop early. - In tooltips, animated chrome and the obfuscated-text animation tick on the same shared timeline. Pack-glyph obfuscation is seeded per frame, so it is deterministic; built-in font obfuscation stays random per render, so a tooltip that mixes animated chrome with built-in
&ktext is not byte-reproducible. Obfuscated text also forces the timeline to sample every tick, so pairing it with a long chrome hold (a shiny strip's 100-tick frame) can push the cycle past the 120-step cap and truncate the hold - deterministically, but earlier than the chrome alone would loop. - The enchant glint is not applied while animated textures drive an item's output (its 33 ms cycle cannot join the tick-based timeline; a warning is logged); in inventories, glint-animated slot items render their static frame while texture animation drives the scene.
- Font glyphs never animate: vanilla does not animate font atlas textures. Server-side glyph animation (server-style codepoint cycling) is the caller's concern.
- Renders without any animated texture stay static, returning a
BufferedImageexactly as before.
Documented edges of the pack renderer, all deliberate:
- Fonts:
ttfproviders render as real vector glyphs (Tier 1): the pack's font file is rasterized with plain Java2D atppem = round(size * oversample)(antialiasing off), scaled down by1 / oversampleto GUI pixels, and placed on the vanilla baseline honoring the providershift. Each pack's font is a privatejava.awt.Fontinstance (never globally registered), so packs shipping same-named fonts never collide. A missing or unloadablettf, plus everyunihexandlegacy_unicodeprovider, still parses but does not render - those codepoints fall back to the built-in fonts (a warning is logged once per font).bitmapproviders whose sheet texture is absent from the pack are skipped with a warning instead of failing the whole font - a deliberate deviation from vanilla for packs referencing unbundled vanilla client sheets (a font whose providers all skip resolves as an empty font). - TTF fidelity (Tier 2, not built): pixel fonts authored for integral ppem (e.g. a pixel
hud.ttfat 28) reproduce exactly; non-pixel outlines render blocky since the downscale is nearest-neighbor with no coverage averaging. The full vanillaMinecraftGlyphVectorpath (glyph-index selection,setGlyphPositionoverlap,Areaoutline knockout, COLR/CPAL) is out of scope - no real server pack needs it, and vanilla only alpha-composites glyph textures. TTF glyphs also render as themselves under&kobfuscation rather than scrambling among equal-width glyphs (that pool is bitmap/space only, to avoid a full-font scan). - Model rotations:
display.guirotations snap to the identity or the horizontal mirror when within 5 degrees of them about y (absorbing decorative tilts) and render through exact flat projections; anything else fails loudly by default.withFullGuiRotations(true)(item and container builders) opts into the true orthographic projection instead - the vanilla GUI presentation, so[30, 225, 0]-style block angles show their three shaded faces. The flag never changes identity- or mirror-classified renders. - True 3D: the orthographic pipeline paints faces back-to-front by projected center depth (a painter's algorithm). Flat stacks and convex solids order exactly like a depth test; mutually intersecting elements are approximated by their center depth. There is no perspective, matching the vanilla GUI camera.
- Tall glyphs in tooltips: the standard tooltip canvas stays line-height based, so glyph art much taller than the line clips there. The container generator expands its canvas from measured art extents on every side; the HUD generator grows its canvas bottom for deep art, but its top edge stays the screen top, so art crossing the top or side edges clips exactly as in game.
- Effects on elements renders in slots: container slot modifiers (
enchant,hover, durability) are ignored for elements-model slot items, with a warning. The standalone item generator is not limited this way - it applies its effect pipeline to elements renders like any other render. - Animation: animated pack textures always play (see Animated textures), so a visual carrying one renders a GIF rather than a still;
interpolatecross-fades per tick (which reaches the cap sooner), and shared timelines cap at 120 steps / 1200 ticks. - Shaders: shader-driven effects (core shader text animations and the like) are out of scope.
Every generator produces a GeneratedObject, and the scene layer arranges several of them into one image. An all-static scene composes to a PNG; if any member is animated, the whole scene becomes a single GIF on a shared tick timeline, with each member's authored timing intact.
MinecraftSceneGenerator lays members out with no layout spec: row() places them left to right, column() stacks them, and grid(columns) fills a row-major grid where each column is as wide as its widest member and each row as tall as its tallest. Spacing, margin, and in-cell alignment have defaults you can override. Scenes are generators themselves, so they nest - a row of tooltips can be one member of a larger column.
Generator roster = MinecraftSceneGenerator.grid(3)
.add(new MinecraftItemGenerator.Builder().withItem("myserver:item/relic").withPack("myserver:menus").build())
.add(new MinecraftItemGenerator.Builder().withItem("myserver:item/star").withPack("myserver:menus").build())
.add(new MinecraftItemGenerator.Builder().withItem("myserver:item/blade").withPack("myserver:menus").build())
.withSpacing(8)
.withAlignment(MinecraftSceneGenerator.Alignment.CENTER)
.build();For layouts that need precise placement, MinecraftSceneGenerator.fromScene(json) builds a generator from a declarative document (schema v1). Named regions each wrap one other generator and are placed either at explicit GUI-px coordinates (at) or anchored to another region's edge (anchor). Every region is measured before anything is placed, so an anchor can target a neighbor whose size is not known up front; placement runs in dependency order over those anchors, and z controls the draw stack (declaration order breaks ties).
{
"schema_version": 1,
"regions": [
{"name": "icon", "type": "item", "item": "myserver:item/relic", "at": [0, 0]},
{"name": "card", "type": "tooltip", "rarity": "EPIC", "lore": "&7A relic of the deep.",
"anchor": {"to": "icon", "edge": "right", "align": "center", "offset": [4, 0]}},
{"name": "badge", "type": "item", "item": "myserver:item/star", "z": 10,
"anchor": {"to": "icon", "edge": "top", "align": "end"}}
]
}Region types are tooltip, item, container, hud, template, and the row/column/grid arrangements (which nest their own regions). The document is parsed and validated eagerly, so a malformed scene throws with a message naming the offender rather than rendering something silently wrong; unknown keys anywhere are logged and ignored, so a document written against a newer schema still renders on an older build. Pass a PackId and PackRepository to fromScene(json, packId, repository) to render pack-backed members.
A template region embeds a content template (see below) inline: template is a full template document, values supplies its global placeholders, and rows supplies each template_repeat section's rows. The scene fills the template with them and renders the result exactly like an nbt tooltip region, so a template filled with a capture's own values renders byte-for-byte identically to the raw capture (a capture whose text carries a literal { or } must escape it as {{/}} in the template, since a lone brace is a placeholder token). Both the template's own parse rejections and any fill rejection (an unsupplied required placeholder, a row keyed by an unknown section) surface prefixed with the region name.
{
"schema_version": 1,
"regions": [
{"name": "vault", "type": "template",
"template": {
"schema_version": 1,
"placeholders": {"owner": {}, "item": {}, "count": {}},
"content": {"components": {
"minecraft:custom_name": {"text": "{owner}'s Vault"},
"minecraft:lore": [
{"template_repeat": "entries", "text": "{item} x{count}", "color": "gray"}
]}}},
"values": {"owner": "Aerh"},
"rows": {"entries": [
{"item": "Diamond", "count": "64"},
{"item": "Emerald", "count": "12"}
]}}
]
}A content template turns a captured item-NBT payload - the same JSON the tooltip generator's parseNbtJson accepts - into a reusable shape by marking its variable text with {placeholder} tokens and its repeatable rows with template_repeat. Authoring a new menu is transcribing a capture and marking it, not writing code.
ContentTemplate template = ContentTemplate.parse("""
{
"schema_version": 1,
"placeholders": {"owner": {}, "item": {}, "count": {}},
"content": {
"components": {
"minecraft:custom_name": {"text": "{owner}'s Vault"},
"minecraft:lore": [
{"template_repeat": "entries", "text": "{item} x{count}", "color": "gray"}
]
}
}
}
""");
JsonObject nbt = template.fill(
Map.of("owner", "Aerh"),
Map.of("entries", List.of(
Map.of("item", "Diamond", "count", "64"),
Map.of("item", "Emerald", "count", "12"))));
GeneratedObject image = new MinecraftTooltipGenerator.Builder()
.parseNbtJson(nbt)
.build()
.render(null);fill layers each row's values over the globals and returns a fresh tree every time, so it feeds straight into parseNbtJson and on into the render pipeline. Every content token must be declared and every section name is unique, both checked at parse time, so a template that parses is guaranteed to fill without a structural surprise. The core guarantee: a template filled with the capture's own values reproduces the capture exactly - glyph runs, kern runs, per-segment fonts, and shadow_color all stay byte-verbatim, and only the marked tokens and rows change.
- Tooltip rendering with full Minecraft formatting code support (colors, bold, italic, obfuscation, strikethrough, underline), hex colors, gradients, text wrapping, rarity coloring, and configurable padding and borders
- Resource pack support to render with a loaded resource pack's item textures, item model definitions, custom fonts, tooltip styles, and glyphs (for example, Hypixel SkyBlock), with vanilla as the default
- Inventory rendering with item placement, stack counts, titles, and configurable rows and columns
- Container screen rendering of generic chest menus with pack backgrounds, title-glyph menu art, and a strict JSON recipe format for menu transcriptions
- HUD line rendering of bossbar-anchored text stacks, pack glyph art included
- Item rendering with enchantment glint animations, hover effects, durability bars, and colored overlays
- Player head rendering from player names, texture URLs, base64 data, or hex texture hashes
- Scene composition of several generators into one image via template-free row/column/grid arrangements or a declarative JSON layout with anchored regions, plus a capture-driven content template engine
- NBT parsing that auto-detects multiple Minecraft NBT formats (1.20.5+ components, 1.13-1.20.4 post-flattening, and pre-1.13)
- Animation via animated GIF output with frame-by-frame compositing
- Caching of rendered objects via Caffeine
- Alternate fonts including Galactic (Standard Galactic Alphabet) and Illageralt
- Java 25
- Maven
Add the JitPack repository:
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>Add the dependency:
<dependency>
<groupId>com.github.Aerhhh</groupId>
<artifactId>MinecraftImageGenerator</artifactId>
<version>master-SNAPSHOT</version>
</dependency>repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
implementation 'com.github.Aerhhh:MinecraftImageGenerator:master-SNAPSHOT'
}mvn clean packageMinecraft font files are not included in the repository. They are generated at build time using minecraft-fontgen. When consuming this library via JitPack, fonts are generated automatically.
For local development, you need Python 3.10+ installed. Then run:
mvn -pl generator -Pgenerate-fonts packageThis installs minecraft-fontgen and generates all font styles into the resources directory. You can specify a Minecraft version with:
mvn -pl generator -Pgenerate-fonts -Dmc.version=26.1 packageThe default is latest, which resolves to the most recent Minecraft release.
MinecraftImageGenerator/
├── generator/ # Core image generation library
│ └── src/main/
│ ├── java/ # Source code
│ └── resources/ # Fonts, spritesheets, textures, JSON configs
├── tooling/ # Asset pipeline tools (spritesheet generation, item rendering)
└── jitpack.yml # JitPack build configuration
- Marmalade - Shared image utilities
- Caffeine - Caching
- Gson - JSON parsing
- SLF4J - Logging
- Lombok - Boilerplate reduction
The project includes a GitHub Actions workflow for generating and updating Minecraft item spritesheets and overlays. This is triggered manually via workflow_dispatch and supports:
- Downloading Minecraft assets for any version
- Rendering items at configurable sizes (requires .NET 10)
- Generating sprite atlases with coordinate metadata
- Generating item overlays for colored variants (armor, potions, etc.)

















