Skip to content

feat(fonts): face-scoped font substitution so single-face clones are safe (SD-3372)#3634

Open
caio-pizzol wants to merge 1 commit into
caio/fonts-write-apifrom
caio/sd-3372-featfonts-face-scoped-font-substitution-single-face-safe
Open

feat(fonts): face-scoped font substitution so single-face clones are safe (SD-3372)#3634
caio-pizzol wants to merge 1 commit into
caio/fonts-write-apifrom
caio/sd-3372-featfonts-face-scoped-font-substitution-single-face-safe

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

Makes font resolution face-aware so a single-face substitute is never faux-styled onto a weight/style it lacks. Today the resolver maps logical->physical by family only, so a customer fonts.map to a Regular-only font (or a bundled single-face clone) would route bold/italic runs to it and the browser would synthesize a faux face, drifting advances and breaking the line-break guarantee that is the point of a metric clone. Resolver safety only; no new fonts ship here.

  • A substitute applies only when it provides the run's weight/style (consulted from the registry's bundled + fonts.add() faces); otherwise the logical family passes through, reported fallback_face_absent and never faux-styled.
  • One stored FontPlan per render is the single source for load (requiredFaces), diagnostics (usedFaces), measure/paint resolution, and cache identity (effectiveSignature). The effective signature replaces resolver.signature at every rendered-layout cache site, because face availability can change a face's resolution for an unchanged family map; resolver.signature now only detects map changes (in DocumentFontController).
  • Measure, paint, field-annotation pills, and per-rId header/footer all resolve face-aware.
  • Additive public surface: FontResolutionRecord.face (optional) + FontResolutionReason gains fallback_face_absent. The report now has per-face rows; declared-but-unused fonts stay as family-level rows (face undefined).
  • Bacasime / Caprasimo (and any single-face font) are NOT shipped here.

Stacked on #3631; do not open to main.

Review: confirm no resolver.signature remains in a measure/resolve/reuse cache path (it should only key map-change detection).
Verified: pnpm check:types clean; pnpm check:public:superdoc green (13 stages, no export-snapshot growth). Unit + behavior/layout/visual run in CI.

…safe (SD-3372)

The resolver mapped logical->physical by family only, so a single-face substitute (a customer fonts.map to a Regular-only font, or a bundled single-face clone) routed bold/italic runs to it and the browser faux-synthesized the missing face, drifting advances and breaking the line-break guarantee that is the point of a metric clone.

Resolution is now face-aware: a substitute applies only when it provides the run's weight/style (consulted from the registry's bundled + fonts.add() faces), else the logical family passes through, reported fallback_face_absent. One stored FontPlan per render drives load (requiredFaces), diagnostics (usedFaces), measure/paint resolution, and cache identity (effectiveSignature), replacing resolver.signature at every rendered-layout cache site since face availability can change resolution without changing the family map. resolver.signature now only detects map changes.

Additive public change: FontResolutionRecord.face (optional) + FontResolutionReason 'fallback_face_absent'. Stacks on #3626 + #3631; no font files shipped here.
@caio-pizzol caio-pizzol requested a review from a team as a code owner June 4, 2026 05:01
@linear-code
Copy link
Copy Markdown

linear-code Bot commented Jun 4, 2026

SD-3372

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant