Skip to content

Fix #189 — OpenTypeFont reads TrueType outlines and font collections - #200

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

sbeitzel merged 1 commit into
developfrom
feature/fix_189

Conversation

@sbeitzel

@sbeitzel sbeitzel commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Fix #189

What changed

  • TrueTypeGlyphs (new): reads glyf through loca, in both the short and long formats.
    • Simple glyphs: run-length flags and short/long coordinate deltas. A run of off-curve points implies on-curve midpoints between them, and contours may start off-curve or have no on-curve point at all.
    • Contours start where fontTools and FreeType start them: at the first on-curve point, or else at the midpoint of the last and first points. A contour that returns to its start along a straight line ends with Z alone.
    • Composites:
      • ARG_1_AND_2_ARE_WORDS and byte offsets;
      • WE_HAVE_A_SCALE, X_AND_Y_SCALE and TWO_BY_TWO;
      • SCALED_COMPONENT_OFFSET (unless UNSCALED_COMPONENT_OFFSET is also set);
      • nesting up to 8 levels, after which compositeTooDeep is thrown, so a self-including composite fails instead of recursing forever.
    • Point-matched components (ARGS_ARE_XY_VALUES clear) are skipped. The issue allows this for now; there is no diagnostic channel this deep in font parsing.
    • Hinting instructions are ignored.
  • GlyphPath:
    • Adds .quadCurve(control:to:), written as SVG Q. Quadratics are kept as they are rather than converted to cubics, which keeps the path data smaller.
    • Adds an affine transformed(…) for placing composite components.
  • Collections:
    • OpenTypeFont.parse(_:faceIndex:), parse(_:postScriptName:) and postScriptNames(in:), the last reading name ID 6.
    • The new property postScriptName.
    • A new error, faceNotFound.
  • Embedding: OpenTypeFont.embedding: EmbeddingPermissions, read from OS/2.fsType. allowsOutlineEmbedding refuses Restricted License, bitmap-only and no-subsetting faces; an outline document embeds only the glyphs it draws, which is a subset. Font provider: use host-registered and system fonts instead of bundling more faces #190 will use this to fall back to a bundled face.
  • Bundled faces: they are CFF and go through the same path as before; the snapshot tests are unchanged.

Fixtures

Tests/CeolKitSVGRendererTests/Resources/truetype-fixtures.py generates three fonts with CeolKit's own outlines, so there is no third-party licence. truetype-fixtures-PROVENANCE.txt describes them.

  • CeolKitTest-Short.ttf and CeolKitTest-Long.ttf: the same glyphs with each loca format. The long format is forced by giving one glyph an odd-length hinting program.
  • CeolKitTest.ttc: two faces with different PostScript names and fsType values (0 and Restricted).

The fixtures cover on-curve-only contours, implied midpoints, an off-curve start, an all-off-curve contour, composites with byte and word offsets, all three transforms, a nested composite and a point-matched component. The oracle is fontTools' own reading of the same bytes, recorded in truetype-reference.json.

Tests (TrueTypeOutlineTests, 10)

  • Every glyph matches the reference in both loca formats, along with its advance width.
  • The Q path data for an all-off-curve contour.
  • A point-matched component is skipped.
  • A composite cycle, made by patching the fixture's bytes, throws compositeTooDeep.
  • Collection faces selected by index and by name, and both faces outline correctly.
  • fsType decoding.
  • The bundled faces still parse and allow outline embedding.
  • macOS only: Courier New and Times New Roman from /System/Library/Fonts/Supplemental are compared with CoreText for glyph IDs and control-point bounding boxes across AgQé&ß. This test is skipped where the files are missing. It catches misplaced or missing contours and composite parts, but not errors in implied midpoints; the reference tests cover those.

With the midpoint arithmetic deliberately broken, the reference tests fail (17 issues). swift test: all tests pass.

Out of scope (per the issue)

  • Shaping, kerning, GPOS/GSUB.
  • Variable fonts (read at their default instance).
  • WOFF/WOFF2.
  • Bitmap and colour glyphs.
  • The glyf cubic-flag extension.

🤖 Generated with Claude Code

OpenTypeFont could only outline CFF-flavoured faces and rejected
collections outright, which ruled out most installed fonts (Times.ttc,
Courier New.ttf, DejaVu, Liberation).

- TrueTypeGlyphs reads glyf via loca, both short and long formats.
  - Simple glyphs: run-length flags, short and long deltas, implied
    on-curve midpoints, contours that start off-curve or have no on-curve
    point. Contours start where fontTools and FreeType start them.
  - Composite glyphs: byte and word offsets, the three transform forms,
    and SCALED_COMPONENT_OFFSET. Nesting is limited to 8 levels, so a
    cycle throws compositeTooDeep instead of recursing forever.
    Point-matched components are skipped.
  - Hinting instructions are ignored.
- GlyphPath gains .quadCurve, emitted as SVG Q, plus an affine transform
  for composite components.
- Collections: parse(_:faceIndex:), parse(_:postScriptName:), and
  postScriptNames(in:) from name ID 6. Table offsets are file-relative in
  both single fonts and collections.
- OS/2.fsType is exposed as EmbeddingPermissions. allowsOutlineEmbedding
  refuses restricted, bitmap-only and no-subsetting faces, ready for #190.

The bundled CFF faces take the same path as before.

Fixtures are CeolKit's own fonts, generated by a committed fontTools
script, with fontTools' reading of the same bytes as the reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sbeitzel
sbeitzel merged commit 9068194 into develop Oct 3, 2026
2 checks passed
@sbeitzel
sbeitzel deleted the feature/fix_189 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