Skip to content

Add optional font hinting - #1116

Open
oetiker wants to merge 4 commits into
linebender:mainfrom
oetiker:feat/font-hinting
Open

Add optional font hinting#1116
oetiker wants to merge 4 commits into
linebender:mainfrom
oetiker:feat/font-hinting

Conversation

@oetiker

@oetiker oetiker commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Adds font hinting on top of skrifa's HintingInstance, which grid-fits glyph outlines so
that stems align to whole pixels. It noticeably improves legibility of small text.

Opt-in, and why

Hinting is off by default and has to be asked for through Options::font_hinting. An SVG is
resolution independent while hinting is not: outlines have to be fitted for one specific
pixel grid, and usvg has to commit to one while flattening text into paths, long before
the scale the tree is eventually rendered at is known. The grid is taken from the font
size in user units, so hinted output lands on whole pixels for an unscaled render, or at
an integer zoom factor.

Being opt-in also means no existing reference image changes.

Two further properties worth stating up front:

  • Hinting only changes the outlines, never the glyph positions, since those come from
    shaping. Text occupies the same space whether it is hinted or not.
  • text-rendering="geometricPrecision" is never hinted, as that property asks for exact
    outlines. That is the one per element control here, and it is a spec property rather
    than anything new. It is covered by a line in the test SVG that stays byte-identical
    across every hinted variant while the rest of the image changes.

API

Options::font_hinting: Option<FontHintingOptions>, where FontHintingOptions carries an engine and a
target. These mirror what skrifa exposes, wrapped rather than re-exported so that skrifa
does not become part of usvg's public API:

Type Values
FontHintingEngine Interpreter, Auto, AutoFallback (default)
FontHintingTarget Mono, Smooth { mode, symmetric_rendering, preserve_linear_metrics } (default)
FontHintingSmoothMode Normal (default), Light, Lcd, VerticalLcd

Both CLIs gained a --font-hinting flag that enables the defaults.

Outlines are drawn at the fitted size, which yields pixels rather than font units, so they
are scaled back to font units and the existing per-glyph transform then applies unchanged.
The outline cache is keyed by the configuration and the grid.

Tests

tests/text/hinting/sizes.svg is an ordinary reference image test: the unhinted rendering
comes out of gen-tests.py like any other, and three further tests render the same file
with a different configuration each. The file is 300px wide, which is what the harness
scales tests to, so it renders unscaled and the hinted outlines land on the grid they were
fitted for. All four outputs are pairwise distinct.

Worth noting for review: FontHintingTarget::Mono makes no difference under the TrueType
interpreter, which is why the mono test pairs it with the automatic hinter. That matches
FreeType's v40 interpreter behaviour and is called out in the docs for Mono, rather than
pretending the knob always does something.

Hinting grid-fits glyph outlines so that stems align to whole pixels,
which noticeably improves legibility of small text.

It is off by default and has to be asked for through `Options::hinting`,
because an SVG is resolution independent while hinting is not: outlines
have to be fitted for one specific pixel grid, and usvg has to commit to
one while flattening text into paths, long before the scale the tree is
rendered at is known. The grid is derived from the font size, so hinted
output lands on whole pixels for an unscaled render.

Hinting only changes the outlines, never the glyph positions, so text
occupies the same space either way. Elements asking for
`text-rendering="geometricPrecision"` are left unhinted, since that
property asks for the exact outlines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oetiker
oetiker marked this pull request as draft August 4, 2026 05:14
@RazrFalcon

Copy link
Copy Markdown
Collaborator

This is something worth exploring, but I'm strongly against ResvgHinting attribute. We should not pollute the SVG tree.
Hinting options should be part of usvg::Options.

Also, I'm not sure about using the extra.rs tests. A regular SVG->PNG approach is more preferred.

@oetiker

oetiker commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

@RazrFalcon this again is an addition I added for byonk since I want to allow full control of resulting render from within svg and since hinting decisions can depend on font choice a general flag is a hammer way to big for beautifully rendered svgs with hinted font.

@oetiker
oetiker marked this pull request as ready for review August 4, 2026 15:14
@RazrFalcon

Copy link
Copy Markdown
Collaborator

resvg strongly follows the spec and doesn't add anything extra. If you can tweak hinting via CSS/SVG properties - you should do it that way. Nothing custom.

The tests rendered through the `extra` path only because a hinted
outline is fitted to the pixel grid of one size, and the regular path
scales every test to 300px wide. Sizing the file to exactly that width
makes the scale 1 anyway, so it can go back to being an ordinary
reference image test, with the unhinted rendering falling out of
`gen-tests.py` like any other.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@oetiker
oetiker force-pushed the feat/font-hinting branch from 111041c to dbd7091 Compare August 5, 2026 18:46
@oetiker

oetiker commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Both points addressed.

The custom properties are gone, along with the attribute name codegen change they needed, so the generated names.rs diff disappears with them. Hinting is configured through usvg::Options only. The one per element control left is text-rendering="geometricPrecision", which is a spec property and reads naturally as no hinting, since it asks for the exact outlines.

For the record on "if you can tweak hinting via CSS/SVG properties, do it that way": as far as I can tell there is no standard property for it. font-smooth was dropped from CSS3 Fonts, never made it onto the standards track, and controls antialiasing rather than hinting in any case; nothing in CSS Fonts Level 4 covers it. So text-rendering is the whole of what the specs offer, and the configuration itself has nowhere standard to live. That is an argument for keeping it in Options, which is where this now puts it.

The tests are regular reference image tests now. They only went through extra because a hinted outline is fitted to the pixel grid of one particular size while the regular path scales every test to 300px wide. Sizing the file to exactly 300 makes that scale 1, so the unhinted rendering is now an ordinary gen-tests.py test and the three hinted configurations render the same file.

@oetiker

oetiker commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

One thing I would like to sound out before building it, since it is the part this design gives up.

A document may reasonably want different hinting for different fonts: a pixel font and a variable font in the same file want opposite treatment, and with a single global setting one of them loses. Rather than bring back per element properties for that, the natural fit looks like a resolver alongside FontResolver and ImageHrefResolver: hand the host the resolved font and the size, get back the HintingOptions for it. That adds nothing to the SVG tree, and it follows a pattern usvg already has.

It would also land in a better place than a CSS property could. Hinting is applied per glyph, after fallback has been resolved, so a font keyed resolver naturally does the right thing for a document that falls back from a variable font to a bitmap font mid-run, which no property on the element could express. And it keeps the markup standard: the SVG already says which font each element uses, so designers get per element behaviour through font-family alone.

Would you be open to that, as a follow-up once this lands? Happy to leave it out entirely if you would rather keep the surface at Options::hinting.

@RazrFalcon

Copy link
Copy Markdown
Collaborator

Let's rename hinting into font_hinting for clarity. Otherwise it's good.

As for tweaking hinting via FontResolver - sure. Maybe someone would find it useful.

oetiker and others added 2 commits August 6, 2026 14:13
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Renames `--hinting` to `--font-hinting` in both CLIs and the `Hinting*`
types to `FontHinting*`, matching `Options::font_hinting`. The `resvg`
CLI was also missing a help entry for the flag.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@oetiker

oetiker commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

done

@oetiker

oetiker commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Renamed. The font prefix went onto the whole surface for consistency: Options::font_hinting, the FontHinting* types (SmoothMode became FontHintingSmoothMode), and --font-hinting in both CLIs — the resvg CLI was also missing a help entry for the flag, which is added.

The FontResolver-style per-font hinting will come as a separate PR once this lands.

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