Skip to content

TrueType hinting - #50

Merged
matmen merged 2 commits into
masterfrom
hinting
Oct 15, 2026
Merged

matmen merged 2 commits into
masterfrom
hinting

Conversation

@matmen

@matmen matmen commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Adds an interpreter for the hinting instructions of TrueType fonts. Hinting aligns stems and edges with the pixel grid, so small text renders crisp instead of blurry, the way Windows (GDI) and FreeType's classic mode render it. This is what #31 asks for.

Behavior

  • On by default: renderText() hints fonts that come with instructions (fpgm, prep or cvt tables) and places their glyphs on whole pixels (rounded advances and kerning).
  • Other fonts are unaffected: fonts without instructions and CFF/OpenType outlines render exactly as before; a test makes sure of that.
  • Opt-out: renderText(font, size, text, color, layout, {hinting: false}) restores the unhinted, subpixel-positioned rendering.

The interpreter

  • Complete TrueType instruction set, including:
    • the font program and control value program, run once per size
    • glyph programs with the four phantom points
    • composite glyphs, including point matching and USE_MY_METRICS
    • the twilight zone
    • function and instruction definitions
    • all rounding modes, including SROUND/S45ROUND
    • delta exceptions
  • Behaves like FreeType's classic (v35) interpreter where the specification leaves details open:
    • 16.16 fixed point scaling with halves rounded away from zero
    • original distances measured on the unscaled outline (IP, IUP, MDRP, MD)
    • no movement for near-perpendicular freedom/projection vectors
    • missing stack arguments count as zero, and invalid point, zone or control value references skip the instruction instead of aborting
  • Fallback: if a glyph program fails anyway (division by zero, unknown instruction, too many instructions or nesting), that glyph renders unhinted.

Verification

Hinted outlines were compared point by point with current FreeType (v35 interpreter, built from source) on 7 fonts: DejaVu Sans (hand hinted), Noto Sans and Noto Sans Symbols (ttfautohint), Open Sans Bold and Open Sans Condensed (ttfautohint), FreeMono (FontForge) and Ethnocentric. Each font was checked with all printable ASCII glyphs at 31 sizes (8–32, 36, 40 and 48 px), 2632 glyph instances per font:

  • No glyph is off by half a pixel or more.
  • 99.8–100% are within 1/16 px.
  • Noto Sans, Noto Sans Symbols and both Open Sans fonts match exactly, with every point and every advance identical.
  • The remaining sub-1/16 px differences (DejaVu, FreeMono) come from floating point instead of integer arithmetic on diagonal vectors.

All of Pillow's test fonts (including broken ones) hint without errors, and 3000 random byte sequences used as glyph programs never crashed or hung (slowest: 10 ms).

Also fixed

The glyph rasterizer dropped segments touching the left edge of a glyph's bitmap: curve subdivision produced x values like -1e-17, which landed in a negative row index. Glyphs then came out inverted. Unhinted text hit this rarely, hinted outlines (whose points often lie exactly on pixel boundaries) often.

Tests

  • tests/font.js:
    • hinted points and advances of several glyphs are compared with FreeType's
    • with hinting on, repeated letters render identically (whole pixel placement)
    • text gets crisper
    • fonts without instructions render unchanged
  • Regenerated targets: three images with text in hinted fonts at 56–128 px (barely visible difference at those sizes). The layout test, which checks subpixel placement, runs with hinting: false.

Credits

Where the TrueType spec leaves details open, the interpreter's behavior and parts of its logic follow FreeType's interpreter. Portions of this software are copyright © 2026 The FreeType Project (https://freetype.org); the FreeType License is included as licenses/FTL.txt and credited in THIRD_PARTY_NOTICES.md.

Closes #31

@matmen matmen added this to the v3.1.0 milestone Oct 15, 2026
@matmen matmen self-assigned this Oct 15, 2026
@matmen matmen added the enhancement New feature or request label Oct 15, 2026
matmen and others added 2 commits October 15, 2026 08:34
An interpreter for TrueType instructions: font and control value programs
per size, glyph programs with phantom points, composites, the twilight
zone and the complete instruction set. renderText() runs it by default
for fonts that have instructions and places their glyphs on whole
pixels; fonts without instructions (and CFF outlines) render as before,
{hinting: false} turns it off.

Checked against FreeType's classic (v35) interpreter on 7 fonts (manual
hints like DejaVu, ttfautohint like Noto/Open Sans, FontForge like
FreeMono), all ASCII glyphs at 31 sizes: no glyph is off by half a pixel
or more, 99.8-100% are within 1/16 px, and Noto, Noto Symbols and both
Open Sans fonts match exactly. The details follow FreeType where the
spec leaves them open: 16.16 scaling with halves rounded away from zero,
original distances measured on the unscaled outline, no movement for
near-perpendicular vectors, invalid references skip the instruction.
Random bytecode never crashes or hangs (instruction and recursion limits).

Also fixes the rasterizer dropping segments that touch the bitmap's left
edge (float noise from curve subdivision gave x < 0), which inverted the
glyph; hinted outlines hit that a lot.

The targets of text in hinted fonts are regenerated (at 56-128 px the
difference is barely visible); the layout test checks subpixel placement
and runs unhinted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljnxm1Qjrocq1izGajLx8J
Where the TrueType spec leaves details open, the interpreter follows
FreeType's ttinterp.c (rounding, original distances, IUP, movement along
near-perpendicular vectors, tolerance of invalid references). The FreeType
License asks for the credit line in the documentation: it is in the source
header, THIRD_PARTY_NOTICES.md and the README, and the license text ships
as licenses/FTL.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ljnxm1Qjrocq1izGajLx8J
@matmen
matmen merged commit a605966 into master Oct 15, 2026
13 checks passed
@matmen
matmen deleted the hinting branch October 15, 2026 08:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Font hinting not working

1 participant