Read a GSUB subtable's record offsets unsigned, so a subtable over 32 KB still resolves - #107
Merged
Merged
Conversation
… KB still resolves (#100) Multiple, Alternate and Ligature Substitution each hold one record per covered glyph, reached through an array of Offset16 from the start of the subtable. Offset16 is unsigned and the counts beside those arrays are uint16, but eight fields in the shaper and the same twelve in the dump were read signed. A subtable larger than 32,767 bytes has entries at or past 0x8000, which came back negative: the reader then seeked 65,536 bytes short of the record it wanted and decoded whatever the table holds there - another glyph's replacement, with a glyph count belonging to neither. Noto Sans Duployan's 'dist' Multiple Substitutions are about 40 KB each, so roughly a quarter of the 1,185 sequences in each sits past the boundary. A font that shapes by expansion then matches and expands what it was handed, so a wrong glyph count is not one wrong glyph: U+1BC44..U+1BC47 came out as 102,109 glyphs and 52 seconds of shaping, and is now 343 glyphs and 0.13s. Rendering the whole Duployan block four characters at a time exhausted 2 GB, and does not now. The residual gap to HarfBuzz's 359 glyphs on that run is #102, whose carry markers are emitted by lookups reachable only as nested lookups. Test font: NotoSans-GSUB2-BigSubtable-Synthetic, built for this - 1,000 covered glyphs each replaced by a run of fifteen markers, whose Sequence array is 36,072 bytes so the last 95 entries sit past the boundary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jakejackson1
force-pushed
the
fix/100-expansion-shaping
branch
from
September 14, 2026 22:47
0c653b0 to
8fe905a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #100.
The expansion is not multiplying. A GSUB subtable's record offsets were read signed, so a subtable over 32 KB resolved a quarter of its records to the wrong place — and a font that computes on the glyphs it gets back then expanded what it was given, again.
The read
Multiple, Alternate and Ligature Substitution each hold one record per covered glyph, reached through an array of
Offset16from the start of the subtable.Offset16is unsigned, and the counts beside those arrays areuint16. Eight fields in the shaper were read with the signed reader, and the same twelve inOtlDump, which keeps its own copy of them:A subtable larger than 32,767 bytes has entries at or past
0x8000, and read signed those come back negative. The shaper then seeked 65,536 bytes short of the record it wanted and decoded whatever the table holds there — another glyph's replacement, with a glyph count belonging to neither.Noto Sans Duployan
Its
distMultiple Substitutions are about 40 KB each, so roughly a quarter of the 1,185 sequences in each sits past the boundary.hb-shapeTwo characters were already right, because every sequence that run reached lay inside the first 32 KB — which is why the issue's first row looked like a plateau rather than a symptom. The third character reaches one that does not, and from there the glyphs it was handed are matched and expanded by the rules that follow. The log the issue asked for points at it: one Multiple Substitution turned 24 glyphs into 10,157 in a single call, having read a glyph count of 10,119 out of the middle of another sequence.
Rendering the whole Duployan block four characters at a time exhausted 2 GB, and does not now.
Two things the issue expected that are not so
The markers are meant to reach the page.
hb-shapewith the same font and the same features leaves the same markers: 175 glyphs for two characters, not 5. They are zero-advance glyphs that the chaining rules compute on, and a renderer draws nothing for them. There is no missing collapsing pass.The residual difference is two further defects rather than one. This PR closes the count gap for runs up to three characters; at four the run is still 16 glyphs short, and those 16 are carry markers emitted by lookups reachable only as nested lookups — which is #102. With that fix applied as well, every sample here matches
hb-shape's glyph count exactly: 91 / 175 / 259 / 359 / 751.The glyphs themselves still do not all agree. The font computes its spacing with a counter built out of marker glyphs, and 2 to 58 of its digits come out wrong depending on run length — traced to one chained context that matches where HarfBuzz matches nothing, and filed as #108. Measured against
hb-shapeglyph for glyph:What does not move
Of the 104 fonts in
packages/,tests/data/ttfand the currentgoogle/fontsreleases of five Noto families, ten carry an offset past0x8000:None of the ten carries a GDEF table, and mPDF refuses
useOTLon a font without one, so none reaches a GSUB lookup as shipped and no page in the corpus moves. The suite passes unchanged. The fix matters for fonts a user brings, which is how #100 was found.The test font
NotoSans-GSUB2-BigSubtable-Synthetic, built for this the way the two GPOS synthetics already intests/data/ttfwere. 1,000 covered glyphs, each replaced by fifteen markers with the glyph itself in the middle — the shape of a font that shapes by expansion. Its Sequence array comes to 36,072 bytes, so the last 95 entries sit past the boundary; only the four glyphs a test types carry an outline, and the markers draw nothing.Against the code before the fix, a character from the far end of the subtable is replaced by eight glyphs belonging to another one and its row of the snapshot is blank.
🤖 Generated with Claude Code