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

107 lines
4.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!--
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: 2025 ai-ad4 and the FreePDFEditor contributors
-->
# 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
```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.