Skip to content

TTF/OTF loader does not read fvar — variable font axes are lost #48

@kostyafarber

Description

@kostyafarber

Summary

When loading a variable TTF (or OTF), the binary font reader extracts outlines, metrics, and the cmap but never reads the fvar table, so axes/instances are dropped. As a result font.is_variable() returns false for fonts that are in fact variable.

Where

  • Loader: crates/shift-backends/src/binary/reader.rsfont_from_skrifa() (lines ~121–160). Only extracts glyph outlines (via skrifa OutlinePen), metrics (units_per_em, ascender, descender, cap_height, x_height), and char map / advance widths. No font.add_axis(...) calls.
  • Dispatch: crates/shift-backends/src/font_loader.rs (lines ~109–110) — both TTF and OTF route through BytesFontAdaptorfont_from_skrifa().
  • Compare with the working path: crates/shift-backends/src/designspace/reader.rs (lines ~54–64) reads axes from the designspace document and calls font.add_axis(...).

Why it's a bug

  • Font struct already supports axes (crates/shift-ir/src/font.rs:88axes: Vec<Axis>).
  • Font::is_variable() (crates/shift-ir/src/font.rs:224-225) keys off !axes.is_empty(), so the TTF path silently reports static.
  • skrifa (v0.32.0) already exposes fvar — the capability is there, it just isn't wired in.

Repro

  1. Load any variable TTF (e.g. Roboto Flex, Recursive).
  2. Inspect font.axes() / font.is_variable() — empty / false.
  3. Load the same family via designspace → axes populated correctly.

Expected

TTF/OTF loader reads fvar (axes + named instances) and populates Font.axes so is_variable() is accurate and downstream UI can expose axes.

Notes

  • Existing test loads_binary_fonts_with_contours (crates/shift-backends/tests/loading.rs:111-121) does not assert on axes — masking this gap. Add an axes assertion for a variable TTF fixture when fixing.
  • Affects: TTF + OTF. Does not affect UFO / Glyphs / Designspace (separate loaders with axis support).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    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