freepdfeditor/docs/spike-results/0003-spike-c-subset-growth.md

4.9 KiB
Raw Permalink Blame History

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 24 (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

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.