Skip to content

fix(paint): draw a variable font at the CSS font-weight - #405

Merged
tannevaled merged 1 commit into
mainfrom
fix/variable-font-weight
Nov 8, 2026
Merged

tannevaled merged 1 commit into
mainfrom
fix/variable-font-weight

Conversation

@tannevaled

@tannevaled tannevaled commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

What

A variable font (OpenType fvar/gvar) is drawn at the weight the page asks for instead of its default instance.

Reference opened: the go-opentype/opentype source at the pinned version (Face.SetVariation, Font.Axes, Font.Instance, advance.go/face.go applying HVAR/MVAR deltas). The variation machinery already exists in the lowest shared library, so no font code is added here. The OpenType fvar spec text itself was not opened for this PR; the claims below rest on the library source and on the Chrome comparison.

  • paint.Fonts.face finds the style's wght axis (cached per style and per font), clamps the CSS weight to its range and calls SetVariation on the face, so glyph outlines and advances both follow the weight. The face cache is keyed by the clamped weight. The axis default keeps the unvaried path, so default-weight text costs the same as before.
  • The vector painter (PDF) gets Fonts.glyphFont: the static instance (Font.Instance) for the weight, so the exported font is the one drawn, not Raleway-Thin.
  • render -file: the CLI sent no FileRoot, so a relative or file: @font-face URL next to the file could never load (the second observation in the issue). It now uses the file's directory as the base and the file root. An explicit http(s) -base is left alone (a remote base must not reach local files).
  • Test font: testdata/fonts/Outfit-wght.ttf (SIL OFL 1.1, licence file beside it), 110 KB, wght 100..900.

Behaviour change to note

The bundled Inter and Lora regulars carry a wght axis themselves, so font-weight 100..599 now draws at that weight (Inter 300 is light, 500 is medium), as Chrome does, instead of always at regular. Bold (>= 600) still uses the bundled static bold.

Evidence

  • Regression tests, failing on main: engine.TestVariableFontWeights (ink grows with weight), paint.TestVariableFontWeightInstances / TestGlyphFontInstance, cmd/render.TestRunFileLoadsRelativeFont.
  • Chrome comparison, same HTML (four paragraphs, weights 100/400/700/900, @font-face weight range): dark-pixel ink per paragraph, Chrome 991 / 2257 / 3262 / 3976, engine 610 / 1844 / 2814 / 3570, engine before the fix 1683 / 1720 / 1673 / 1729 (serif fallback, flat). Same monotonic progression, same glyph shapes by eye; the engine draws about 10-40 percent less ink (rasteriser coverage, not weight).
  • Benchmarks (1-min load ~31, so times are NOT usable; allocation counts are exact): RenderInlineFontFace 1499 -> 1504 allocs/op (6331128 -> 6332772 B/op); MeasureRepeatedText 0 -> 0 allocs/op. Times to be re-measured at low load.

Found, not fixed here

The CSS font shorthand is not parsed at all (font: 32px V, serif leaves the generic family and size), which also hides @font-face families set that way. Filed as Refs #404 (stays open).

Closes #399

Co-Authored-By: Claude Sonnet 5.5 noreply@anthropic.com

🤖 Generated with Claude Code

A face of a font with an OpenType wght axis is now instanced at the
requested weight (clamped to the axis range) through
opentype.Face.SetVariation, which applies gvar outlines and HVAR/MVAR
metrics, so measurement and painting agree. The default master keeps the
unvaried path. The vector painter hands exporters the static instance
(Font.Instance) so a PDF embeds the weight it drew. The bundled Inter and
Lora regulars carry a wght axis too, so weights 100-599 now draw at their
own weight instead of regular.

render -file now uses the file's directory as base and file root, so a
relative @font-face font beside it loads; an http(s) -base keeps local
files out of reach.

Bundles Outfit[wght].ttf (SIL OFL 1.1) as a test font.

Closes #399

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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.

text: a variable font is drawn at its default instance for every font-weight

1 participant