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.rs — font_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 BytesFontAdaptor → font_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:88 — axes: 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
- Load any variable TTF (e.g. Roboto Flex, Recursive).
- Inspect
font.axes() / font.is_variable() — empty / false.
- 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).
Summary
When loading a variable TTF (or OTF), the binary font reader extracts outlines, metrics, and the cmap but never reads the
fvartable, so axes/instances are dropped. As a resultfont.is_variable()returnsfalsefor fonts that are in fact variable.Where
crates/shift-backends/src/binary/reader.rs—font_from_skrifa()(lines ~121–160). Only extracts glyph outlines (via skrifaOutlinePen), metrics (units_per_em, ascender, descender, cap_height, x_height), and char map / advance widths. Nofont.add_axis(...)calls.crates/shift-backends/src/font_loader.rs(lines ~109–110) — both TTF and OTF route throughBytesFontAdaptor→font_from_skrifa().crates/shift-backends/src/designspace/reader.rs(lines ~54–64) reads axes from the designspace document and callsfont.add_axis(...).Why it's a bug
Fontstruct already supports axes (crates/shift-ir/src/font.rs:88—axes: 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 exposesfvar— the capability is there, it just isn't wired in.Repro
font.axes()/font.is_variable()— empty / false.Expected
TTF/OTF loader reads
fvar(axes + named instances) and populatesFont.axessois_variable()is accurate and downstream UI can expose axes.Notes
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.