Skip to content

feat(engines): font resolver, HarfBuzz shaper and Unicode tables for the layout engine - #695

Merged
KnifeLemon merged 4 commits into
builtin-enginesfrom
engine/font-shaper
Oct 28, 2026
Merged

KnifeLemon merged 4 commits into
builtin-enginesfrom
engine/font-shaper

Conversation

@KnifeLemon

Copy link
Copy Markdown
Owner

What does this change?

Implements the Project 4 item "Layout engine: font resolver, HarfBuzz shaper and Unicode tables" (M0, P0). These are the text services that the paragraph layout, CJK typography and bidi items build on. Everything is in src/Filee.Engines/Typesetting and runs in-process on what Filee already references: SkiaSharp's font manager, HarfBuzzSharp and .NET. No converter uses it yet.

  • Unicode tables (UnicodeData.cs + generated UnicodeData.Generated.cs): Line_Break, Bidi_Class, Script and East_Asian_Width for every code point, as range tables. Unassigned code points get the UCD's @missing defaults (ID for CJK blocks, R / AL for the right-to-left blocks, ...).
    • build/generate-unicode-tables.ps1 downloads the pinned UCD files from unicode.org into build/.cache and checks their SHA-256, like fetch-engines.ps1. The UCD files are not committed; the generated C# is.
    • Version: Unicode 16.0.0, the one .NET 10 uses for CharUnicodeInfo, Rune and StringInfo. A test walks every code point to check that the tables and .NET agree on what is assigned.
    • The Unicode License v3 notice is added to THIRD-PARTY-NOTICES.md.
  • Font slots (FontSlots.cs): character → 한글's seven language slots (HLanguage) and Word's ascii / hAnsi / eastAsia / cs fonts, including w:hint="eastAsia" for ambiguous-width characters. A grapheme cluster takes the slot of its first character, so marks stay with their base.
  • Font resolver (FontResolver.cs, FontNames.cs, SystemFonts.cs, FontCollection.cs):
    • Installed fonts come through SKFontManager (DirectWrite, CoreText, fontconfig). Nothing is downloaded or shipped.
    • Other names: 한글's Korean names ↔ English names (함초롬바탕 ↔ HCR Batang, 맑은 고딕, 바탕/굴림/돋움/궁서, the HY and Nanum fonts; checked against the fonts' name tables). 한글's built-in fonts (한양신명조, 한양중고딕, ...) map to the HY TrueType fonts of the same design. Metric-compatible replacements: Arial → Liberation Sans, Calibri → Carlito, ...
    • Style words in names ("Pretendard Medium") and CSS-style weight / slant matching. A missing bold or italic is reported as synthetic for the drawing to fake.
    • Per-cluster fallback, in this order: the run's font, fonts already used in the run, a list per script, then the platform's MatchCharacter. Han characters follow the text's language. Punctuation and spaces stay with their neighbours' font.
  • Shared rules (FontSubstitution.cs): the language, coverage and unusable-font rules move out of Hwp/RhwpFonts.cs unchanged. RhwpFonts delegates to them, so rhwp behaves as before, and RhwpFontsTests / RhwpInputTests pass unchanged. InstalledFont now derives from the shared FontCoverage.
  • Shaper (Shaper.cs): HarfBuzz shaping generalised from Vector/GlyphOutlines.cs.
    • A run is shaped with up to five characters of context on either side, so a word split into two runs still joins (Arabic) and reorders (Indic).
    • Results are in font units and cached by (font, features, script, direction, language, run text with context).
    • Each result has glyph IDs, advances, offsets, the cluster map (visual order, as PageContent takes it) and HarfBuzz's unsafe-to-break flags.
    • ShapedRun.ToGlyphRun makes the page model's GlyphRun. PdfPageWriterTests now builds its page with it instead of its own HarfBuzz code.
    • Shaper.Metrics gives the font's metrics: ascender / descender / line gap, Win metrics, x-height, cap height, underline, strikeout and super / subscript.
  • docs/ENGINES.md: new section "Filee renderer: fonts, shaping and Unicode tables". The PDF section's "Still open" list now records the decisions below.

The namespace is Typesetting, not Layout: Filee.Engines.Layout would hide ACadSharp's Layout type in the CAD code and tests.

Decisions (open questions from #683)

  1. Variable fonts: shaped, subset and drawn at their default instance. HarfBuzzSharp 8.3 has no binding for variation coordinates, and Filee has no gvar/HVAR instancer. DirectWrite's named instances of one variable font count as one face, with the weight its OS/2 table gives. A heavier requested weight is reported as synthetic bold. An instancer is still open.
  2. CFF2: used only when no other font for the script is installed, the same rule as for rhwp. When one is used, Skia embeds it as Type 3: still searchable, but not subset.
  3. Unicode version: 16.0.0, as in .NET 10. The HarfBuzz 8.3 inside HarfBuzzSharp uses Unicode 15.1 internally, so scripts new in 16.0 (Garay, Todhri, ...) get HarfBuzz's default shaper until HarfBuzzSharp updates.

Shaping acceptance

Arabic, Devanagari, Gujarati, Thai and Hangul jamo strings written in the tests are checked three ways:

  • Against HarfBuzz itself, with every installed font of the script. Our result must equal the same text shaped the plain way: one buffer, properties guessed by HarfBuzz, serialized by HarfBuzz in hb-shape's format. Glyphs, clusters, advances and offsets all have to match.
  • Against recorded HarfBuzz output for the pinned Noto fonts of build/fetch-fonts.ps1, which CI fetches. There is no pinned Gujarati font, so Gujarati is checked with whichever of Nirmala UI, Shruti, Lohit or Noto is installed.
  • By what each script needs, whatever the font: Arabic joining forms and lam-alef, i vowel signs drawn before their consonant, Indic conjuncts, the Thai SARA AM split, jamo composed into precomposed syllables, and a dotted circle only for a broken cluster.

Open questions

  1. The resolver reports synthetic bold / italic, but GlyphRun can't draw them yet. Should the paragraph layout item or the output item add emboldening / skew to the page model?
  2. For Latin-1 letters, Word's hAnsi ↔ eastAsia rule also depends on the run's East Asian language (w:lang w:eastAsia="zh-*"). Only the hint is used now.
  3. Not verified: CoreText (macOS) and fontconfig matching of Korean names in the real font manager. The fake font list covers the logic, and the portable CI jobs run the same tests with their own fonts.

Type of change

  • Bug fix
  • New feature or improvement
  • New format or engine
  • Translation
  • Documentation
  • Build, CI or dependencies

How was it tested?

  • dotnet format --verify-no-changes
  • dotnet test: Filee.Engines.Tests 2120 passed, 199 skipped. dotnet build Filee.slnx -c Release gives no new warnings.
  • Tried it in the app (what you converted or clicked, and on which OS): not wired to a converter yet. The new tests build their inputs in code. They use the pinned Noto fonts and installed fonts on Windows 11, and skip when a font is missing.

Checklist

  • New UI strings are in all src/Filee.App/Assets/i18n/*.json files (none)
  • New third-party code or binaries are listed in THIRD-PARTY-NOTICES.md (no packages, fonts or programs; the Unicode data notice is added)
  • The PR does one thing (unrelated changes go in a separate PR)

🤖 Generated with Claude Code

KnifeLemon and others added 3 commits October 28, 2026 12:00
Line_Break, Bidi_Class, Script and East_Asian_Width as range tables, generated by
build/generate-unicode-tables.ps1 from the pinned UCD files (SHA-256 checked, not committed).
16.0.0 is the Unicode version .NET 10 uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rules move unchanged to Typesetting/FontSubstitution.cs; RhwpFonts delegates to them and
InstalledFont derives from the shared FontCoverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FontResolver: installed fonts through SKFontManager, 한글 / English names, style matching,
  substitutes for missing fonts, per-cluster fallback; variable fonts at their default instance,
  CFF2 fonts only without another font for the script.
- FontSlots: 한글's seven language slots and Word's four run fonts.
- Shaper: HarfBuzz shaping with context, cached in font units, glyph IDs, advances, offsets,
  clusters and unsafe-to-break flags as the page model takes them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the feature New features label Oct 28, 2026
@KnifeLemon KnifeLemon added the built-in engines Filee: built-in engines (Project 4), on the builtin-engines branch, not released yet label Oct 28, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@KnifeLemon
KnifeLemon merged commit d83f9a2 into builtin-engines Oct 28, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

built-in engines Filee: built-in engines (Project 4), on the builtin-engines branch, not released yet feature New features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant