6.1 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What this is
Standalone tool that produces bitmap fonts (.gif + .fnt) for the "Projecte 2026" game engine.
font_gen.py— pure rasterising core + CLI. The two public functions arebuild_font_bitmap(TTF → in-memoryFontBitmapResult, no disk I/O) andsave_font_files(writes.gif+.fnt). The CLI wrappersrender_font(TTF) andrender_gif_font(GIF) layer logging on top.font_gen_gui.py— Tkinter GUI that imports the core, runsbuild_font_bitmapon every input change (debounced 200 ms) for live preview, and callssave_font_fileson GENERAR.
The two files are independent: copy them anywhere and run. There is no install step, no test suite, no linter config.
Running
pip install Pillow # only mandatory dependency
python3 font_gen.py # GUI (no args)
python3 font_gen.py --ttf foo.ttf --size 8 --output foo # CLI from TTF
python3 font_gen.py --gif foo.gif --output foo --columns 16 \
--box-width 10 --box-height 7 --cell-spacing 1 # CLI from existing GIF
magick (ImageMagick) is invoked optionally inside save_font_files to clamp the GIF palette to 2 colours; if missing it is silently skipped.
Default output directory is next to the script (os.path.dirname(__file__)). The CLI accepts --dir to override; the GUI always writes alongside the script.
Output contract (don't break these)
These properties are consumed by the engine and must hold for any change to the rasteriser:
- Indexed GIF, palette index 0 = background, index 1 = glyph. Compatible with
SurfaceSprite::render(1, color)in the engine. The post-processmagick … -colors 2 GIF87:enforces a 2-colour palette when available. - Character set order is fixed.
ALL_CHARS= ASCII 32–126 followed by the ES/CA/VA extension list, in that exact sequence..fntrows and GIF cells are addressed by this order; reordering breaks every existing.fntconsumer. .fntformat: header keys (box_width,box_height,columns, optionalcell_spacing, optionalrow_spacingonly when it differs fromcell_spacing), blank line, then<codepoint_decimal> <visual_width> # <name>per supported char. See_write_fnt.- Visual width is measured from pixels, not from the font's typographic advance — last column containing an opaque pixel + 1.
getlength()is only used to size the canvas.
TTF rendering: the non-obvious bits
.notdefdetection: some TTFs return a substitute glyph (with positive advance and a valid bbox) for missing codepoints, which would falsely pass thegetlength(ch) >= 1check. The tool rendersU+FFFEas a reference and rejects any char whose pixels match it. See_is_notdefinsidebuild_font_bitmap.- Fallback table (
CHAR_FALLBACKS) maps accented ES/CA/VA chars to ASCII equivalents (À→A,ñ→n, …). When a TTF lacks the accented glyph, the fallback is drawn into the cell but the original codepoint is recorded in the.fnt, so localised text still resolves at runtime. - Vertical placement:
draw.text((-bbox[0], y_offset), …)wherey_offset = -min(getbbox.top)over all chars to draw, so the topmost pixel of any glyph (including the diacritic on À/É/Ç) lands at y=0 in the scratch. Don't revert this tocap_topsaverage — that variant clips accents above the cap line. The.notdefdetection uses a separate tentativenotdef_scratch_h/notdef_y_offset(consistency between reference and per-char render is all that matters there); the realscratch_handy_offsetare recomputed fromgetbboxspans after classification. box_widthauto-mode usesmax(getlength)across rendered chars; override with--box-widthfor pixel-exact bitmap strikes.box_heightauto-mode is pixel-tight, notascent + descent. Glyphs are rendered to a scratch canvas sized to the bbox span, thenbox_height = max_opaque_y - min_opaque_y + 1across all rendered glyphs (andy_crop = min_opaque_yis applied during the second pass). This trims unused leading/descent space — for many bitmap fonts the typographic line height reserves rows that no glyph actually paints. Override with--box-heightto disable.
GIF mode quirks (CLI-only)
cell_spacingis both the inter-column gap and the top/left border. Origin of cell(col, row)is(col*(bw+cs)+cs, row*(bh+rs)+cs)— seerender_gif_font.- Empty cells (all index 0) are marked unsupported and dropped from the
.fnt, except whitespace chars which get a default width ofbox_width // 2. - The source GIF is
shutil.copy2'd unchanged to the output dir; only the.fntis regenerated. - This mode is intentionally not exposed in the GUI — it's a maintenance path for re-measuring an existing bitmap font, not a primary workflow.
GUI specifics
- Live preview: every change to TTF / size / advanced options schedules
_update_previewviaself.after(200, …)(debounced). Render runs on the main thread — for typical pixel-font sizes it is sub-second. ImageTk.PhotoImagelifetime: stored onself._preview_img_tkto prevent Tk from garbage-collecting the image and showing a blank label. If you refactor preview rendering, keep this reference alive.- Preview palette is for display only: index 0 is recoloured white and index 1 black so the user can read the glyphs on screen. The image saved to disk uses the original palette (index 1 = white) —
_render_previewoperates on a copy. - Preview scale: nearest-neighbor upscale by
min(8, PREVIEW_MAX_W // w, PREVIEW_MAX_H // h). Pixel-perfect, never blurred.
Entry point dispatch
In font_gen.py __main__: len(sys.argv) == 1 launches the GUI directly (no argparse). Anything else goes through main(), which honours --gui to launch the GUI explicitly. The GUI is imported lazily inside _launch_gui() so CLI-only invocations don't require tkinter.