# Gum Tool Font Generation

> Font Generation Pipeline

- Skill: `vchelaru/gum-tool-font-generation` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/gum-tool-font-generation`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/gum-tool-font-generation/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: vchelaru (https://skillmd.com/u/vchelaru)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vchelaru/gum-tool-font-generation

---


# Font Generation Pipeline

Gum generates BMFont-format bitmap fonts (`.fnt` + `.png` atlas). Pipeline: **collect font properties → build BmfcSave → pick a generator backend → produce .fnt + .png**. Two interchangeable `IFontFileGenerator` backends exist, chosen per-project via `GumProjectSave.FontGenerator` (`FontGeneratorType.BmFont`, the default for back-compat, or `.KernSmith`):

- **`BmFontExeFileGenerator`** shells out to the embedded `bmfont.exe` (Windows-only). Legacy path; does **not** support dropshadow at all — `BmfcTemplate.bmfc` has no dropshadow placeholders, so `BmfcSave.HasDropshadow`/`Dropshadow*` fields are silently ignored on this backend.
- **`KernSmithFileGenerator`** calls the [KernSmith](https://github.com/kaltinril/Kernsmith) library in-process (cross-platform, no external exe, supports dropshadow). This is the newer, recommended backend.

`FontFileGeneratorSelector` picks between them at generation time (project isn't loaded yet when DI wires services up, so selection can't happen at construction).

## Architecture

```
FontManager (tool facade)
  └─ HeadlessFontGenerationService (core logic, headless)
       ├─ BmfcSave (data model + .bmfc serialization)
       │    └─ BmfcTemplate.bmfc (template file with placeholders, bmfont.exe only)
       └─ FontFileGeneratorSelector → BmFontExeFileGenerator | KernSmithFileGenerator
```

**FontManager** is the tool-facing entry point. It wires `IFontGenerationCallbacks` (UI output, spinner, file-watch suppression) and delegates all real work to **HeadlessFontGenerationService** via `IHeadlessFontGenerationService`.

**HeadlessFontGenerationService** is platform-checked for the bmfont.exe backend only (throws `PlatformNotSupportedException` off-Windows when `FontGeneratorType.BmFont` is selected). It owns:
- Collecting all unique fonts a project needs (`CollectRequiredFonts`)
- Deciding whether a font file already exists or needs (re)generation
- Invoking the selected `IFontFileGenerator` — one call per font, all awaited via `Task.WhenAll` for parallelism
- Texture size estimation (heuristic) and optimization (binary search over `AvailableSizes`)

**Texture-size estimation is bmfont.exe-specific and does not run for KernSmith.** `EstimateBlocksNeeded`'s heuristic (and the dropshadow blindness that falls out of it) only matters for `BmFontExeFileGenerator`. `IFontFileGenerator.RequiresSizeEstimation` (false on `KernSmithFileGenerator`, true on `BmFontExeFileGenerator`) gates the whole `AssignEstimatedNeededSizeOn` call in `HeadlessFontGenerationService` — KernSmith instead sizes its own atlas via `FontGeneratorOptions.AutofitTexture`.

**BmfcSave** holds the six font properties (FontName, FontSize, OutlineThickness, UseSmoothing, IsItalic, IsBold) plus ranges, spacing, and output dimensions. Its `Save()` method loads `BmfcTemplate.bmfc` and does string replacement to produce the `.bmfc` file that bmfont.exe consumes. It also owns `FontCacheFileName` which determines the output path.

## Generation Flow

1. **Property collection**: `TryGetBmfcSaveFor` reads font properties from a `StateSave` (with optional instance prefix and forced overrides) and returns a `BmfcSave` or null. For direct Text instances this is sufficient, but for component instances containing Text, `CollectFontsFromNestedTextInstances` recursively descends using `RecursiveVariableFinder` to resolve font properties through exposed variables and inheritance.
2. **Deduplication**: `CollectRequiredFonts` iterates all elements/states/instances and deduplicates by `FontCacheFileName` (a deterministic name encoding all font parameters).
3. **Size estimation**: Before generating, `AssignEstimatedNeededSizeOn` either runs a binary-search optimization (`GetOptimizedSizeFor`, controlled by `AutoSizeFontOutputs` project setting) or uses a heuristic lookup table (`EstimateBlocksNeeded`) based on effective font size.
4. **Template expansion**: `BmfcSave.Save()` loads `Content/BmfcTemplate.bmfc`, replaces placeholders, and writes the `.bmfc` file alongside the target `.fnt` path.
5. **bmfont.exe invocation**: `CreateBitmapFontFilesIfNecessaryAsync` launches `bmfont.exe -c "<bmfc>" -o "<fnt>"` via `Process.Start` with `UseShellExecute = true`. The process is awaited async (`WaitForExitAsync`) or synchronously depending on `createTask`.
6. **File-watch suppression**: Before writing, all expected output paths (.bmfc, .fnt, .png pages) are registered with `IFontGenerationCallbacks.OnIgnoreFileChange` so the file watcher doesn't trigger reloads.

## Font Cache Naming

`BmfcSave.FontCacheFileName` produces paths like `FontCache/Font18Arial.fnt`. The name encodes all parameters that affect output:
- Base: `Font{size}{name}` (spaces in font name → underscores)
- Suffixes: `_o{N}` (outline), `_noSmooth`, `_Italic`, `_Bold`

This means two elements with identical font settings share one cached file.

## Channel Behavior (Outline vs No-Outline)

When OutlineThickness is 0: alpha=0, RGB channels=4 (glyph in alpha channel).
When OutlineThickness > 0: alpha=1, RGB channels=0 (outline uses color channels).

## Texture Size Optimization

`GetOptimizedSizeFor` does a binary search over `AvailableSizes` (32x32 up to 8192x8192) to find the smallest texture that keeps the font on a single page. Each probe generates the font to a temp directory and parses the `pages=` line from the resulting `.fnt` file.

The heuristic fallback (`EstimateBlocksNeeded`) uses a lookup table mapping effective font size to a number of 256-pixel blocks, then factors the block count into width x height.

## Character Ranges

- Default: `32-126,160-255` (ASCII + Latin-1 Supplement)
- Project-level `FontRanges` setting overrides for all fonts
- Ranges are validated (no spaces, proper start < end), with automatic fallback to default
- Space character (32) is always included via `EnsureRangesContainSpace`
- Large range sets are split across multiple `chars=` lines (max 10 blocks per line) for bmfont.exe compatibility
- `GenerateRangesFromFile` can derive ranges from a text file's unique characters

## Embedded Resources

`bmfont.exe` and `BmfcTemplate.bmfc` are embedded in the `Gum.ProjectServices` assembly and extracted on first use by `EnsureToolsExtracted`. The template uses placeholder tokens like `FontNameVariable`, `FontSizeVariable`, `{UseSmoothing}`, etc.

## Standalone CLI

`GumProjectFontGenerator` is a standalone console app that loads a `.gumx` project and generates all missing fonts. It uses `HeadlessFontGenerationService` directly with no callbacks.

## Font Creation Paths

All font generation now routes through `HeadlessFontGenerationService`. There are two call sites:

**On-demand:** `CustomSetPropertyOnRenderable` resolves `IFontManager` via DI (`Builder.Get<IFontManager>()`) and calls `CreateFontIfNecessary(BmfcSave)` when a font property changes. The BBCode path (`GetAndCreateFontIfNecessary`) uses the same pattern for inline font tags.

**Bulk (tool-only):** `CreateAllMissingFontFiles` scans the entire project on load or "regenerate fonts" and generates all missing font files in parallel.

Both delegate to `HeadlessFontGenerationService`. Legacy `IRuntimeFontService`, `GenerateMissingFontsForReferencingElements`, and the embedded bmfont.exe in RenderingLibrary have been removed.

## Key Files

| File | Purpose |
|------|---------|
| `Gum/Services/Fonts/IFontManager.cs` | Interface for the tool-facing font manager |
| `Gum/Services/Fonts/FontManager.cs` | Tool facade — delegates to headless service via DI |
| `Tools/Gum.ProjectServices/FontGeneration/HeadlessFontGenerationService.cs` | Core generation logic — collection, size estimation, bmfont.exe invocation |
| `Tools/Gum.ProjectServices/FontGeneration/IHeadlessFontGenerationService.cs` | Interface for the headless service |
| `Tools/Gum.ProjectServices/FontGeneration/IFontGenerationCallbacks.cs` | Callback interface (output, spinner, file-watch ignore) |
| `Gum/Services/Fonts/ToolFontGenerationCallbacks.cs` | Tool-specific callback implementation |
| `RenderingLibrary/Graphics/Fonts/BmfcSave.cs` | Font data model, .bmfc serialization, cache file naming, range utilities |
| `Gum/Content/BmfcTemplate.bmfc` | Template with placeholders for bmfont.exe config |
| `GumProjectFontGenerator/Program.cs` | Standalone CLI for batch font generation |

## Off Windows: BmFont requests resolve to KernSmith

`bmfont.exe` only runs on Windows, but `GumProjectSave.FontGenerator` defaults to `BmFont` for back-compat. `FontGeneratorResolver` (Gum.ProjectServices) maps the *requested* type to the *effective* one: `BmFont` on macOS/Linux becomes `KernSmith`; everything else passes through. `FontFileGeneratorSelector` and the CLI `fonts` command both go through it, and the selector takes an optional `isBmFontSupported` delegate so tests can exercise the substitution on any OS. KernSmith output differs slightly from bmfont output, so a project generated on Windows and then on macOS will have different `.fnt`/`.png` bytes unless it already uses KernSmith.

