Skip to content

Binary (TTF/OTF) import is lossy — design a proper import path (components, glyph set, names, variations) #143

Description

@kostyafarber

Summary

The binary reader (crates/shift-backends/src/binary/reader.rs) is currently a "draw what skrifa gives us" importer. That's fine as a preview, but as an import path into an editor it silently discards most of the structure we care about. Found while dogfooding with a subset Space Grotesk VF (see the shift-dogfood repo): the TTF import can't exercise component decomposition, anchors, or variable editing because none of that data survives.

#48 already tracks the fvar slice of this. This issue is the umbrella for the rest: what a real binary import path should preserve.

What gets lost today

1. Components are flattened to contours.
font_from_skrifa() (binary/reader.rs:147) draws every glyph through skrifa's OutlinePen with DrawSettings::unhinted(...) (reader.rs:179), which resolves composite glyphs into outlines. A font with 652 composites imports as 100% contours — there is nothing to decompose, and editing a base glyph no longer propagates. Instead we should read glyf composite records directly (component glyph id + transform) and build Component IR, the same shape the designspace reader produces.

2. Unencoded glyphs are skipped entirely.
The import loop iterates char_map.mappings() (reader.rs:173), so anything without a cmap entry — a.alt, .case/.sc variants, and unencoded marks used as composite bases — never imports. Should iterate the full glyph set (maxp/glyf), not the charmap.

3. Glyph names ignore the post table.
Names are synthesized as the literal character or uniXXXX (reader.rs:193). A post format 2.0 font carries production names (eacute, acutecomb) — we should use them, falling back to AGL-style names only for post format 3. Literal-character names (é) are also going to be hostile to anything keyed by glyph name (features, kerning groups, .shift round-trips).

4. All variation data is dropped.
Both metrics and outlines are read at LocationRef::default() only (reader.rs:154, reader.rs:179). #48 covers reading fvar axes; the bigger design question for a variable-first editor is what to do with gvar: reconstruct editable masters from the deltas (min/default/max per axis, plus intermediates where gvar has them), or import static-at-default and tell the user. Either is defensible — silently static is not.

5. No anchors (expected, worth documenting).
Anchors don't exist in compiled fonts; they're baked into GPOS mark-attachment at build time. Deriving anchors from GPOS mark/mkmk lookups is possible and would make imported binaries much more editable, but it's inference, not recovery — fine to declare out of scope, in which case the import UX should say so.

Scope notes

  • OTF/CFF has no composites at all (CFF charstrings are flat, modulo legacy seac), so item 1 is TTF/glyf-only; the CFF path stays outline-based.
  • Dispatch is shared: both TTF and OTF route through BytesFontAdaptor (font_loader.rs:111-112).
  • Once 1–4 land, a shift convert font.ttf out.shift CLI subcommand becomes worth exposing — the pieces (binary::read_font_file + ShiftSourcePackage::save_font) already exist, it's just not worth wiring up while the reader is this lossy.

Suggested order

  1. Full glyph set + post names (2, 3) — small, unblocks everything keyed by name
  2. Preserve glyf composites as Component IR (1)
  3. fvar axes (TTF/OTF loader does not read fvar — variable font axes are lost #48), then the gvar/masters design decision (4)
  4. Decide and document the anchor policy (5)

Acceptance sketch

Importing the shift-dogfood subset TTF (397 glyphs, 169 composites, wght 300–700) should yield: 397 glyphs including unencoded ones, eacute as a live component of e + acutecomb with production names, and either editable wght masters or an explicit "imported static at default location" notice.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions