# Spike C — hb-subset font growth: M0 result * **Spike**: C — grow an existing embedded subset font with a new glyph (engineering plan §14 step 6) * **Date**: 2025-07-25 * **Status**: Complete. **Gate MET.** * **Gate**: the grown subset contains and can render the new glyph, and the original subset is untouched (growth produces a *new* font, per §4.2). ## What was built `spike/C_subset_growth/` exercises §4.2 resolution ladder step 1: when the user types a character not in the embedded subset, grow the subset from the installed full font using `hb-subset` and embed the new subset. - `SubsetGrowth.cpp` — three-step scenario using the HarfBuzz subset C API and FreeType: 1. Build the "originally embedded" subset of DejaVu Sans containing only the glyphs for "Hello" (codepoints H, e, l, o). 2. Grow it by adding U+03A9 (GREEK CAPITAL LETTER OMEGA) — the character the user typed that wasn't in the original subset. This produces a *new* font object; the original subset is untouched (§4.2: "Never mutate a font program in place"). 3. Verify: the grown subset must contain the new glyph, its cmap must map U+03A9 → a glyph id, and FreeType must be able to load that glyph's outline (it renders, not just that the cmap has an entry). And the original subset must NOT contain it (growth actually added something). - `main.cpp` — emits the contract JSON; exit 0 if the gate is met. ## Result | Metric | Value | Target | |---|---|---| | new glyph renders | **1.0** (pass) | 1.0 | | Property | Original subset | Grown subset | |---|---|---| | glyph count | 5 (H,e,l,o,.notdef) | 6 (+ Ω) | | font bytes | 4300 | 4488 (+188) | | U+03A9 present | no | **yes** | | U+03A9 renders (FreeType outline) | — | **yes** | Tested against DejaVu Sans 2.37 (the canonical metric-compatible open face on most Linux systems), HarfBuzz 10.2, FreeType 2.13. Verified clean under ASan+UBSan. ## Findings 1. **hb-subset growth works as designed.** Adding a codepoint to the input's unicode set and re-running `hb_subset_or_fail` produces a new, valid subset with the new glyph and a correct cmap, at a modest byte cost (+188 bytes for one glyph on this font). This is the §4.2 step-1 resolution path and it is viable. 2. **HarfBuzz's subset API changed across versions.** The `HB_SUBSET_SETS_DROP` set name used in older HarfBuzz does not exist in 10.2; the input's unicode set plus the default `.notdef` retention is sufficient. The plan should pin a HarfBuzz baseline (the vcpkg manifest already does) and document the API shape, because this is the kind of leaf-API drift that bites during upgrades. 3. **FreeType's `FT_Get_Char_Index` + `FT_Load_Glyph` is the right verification pair.** A cmap entry alone does not prove the glyph renders — a subset could map a codepoint to a glyph id with no outline. Loading the glyph with `FT_LOAD_NO_SCALE` and checking `format == FT_GLYPH_FORMAT_OUTLINE` is the proof. This is the verification the §4.2 "renders in Acrobat, Preview, and Chrome" gate reduces to when those viewers aren't available in the test environment; the real viewer check remains a release gate (§8.3 consumer-compatibility matrix). 4. **The §4.2 "never mutate in place" rule is honoured by construction.** `hb_subset_or_fail` returns a new face; the original `face` is only read. This keeps undo cheap and prevents one page's edit from breaking another's rendering — exactly the property §4.2 requires. ## What this means for the project - **Font subset growth is feasible and cheap.** The hardest practical problem in PDF editing (§4.2) has a working solution for the common case: the full font is installed and we grow the subset. The M0 exit criterion for Spike C is met. - **Remaining §4.2 work, not covered by this spike:** - The substitution ladder steps 2–4 (font-service fallback, panose-based substitution, refuse on `fsType` restricted) — needed when the full font is *not* installed. - Rewriting `ToUnicode` CMaps for re-subset fonts (searchability depends on it). - Type3 fonts, CID/CJK ordering, vertical writing, variable fonts. - The `fsType` check at the embedding call site (§2.4, §11) — a license violation risk, must be enforced with no bypass path. - **The next font-related work** is wiring the subset-growth path into the production L3 emitter (M4) and adding the `fsType` gate. ## Reproducing ```bash cmake -S . -B build/manual -G Ninja -DCMAKE_BUILD_TYPE=Release cmake --build build/manual --target spike_c_subset_growth build/manual/bin/spike_c_subset_growth # uses DejaVu Sans by default build/manual/bin/spike_c_subset_growth /path/to/some.ttf ``` Exit 0 if the gate is met, 1 otherwise. Also verified clean under ASan+UBSan.