Skip to content

Implement full bidirectional hinting for CFF and TrueType rendering - #552

Merged
JimBobSquarePants merged 14 commits into
mainfrom
js/full-hinting
Aug 6, 2026
Merged

Implement full bidirectional hinting for CFF and TrueType rendering#552
JimBobSquarePants merged 14 commits into
mainfrom
js/full-hinting

Conversation

@JimBobSquarePants

Copy link
Copy Markdown
Member

Prerequisites

  • I have written a descriptive pull-request title
  • I have verified that there are no overlapping pull-requests open
  • I have verified that I am following matches the existing coding patterns and practice as demonstrated in the repository. These follow strict Stylecop rules 👮.
  • I have provided test coverage for my change (where applicable)

Description

This pull request introduces significant enhancements to glyph hinting and rendering in the font engine, primarily by adding support for a new Full hinting mode and refactoring how hinting parameters are propagated and used throughout the rendering pipeline. The changes improve outline placement accuracy, device pixel alignment, and caching behavior, while also making the codebase more extensible for future hinting strategies.

Glyph hinting and rendering improvements

  • Introduced a new HintingMode.Full value to enable full horizontal and vertical pixel grid alignment for glyph outlines, enhancing sharpness at small sizes (HintingMode.cs).
  • Refactored RenderTo in FontGlyphMetrics to resolve and apply hinting modes more precisely, including device pixel snapping for decoration origins and outline placement, and passing hinting mode through to renderer parameters. [1] [2] [3] [4]
  • Added new virtual methods to FontGlyphMetrics for extensible hinting: GetScaledSize, TryGetHintedAdvanceWidth, ResolveHintingMode, ResolveOutlineOrigin, TryGetFittedOutlinePlacement, and SnapComposedTranslation, enabling format-specific control over outline fitting and placement. [1] [2]

Renderer and metrics parameter propagation

  • Extended GlyphRendererParameters to include the requested HintingMode, ensuring renderer caches and equality checks account for hinting strategy. [1] [2] [3] [4] [5]
  • Updated GetScaledSize and related methods to accept and propagate HintingMode, allowing for hinting-aware scaling and layout. [1] [2]

TrueType font and hinting support

  • Enhanced StreamFontMetrics.TrueType to support device advance width lookups and store diagnostic information about hinting faults, and refactored hinting application to return a result and accept both scaled and unscaled glyph vectors. [1] [2]

Minor improvements

  • Replaced an unused variable assignment with a discard in LoadFontName for clarity.

These changes collectively improve the fidelity and configurability of glyph rendering, especially for scenarios requiring precise pixel alignment and advanced hinting strategies.

Adds a full bidirectional hinting mode reproducing classic GDI-style
rendering for small text (ImageSharp.Drawing #134):

- The interpreter lifts the v40 backward-compatibility movement gates
  under the new mode and reports the v35 bi-level engine identity
  through GETINFO, so fonts execute their classic grid fitting branches.
- Phantom point originals stay unrounded per TT_Hint_Glyph; only the
  dependent current positions round, fixing mirrored stem placement.
- The em square is constrained to whole pixels under the new mode and
  hinted whole-pixel advances flow into layout, resolved from the hdmx
  table (newly parsed) or the rounded linear advance.
- A geometric grid fitter fully fits axes the instructions left
  unfitted and rescues sub-pixel strokes on instruction-fitted axes,
  standing in for bi-level dropout control at the outline level.
- Emit origins snap to whole pixels for upright full-hinted renders.
- Scaled outline caches and renderer parameters key on hinting mode;
  the glyph zone lives in interpreter-owned reusable buffers so the
  hint path performs no per-glyph allocation.

Sub/superscript control value scaling now follows the outline scale,
and hinting faults are retained for diagnostics instead of being
swallowed. Benchmarks show +2% warm-path cost and no added allocation.
Charstrings now evaluate once per pixel size into a buffered outline
cached per size and hinting mode, replayed through a unit-scale
transforming renderer so rendering arithmetic is reproduced exactly.
This removes the per-render charstring re-parse and provides the
outline surface the geometric grid fitter and CFF-based COLR support
build on.

The Private DICT hinting values the parser previously discarded are
now retained: alignment zones (delta decoded), blue policy parameters,
standard stem widths, stem snap families and the language group.
Stem operators previously consumed only to keep the operand stack
balanced now optionally decode into absolute low/high edge pairs:
hstem/hstemhm record horizontal zones, vstem/vstemhm and the implicit
operands preceding mask operators record vertical zones, and ghost
stems keep their inverted edges. Collection is enabled only by outline
building, which scales the zones into pixel space and retains them on
the buffered outline for the geometric grid fitter to consume as
authoritative stem edges.
The geometric grid fitter gains a buffered outline entry whose edges
are seeded directly from the declared stem zones, so the detection and
pairing heuristics never run for CFF; anchor snapping, stem width
normalization, counter preservation and interpolation proceed as for
detected edges. Private DICT blue zone flats provide the alignment
anchors, generalizing the fitter options from the TrueType specific
x-height and cap height pair to an anchor list.

Under full hinting CFF constrains the em square to whole pixels,
fits the buffered outline once per size, and snaps the composed
replay translation to the grid for upright renders through a sign
exact origin adjustment. Standard and unhinted CFF rendering remain
byte identical.
CFF hinting modes now mirror the TrueType interpreter: unhinted output
is untouched, standard hinting fits the vertical axis only from the
declared horizontal stem zones and blue zone flats, and full hinting
fits both axes. Outline caches key on the actual mode since each now
shapes geometry differently.

The grid fitter anchor list becomes font level state in design units
with a per size scale on the options: blue zone flats precompute at
parse time and TrueType lazily shares one x-height and cap height
array, so per glyph fitting no longer allocates anchor arrays.
Renders a small size ladder of a professionally hinted CFF face under
each hinting mode so fitting behavior is visually pinned alongside the
numeric assertions: unhinted, vertical only fitting with even
baselines and x-heights, and full grid fitting with crisp stems.
The CFF hinting mode ladder now mirrors the TrueType robustness tests
exactly, sizes five through sixty three over the same pangram, pinning
the current fitting baseline for the quality iteration driven by the
GDI comparison harness. CffGlyphMetrics exposes the buffered outline
and hinting values internally for diagnostics and tests.
Top alignment zones now take the ceiling of the flat edge so an
x-height or cap height always earns its full pixel row, matching
bi-level rasterizer output; each edge snaps to its nearest anchor
rather than the first in declaration order. Descender zone flats from
OtherBlues anchor bottom edges, ghost stems seed single alignment
edges instead of being discarded, and overshoot ink flattens onto its
fitted row. Full hinting also substitutes whole pixel advances for
CFF glyphs through a virtual hinted advance lookup shared with the
TrueType path.
Declared stems whose flanks both carry a sustained run of outline,
such as straight stem sides and the tall sweeps of bowl sides, round
their widths with a downward bias so a stroke of around one and a
half pixels renders as a single crisp pixel, matching classic
rasterizer regularization of declared hints. Diagonal strokes are
excluded: their flanks cross the declared zone in a short stretch and
narrowing them drops per row coverage below the threshold, breaking
the stroke apart. Grid fitting instrumentation now also covers the
declared edge path.
The first counter mask in a charstring names the stems participating
in counter control; the evaluation engine now counts them per axis
during stem collection. When a mask covers all of an axis''s stems,
three or more, the grid fitter chains successive stem centers by the
pitch to the previous fitted stem rounded to whole pixels, so the
equal counters the glyph was authored with stay equal in the fit
instead of each center rounding its own way. The chain accepts an
extra half pixel of movement over the per flank cap because a chained
stem accumulates the rounding of every pitch before it.
The declared stem path is rebuilt on the hint map architecture of the
Adobe CFF engine. Hints captured by an alignment zone band move
rigidly onto the zone''s fitted row and lock; remaining hints are
positioned through a captured-only initial map so stems keep their
place relative to aligned features, then adjust so one edge lands on
a pixel boundary, preferring the smallest move that preserves half a
pixel of counter to each neighbor, with blocked moves retried top
down. Insertion order resolves conflicts: overlapping hints are
rejected in favor of earlier, captured ones. Every point transforms
through the resulting piecewise linear map, so curves crossing a zone
stretch smoothly between hint edges instead of collapsing onto them.
Ghost bottom edges now use the correct second pair value. Alignment
zones travel as full bands with their fuzz rather than flat lists,
and the counter mask and wall width treatments carry over on top of
the map. The flank membership, satellite, and interpolation passes no
longer apply to declared hints and remain for detected ones.
Replaces the geometric GlyphGridFitter with a GDI-accurate hinting pipeline for both TrueType and CFF formats.

TrueType changes:
- Scale outlines using GDI's integer scl_ComputeScaling routine (TrueTypeScaler) for exact 26.6 coordinates
- Pass unscaled font-unit points to the interpreter for correct IP/IUP interpolation
- Implement integer fixed-point arithmetic throughout (Project, MovePoint, SHPIX, IUP, ISECT, MUL, normalize)
- Add SCANCTRL/SCANTYPE support and Inhibited hinting result
- Fix phantom point origin shift (fsg_SimpleInnerGridFit) and GETINFO version reporting
- Remove GlyphGridFitter, GridFitOptions, GridFitAxisMode, GridFitterTuning

CFF changes:
- Replace scaled stem storage with character-space stems and a new HintMap fitter
- Add CffFixedPoint, CffBlueZoneTable, DeviceZone, HintMap, HintMapOptions
- Add per-region hintmask support (CffHintRegion, CffHintMask, CffCounterMask)
- Implement cubic-to-quadratic conversion via GDI's QueryCurveTo path
- Move HintZone to Cff namespace; add ExpansionFactor and FamilyZones to CffHintingValues

Layout/rendering:
- Snap composed outline translations via shared SnapComposedTranslation for both formats
- Substitute whole-pixel hinted advances on the flow axis only
- Add layout tests for horizontal, vertical, mixed, and fractional-origin hinting
Moves hinted outline-origin resolution into shared `FontGlyphMetrics` flow so renderer bounds and emitted outlines use the same snapped translation, even when `BeginGlyph` short-circuits to renderer-side caches. Adds `TryGetFittedOutlinePlacement` support for TrueType and CFF, including cached fitted horizontal ink extents for CFF full vertical hinting, and expands hinting tests to assert bounds/outline delta consistency and fitted CFF extent availability.
@codecov

codecov Bot commented Aug 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.28841% with 186 lines in your changes missing coverage. Please review.
✅ Project coverage is 81%. Comparing base (f20e960) to head (e5a56fb).

Files with missing lines Patch % Lines
...bors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs 63% 46 Missing and 4 partials ⚠️
src/SixLabors.Fonts/Tables/Cff/CffHintingValues.cs 78% 13 Missing and 2 partials ⚠️
src/SixLabors.Fonts/Tables/Cff/CffOutline.cs 93% 9 Missing and 5 partials ⚠️
...rc/SixLabors.Fonts/Tables/Cff/CffOutlineBuilder.cs 72% 14 Missing ⚠️
src/SixLabors.Fonts/Tables/Cff/CffFixedPoint.cs 64% 5 Missing and 5 partials ⚠️
.../SixLabors.Fonts/Tables/Cff/CffEvaluationEngine.cs 85% 6 Missing and 3 partials ⚠️
src/SixLabors.Fonts/Tables/Cff/CffHintMask.cs 30% 9 Missing ⚠️
src/SixLabors.Fonts/Tables/Cff/CffBlueZoneTable.cs 89% 4 Missing and 4 partials ⚠️
src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs 89% 4 Missing and 4 partials ⚠️
...Labors.Fonts/Tables/TrueType/Glyphs/GlyphVector.cs 89% 3 Missing and 5 partials ⚠️
... and 11 more
Additional details and impacted files
@@           Coverage Diff           @@
##            main    #552     +/-   ##
=======================================
- Coverage     83%     81%     -2%     
=======================================
  Files        389     402     +13     
  Lines      32859   35405   +2546     
  Branches    4955    5508    +553     
=======================================
+ Hits       27278   28706   +1428     
- Misses      4497    5511   +1014     
- Partials    1084    1188    +104     
Flag Coverage Δ
unittests 81% <83%> (-2%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JimBobSquarePants
JimBobSquarePants merged commit e0f3834 into main Aug 6, 2026
12 of 13 checks passed
@JimBobSquarePants
JimBobSquarePants deleted the js/full-hinting branch August 6, 2026 05:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant