Skip to content

About

A library for generating Minecraft item tooltips, inventory displays, and player head images

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

MinecraftImageGenerator

A Java library for programmatically generating Minecraft-themed images: item tooltips, inventories, individual items, and player heads, as static PNGs or animated GIFs.

JitPack Java Maven


Legendary item tooltip with full formatting    Animated enchantment glint    Rendered player head

Everything below was rendered by the library itself. Each snippet is the actual code that produces the image next to it.

Showcase

Item tooltips

Full Minecraft formatting: colors, bold, italic, strikethrough, underline, obfuscation, text wrapping, rarity coloring, and configurable padding and borders.

Legendary tooltip
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();

Gradient and hex color text

Use %%gradient:#start:#end%%...%%/gradient%% for smooth per-character gradients, and &#RRGGBB for inline hex colors.

Gradient and hex color tooltip
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
        &#7fdbff❉ Clarity: &#7fdbff+250""")
    .withRenderBorder(true)
    .build();

Alternate fonts

Render text in the Standard Galactic Alphabet (&g) or Illageralt (&h).

Standard Galactic Alphabet tooltip
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();

Items and enchantment glint

Render any vanilla item from its id, optionally with an animated enchantment glint. When a generator is animated, getGifData() returns the encoded GIF.

Diamond sword item      Animated enchant glint
// 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());

Inventories

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 %%.

Inventory grid with items and stack counts
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();

Player heads

Render a 3D player head from a player name, texture URL, base64 texture data, or a texture hash.

Rendered player head
new MinecraftPlayerHeadGenerator.Builder()
    .withSkin("Aerh") // name, texture URL, base64, or hash
    .withScale(-2)    // positive upscales, negative downscales
    .build();

Quick start

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"));
}

Resource pack support

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):


Verdant Edge, a rare sword

Whisperwind, an epic longbow

Wrathflame Greatsword, a legendary greatsword

Maelstrom, a mythic staff

Soulrend, a supreme demon blade with animated obfuscated text

Heart of the Frozen Star, a special relic

Aeon the Last Hour, an ultimate artifact
  • Vanilla (minecraft:minecraft) is always available and is the default, so withPack is opt-in.
  • Pack ids are namespace:name at 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's minecraft: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.

Server pack support

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.

Loading large packs

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 maxEntries counts depends on the source type: a zip counts every central-directory record (regular files and directory entries), a directory source counts only regular files under assets/. Size the limit comfortably above the pack's record count.
  • Sheet-shaped textures (font glyph sheets, animated tooltip sprite strips) decode under the separate sheetTextureMaxDim cap (default 8192); item and general GUI textures keep the strict maxTextureDim image-bomb guard.

Pack variants

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 variantOf id, 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 variantOf pack does not have to be registered; only one level is followed. ResourcePackService users set variantOf on the PackDefinition.

Pack fonts in text

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: bitmap sheets at any declared height and ascent, space providers with negative or fractional advances (how packs kern glyph art together), reference expansion, and filter objects.
  • One deliberate deviation from vanilla: a bitmap provider whose sheet texture is absent from the pack is skipped with a warning instead of failing the whole font. Real packs override minecraft:default with 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.

Tooltip styles

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.

Container screens

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 * rows GUI px, the top-left slot interior sits at (8, 18) with an 18 px pitch, and the title draws unclipped at (8, 6) in #404040 with 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 enchant and hover included) plus an optional trailing :amount; pack items needing custom model data use withSlot(int, String, CustomModelData).

Menu recipes

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 takes text (required), font (a resource location, e.g. a pack font), color (strict #RRGGBB), and boolean bold / 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.

Pack item models

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..layer4 textures stack bottom-to-top with tint i coloring layer i, vanilla's item model baking. Elements-based models rasterize directly at the target resolution, so sub-pixel geometry survives at any scale. Identity and mirrored display.gui rotations use exact flat projections; withFullGuiRotations(true) renders every other rotation as the true orthographic projection of the 3D model (see Limitations).
  • Per-element rotation entries (any angle about one axis - the modern 1.21.6+ rule; origin and rescale included) render with vanilla semantics - the geometry decorative medals and tilted props are built from.
  • gui_light: side models (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: front and shade: false elements 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_gui items 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_dispatch and composite nodes are supported, along with constant, custom_model_data and dye (rendered at its required default color) tint sources.
  • withItemDamage(damage, maxDamage) feeds range_dispatch nodes with property: minecraft:damage: normalize: true (the vanilla default) evaluates the 0..1 damage fraction, normalize: false the 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/generated or minecraft:item/handheld without 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.
  • MinecraftNbtParser picks the component up straight from 1.20.5+ NBT: an item whose components carry minecraft:item_model is addressed by that model instead of by its id, and the value is exposed on ParsedNbt.getParsedItemModel(). The model wins over the player head render even when minecraft:profile is 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.

HUD lines

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 / 2 integer 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 with withGuiWidth(...).
  • Runs without an explicit color draw in white with a drop shadow, matching vanilla bossbar names.
  • TitleRun is the same styled-run type the container title uses, so pack fonts, colors, bold and italic behave identically in both.

Animated textures

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-frame time overrides frametime), default top-to-bottom frame order when the list is absent, and width/height frame-size overrides. interpolate: true cross-fades like the vanilla client: each keyframe expands into one blended frame per tick, linearly interpolating the previous keyframe toward the next by subTick / 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 and AnimationTimeline.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 &k text 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 BufferedImage exactly as before.

Limitations

Documented edges of the pack renderer, all deliberate:

  • Fonts: ttf providers render as real vector glyphs (Tier 1): the pack's font file is rasterized with plain Java2D at ppem = round(size * oversample) (antialiasing off), scaled down by 1 / oversample to GUI pixels, and placed on the vanilla baseline honoring the provider shift. Each pack's font is a private java.awt.Font instance (never globally registered), so packs shipping same-named fonts never collide. A missing or unloadable ttf, plus every unihex and legacy_unicode provider, still parses but does not render - those codepoints fall back to the built-in fonts (a warning is logged once per font). bitmap providers 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.ttf at 28) reproduce exactly; non-pixel outlines render blocky since the downscale is nearest-neighbor with no coverage averaging. The full vanilla MinecraftGlyphVector path (glyph-index selection, setGlyphPosition overlap, Area outline 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 &k obfuscation rather than scrambling among equal-width glyphs (that pool is bitmap/space only, to avoid a full-font scan).
  • Model rotations: display.gui rotations 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; interpolate cross-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.

Scene composition

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.

Row, column, and grid

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();

JSON scenes

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"}
     ]}}
  ]
}

Content templates

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.

Features

  • 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

Requirements

  • Java 25
  • Maven

Installation

Maven (via JitPack)

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>

Gradle (via JitPack)

repositories {
    maven { url 'https://jitpack.io' }
}

dependencies {
    implementation 'com.github.Aerhhh:MinecraftImageGenerator:master-SNAPSHOT'
}

Building

mvn clean package

Font generation

Minecraft 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 package

This 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 package

The default is latest, which resolves to the most recent Minecraft release.

Project structure

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

Dependencies

Asset pipeline

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.)

About

A library for generating Minecraft item tooltips, inventory displays, and player head images

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages