gsfnt
CLI tool that packs all images in a directory into a BMFont text .fnt file plus a single merged RGBA PNG atlas. Each glyph’s Unicode code point is taken from the first character of the image filename stem (the part before the extension).
Install
Or build from this repository:
# binary: target/release/gsfnt
Usage
This writes:
font.fnt— AngelCode-style BMFont (text format)font.png— atlas image referenced by the.fnt
If --output already ends with .fnt, that path is used as-is and the PNG is the same path with .png.
Options
| Option | Description |
|---|---|
-i, --input |
Directory containing source images |
-o, --output |
Output prefix (see above) |
--max-width |
Maximum row width for shelf packing (default: 4096) |
--align |
Vertical alignment direction: top, center, bottom (default: bottom) |
--align-by |
What the alignment is measured against: auto, box, ink (default: auto) |
--ink-threshold |
Alpha at or above which a pixel counts as ink, 1-255 (default: 128) |
--padding |
Transparent gap in pixels between glyphs in the atlas (default: 1) |
Supported image extensions
png, jpg, jpeg, webp, gif, bmp
Filename → glyph
A.png→ glyph forA(U+0041)中.png→ glyph for中emoji_rest.png→ glyph for the first character only (e)
Duplicate code points (two files mapping to the same first character) are rejected with an error listing both paths.
On macOS and Windows filenames are case-insensitive, so a.png and A.png cannot live in the same directory. Characters that are illegal in filenames (/, :) cannot be expressed either.
Vertical alignment
--align picks the direction; --align-by picks what is aligned.
boxcompares the source image rectangles.inkcompares the glyphs' visible pixels, ignoring anything fainter than--ink-threshold.auto(the default) usesboxwhen every source image has the same height, andinkwhen they differ.
The distinction matters because art exported with "trim transparent pixels" leaves each glyph a different height, and a single almost-invisible antialiased row is enough to shift a glyph by one pixel. Aligning image boxes then makes a line of digits visibly wobble. Aligning the ink does not. When all sources share one canvas size, box is used so that deliberate empty space (descenders, for instance) is preserved.
The tool prints which basis it picked.
Font metrics (output)
lineHeight/base= maximum image height among all glyphsxoffset= 0xadvance= glyph widthyoffset, with--align-by box=lineHeight - heightforbottom,0fortop, half the leftover forcenteryoffset, with--align-by ink= the shift that puts every glyph's ink edge on the same row; the largest edge is the anchor, so no offset is negative
A glyph's box may reach past lineHeight under ink alignment, which is a normal descender in BMFont terms.
Godot notes
The common line is written with alphaChnl=0 redChnl=0 greenChnl=0 blueChnl=0, which is what Godot needs to import the atlas as an RGBA color font. Any other value for alphaChnl makes Godot read the font as monochrome-with-outline, rebuild it from the red channel and force it white, which destroys colored fonts. Versions up to 0.1.1 wrote alphaChnl=1 and need this line corrected by hand.
--padding defaults to 1 because Godot scales bitmap fonts with linear filtering, and touching glyphs bleed into each other when scaled.
License
MIT. See LICENSE.