Skip to content

Read variable fonts: gvar, CFF2, avar and HVAR (#412, step 6) - #471

Open
dsn27 wants to merge 4 commits into
feature/cff-outlinesfrom
feature/variable-fonts
Open

dsn27 wants to merge 4 commits into
feature/cff-outlinesfrom
feature/variable-fonts

Conversation

@dsn27

@dsn27 dsn27 commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #412, step 6 (fonts). This PR is stacked on #470 (CFF outlines). Its base is feature/cff-outlines, so the diff shows only the variable font work. It will be retargeted once #470 is merged.

What changes

ManagedFontProvider now reads variable OpenType fonts. Each named instance of a variable font becomes a face of its own, named the way GDI names it:

  • "Bold" and "Italic"/"Oblique" in the instance name become the style.
  • "Regular" is dropped.
  • The remaining words are appended to the family name, so "Light Condensed" of "Bahnschrift" becomes the family "Bahnschrift Light Condensed".
  • The instances of an italic font stay italic.

This removes the limitation described in #468:

  • TrueType variable fonts such as Bahnschrift and Segoe UI Variable no longer show up as their default instance only.
  • CFF2 fonts are no longer left out of the font list.

Parsing

  • FontVariations (new, internal) parses fvar, avar, the gvar header and HVAR. It normalizes user coordinates through avar and computes tuple scalars with the same semantics as fontTools' supportScalar, covering both peak and intermediate regions.
  • TrueType outlines apply gvar deltas:
    • shared and private packed point numbers, and packed deltas;
    • interpolation of untouched points (IUP);
    • the four phantom points;
    • deltas on the offsets of composite components.
  • CFF2 outlines in CffOutlines: 32-bit INDEX counts, the Top DICT outside an INDEX, FDSelect format 4, vsindex and blend with the ItemVariationStore.
  • Advance widths come from HVAR (with DeltaSetIndexMap). Without HVAR they come from the phantom points. They are fractional and not rounded.
  • TrueTypeFont gets internal IsVariable, Axes, NamedInstances, NormalizedCoordinates and WithVariation(double[]). GetAdvanceWidth now returns double.

Not included

Public API

No public API is removed or changed. ManagedFontProvider simply lists more faces than before.

Tests

Test fonts are in tests/CADability.Tests/Files/Fonts/:

  • VarTestSans.ttf (gvar + HVAR), VarTestSans-NoHVAR.ttf (phantom-point advances) and VarTestSans.otf (CFF2).
  • They are subsets of Source Sans 3 VF (OFL-1.1) taken from the npm package source-sans. They are renamed to "VarTest Sans" because "Source" is a Reserved Font Name.
  • make_variable_test_fonts.py creates the fonts and writes their reference outlines and advance widths with fontTools.
  • The REUSE.toml entries are added.

VariableFontTests checks:

  • the axes, the named instances and the header-only reading;
  • the advance widths of every character at every instance against fontTools: exact with HVAR; within 0.5 without HVAR, because fontTools rounds phantom-point widths;
  • the CFF2 outlines command by command (1e-5);
  • the TrueType outlines by Hausdorff distance (1e-3 font units) at a named instance and at two locations between instances;
  • that the varied outlines really differ from the default;
  • that the provider exposes the instances as GDI-named faces;
  • IUP with a made-up glyph, because the subsets have no untouched points outside their reference points.

Mutation checks that make tests fail:

  • IUP switched off;
  • IUP without clamping;
  • wrong intermediate scalar;
  • blend ignoring deltas;
  • "Regular" not dropped;
  • "Bold" not recognized.

Beyond the test subsets, the full Source Sans 3 VF fonts (Upright and Italic, TTF and OTF; 2,750 glyphs each at 11 locations) were compared against fontTools with a local harness:

  • widths: no difference;
  • CFF2 commands: at most 3e-13;
  • TrueType outlines: Hausdorff distance at most 1.2e-4 font units.

Locally, all 657 tests of CADability.Tests pass (12 skipped, Windows only), and all 88 tests of CADability.ImportTests pass. CADability.Forms builds.

A second commit corrects the documentation of HasOutlines and HasCffOutlines, which still said that CFF2 cannot be read.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5

The managed font reader now handles variable OpenType fonts. The named
instances of a variable font become faces of their own, with family names
as GDI makes them ("Bahnschrift Light Condensed", "Segoe UI Variable
Display Semibold"), so these fonts no longer fall back to their default
instance or, for CFF2, disappear from the font list.

- FontVariations parses fvar, avar, the gvar header and HVAR, normalizes
  user coordinates and computes tuple scalars like fontTools'
  supportScalar (peak and intermediate regions).
- TrueType outlines: gvar deltas with shared and private point numbers,
  packed deltas, interpolation of untouched points (IUP), phantom points
  and the offsets of composite components.
- CFF2 outlines: 32-bit INDEX counts, the Top DICT outside an INDEX,
  FDSelect format 4, vsindex and blend with the ItemVariationStore.
- Advance widths come from HVAR, or from the phantom points without HVAR.
  They are not rounded. MVAR is not applied.

The test fonts are subsets of Source Sans 3 VF (OFL-1.1), renamed to
"VarTest Sans" because "Source" is a Reserved Font Name. Their reference
outlines and advance widths were written with fontTools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YXvDW91oXpy7RquGHNxgR5

This branch has not been deployed

No deployments
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.

2 participants