Files

6.1 KiB
Raw Permalink Blame History

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 are build_font_bitmap (TTF → in-memory FontBitmapResult, no disk I/O) and save_font_files (writes .gif + .fnt). The CLI wrappers render_font (TTF) and render_gif_font (GIF) layer logging on top.
  • font_gen_gui.py — Tkinter GUI that imports the core, runs build_font_bitmap on every input change (debounced 200 ms) for live preview, and calls save_font_files on 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-process magick … -colors 2 GIF87: enforces a 2-colour palette when available.
  • Character set order is fixed. ALL_CHARS = ASCII 32126 followed by the ES/CA/VA extension list, in that exact sequence. .fnt rows and GIF cells are addressed by this order; reordering breaks every existing .fnt consumer.
  • .fnt format: header keys (box_width, box_height, columns, optional cell_spacing, optional row_spacing only when it differs from cell_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

  • .notdef detection: some TTFs return a substitute glyph (with positive advance and a valid bbox) for missing codepoints, which would falsely pass the getlength(ch) >= 1 check. The tool renders U+FFFE as a reference and rejects any char whose pixels match it. See _is_notdef inside build_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), …) where y_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 to cap_tops average — that variant clips accents above the cap line. The .notdef detection uses a separate tentative notdef_scratch_h / notdef_y_offset (consistency between reference and per-char render is all that matters there); the real scratch_h and y_offset are recomputed from getbbox spans after classification.
  • box_width auto-mode uses max(getlength) across rendered chars; override with --box-width for pixel-exact bitmap strikes.
  • box_height auto-mode is pixel-tight, not ascent + descent. Glyphs are rendered to a scratch canvas sized to the bbox span, then box_height = max_opaque_y - min_opaque_y + 1 across all rendered glyphs (and y_crop = min_opaque_y is 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-height to disable.

GIF mode quirks (CLI-only)

  • cell_spacing is both the inter-column gap and the top/left border. Origin of cell (col, row) is (col*(bw+cs)+cs, row*(bh+rs)+cs) — see render_gif_font.
  • Empty cells (all index 0) are marked unsupported and dropped from the .fnt, except whitespace chars which get a default width of box_width // 2.
  • The source GIF is shutil.copy2'd unchanged to the output dir; only the .fnt is 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_preview via self.after(200, …) (debounced). Render runs on the main thread — for typical pixel-font sizes it is sub-second.
  • ImageTk.PhotoImage lifetime: stored on self._preview_img_tk to 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_preview operates 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.