Skip to content

Fix #190 — font lookup: registered, system and bundled faces - #201

Merged
sbeitzel merged 1 commit into
developfrom
feature/fix_190
Oct 3, 2026
Merged

sbeitzel merged 1 commit into
developfrom
feature/fix_190

Conversation

@sbeitzel

@sbeitzel sbeitzel commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Fix #190

What changed

  • FontRequest (public): a family, weight (regular/bold) and style (upright/italic, with oblique counted as italic).

    • init(postScriptName:) parses Times-Roman, Times-BoldItalic, Helvetica-Oblique, Courier-Bold and similar names, and keeps the original name for an exact match.
    • The base 14 and serif/sans-serif/monospace come with stand-in lists, following abcm2ps (serif = Times, sans-serif = Helvetica): the macOS/Windows equivalents, then Liberation, Nimbus (URW) and DejaVu.
  • FontLibrary (public, immutable, Sendable): fonts the host supplies as Data, CFF or TrueType, single fonts or collections (OpenTypeFont: read TrueType (glyf) outlines and font collections (.ttc) #189), parsed once.

    • Set it on SVGRenderConfig.fontLibrary.
    • It is part of the config rather than a process-wide registry, so the same config always gives the same output, and tests don't affect one another.
  • SVGRenderConfig.systemFonts, default false, uses installed fonts on Apple platforms. On Linux there is no system lookup; a host supplies fonts through FontLibrary instead.

  • FontProvider resolves a request:

    • It searches the host's fonts, then system fonts, then the bundled ones. For each family (the requested one first, then its stand-ins), an exact match from any source beats a near match.
    • A face whose fsType forbids embedding is skipped when outlining.
    • Results are cached per render.
    • The emitter's own Bravura and Libertinus Serif requests go straight to the bundled fonts, so existing output is unchanged. The snapshot tests pass.
  • FontResolution (public) records the face used, its origin and any faces refused. diagnostics(at:) produces two new codes:

    • .fontSubstituted: an info note when a stand-in answered, otherwise a warning;
    • .fontNotEmbeddable: a warning.

    Support the §11.4.2 font directives (%%titlefont, %%vocalfont, %%wordsfont, …) #186 will report these against the directive that named the font.

  • FaceKey is now an open-ended key. The bundled fonts keep their <defs> ids, and other fonts get f-<PostScript name>.

  • TextOutliner.outline(_:font:fontSize:fill:library:systemFonts:) draws text in a named font. OutlinedText.resolution reports which face was used.

  • OpenTypeFont now also reads the family name (name ID 16, else 1), usWeightClass and the italic flag. Where a font has no OS/2 table (macOS's own Courier.ttc), weight and italic come from head.macStyle.

A deliberate departure from the issue

The issue suggested converting CoreText glyph paths (CTFontCreatePathForGlyph → CGPath → GlyphPath). Instead, CoreText is used only to find the font file and face name, and the file is read by our own parser from #189. As a result, every face, wherever it came from, goes through the same measuring, outlining and fsType checks, and a system font draws exactly as the same file supplied by a host would. A system font in a format we can't read is skipped.

Lookups pass the family or PostScript name as a mandatory attribute. Otherwise CoreText answers any query, including one for a family that isn't installed, with some fallback face (usually Helvetica).

Tests (FontProviderTests, 14)

  • PostScript-name parsing and stand-in lists.
  • A host-supplied fixture font (from OpenTypeFont: read TrueType (glyf) outlines and font collections (.ttc) #189) is drawn when named, by PostScript name or by family: the fixture's "A", not Libertinus's. Its diagnostics are empty.
  • A request for a weight the family lacks is answered by the nearest face, flagged as a substitute.
  • A restricted (fsType 0x0002) face is refused, producing fontNotEmbeddable and fontSubstituted.
  • An unreadable font in the library is reported by its position.
  • With nothing supplied and system lookup off, the bundled fonts answer everything, with a warning.
  • The bundled fonts keep their keys.
  • SVGBuilder writes f-CeolKitTest-Regular-g… glyph definitions for a host-supplied font.
  • macOS:
    • with system lookup on, Courier-Bold resolves through CoreText to the system's Courier-Bold and is outlined (0.6 em monospaced advances);
    • serif italic → Times-Italic, reported as an info note;
    • system lookup is off by default;
    • a family that isn't installed is not answered by CoreText's fallback.

swift test: all tests pass on macOS. Linux: built and tested in the swift:6.3 container as CI does, and all 1,420 tests pass (the CoreText tests compile out).

Not yet wired

Nothing in a document names a font yet. #186 will map the font directives onto FontRequest, report FontResolution diagnostics at the directive's source, and route layout measurements through the provider. #191 builds on FontResolution and FontLibrary.

🤖 Generated with Claude Code

The renderer could draw text only in the faces it bundles, and
OutlineFontSet.resolve answered nil for any other name.

- FontRequest (public): family, weight and style, or parsed from a
  PostScript name (Times-BoldItalic, Helvetica-Oblique). The base 14 and
  the generic families (serif, sans-serif, monospace) list stand-ins:
  Times/Times New Roman/Liberation Serif/Nimbus/DejaVu and so on.
- FontLibrary (public, immutable, Sendable): faces the host supplies as
  font Data, CFF or TrueType, single faces or collections, parsed once.
  Set on SVGRenderConfig.fontLibrary. Per-config rather than
  process-wide, so a config always gives the same output.
- SVGRenderConfig.systemFonts (default false): installed fonts on Apple
  platforms. CoreText only locates the file and face, which our own
  parser (#189) then reads, so every face is outlined and fsType-checked
  by the same code. With a mandatory attribute set, CoreText's
  everything-is-Helvetica fallback cannot answer a missing family.
- FontProvider resolves a request through registered, then system, then
  bundled faces. For each family (asked-for first, then stand-ins), an
  exact face from any source beats a near one. Faces whose fsType
  forbids embedding are skipped when outlining. Results are cached per
  provider. The emitter's own Bravura and Libertinus Serif requests go
  straight to the bundle, so existing output is unchanged.
- FontResolution (public) records the face used, its origin and any
  refusals. diagnostics(at:) produces .fontSubstituted (info for a
  stand-in, warning otherwise) and .fontNotEmbeddable, for #186 to
  report against the directive that named the font.
- FaceKey becomes an open-ended key. Bundled faces keep their ids;
  other faces get "f-<PostScript name>".
- TextOutliner.outline(_:font:fontSize:fill:library:systemFonts:) draws
  a named face, and OutlinedText.resolution says which face was used.
- OpenTypeFont reads family (name 16/1), usWeightClass and italic, with
  head.macStyle as the fallback for faces without OS/2 (macOS
  Courier.ttc).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sbeitzel
sbeitzel merged commit 035b1f4 into develop Oct 3, 2026
2 checks passed
@sbeitzel
sbeitzel deleted the feature/fix_190 branch October 4, 2026 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant