Give an Arabic joining form's plain context rule no backtrack or lookahead (#189) - #196
Merged
jakejackson1 merged 2 commits intoSep 17, 2026
Conversation
jakejackson1
force-pushed
the
fix/170-class-rule-positions
branch
from
September 16, 2026 22:21
6680d8b to
7168e2e
Compare
jakejackson1
force-pushed
the
fix/189-context-rule-sequences
branch
from
September 16, 2026 22:22
a22ca5c to
b3e5d2b
Compare
…ahead (#189) A Type 5 rule has an input sequence and nothing else, but where it belonged to an Arabic joining form and was Format 1 or 2 the parser handed the shaper $backtrackGlyphs and $lookaheadGlyphs for its prel and postl. Type 5 never sets either, so the rule got the sequences of the last chained rule read for the script, and Shaper\Arabic gave the form only where the text also held that unrelated rule's context. Format 3 already gave both empty. The 5.1 and 5.2 calls no longer pass contextRule() those sequences, which was the only use of its $arabic parameter, so the parameter goes and the entry an Arabic form's rule becomes reads the rule's own backtrack and lookahead. Every branch of the walk that reads the two variables now sets them first, so their initialisation goes too. No font in tests/data/ttf reaches this, and regenerating every golden master from cold moves none of them. NotoSansArabic-GSUB5Form-Synthetic makes it visible: it is NotoSansArabic-Joining-Subset (Noto Sans Arabic 2.012, OFL 1.1) with its GSUB replaced, in fontTools 4.59.2, by a 'fina' Type 6 Format 3 giving beh its dotless final form after beh and before low alef, then an 'init' Type 5 Format 1 and a 'medi' Type 5 Format 2 giving the dotless initial and medial forms, and name IDs 1, 4 and 6 renamed. Before, both Type 5 rules asked for beh before and low alef after, so beh beh drew no initial form and beh beh beh low-alef no initial or medial one. hb-shape 14.3.1 draws both forms, as mPDF now does, and the font's shaping master draws the initial form before the tatweel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Type 6 Format 1 was the last branch holding its backtrack and lookahead in walk-wide variables it did not need, the shape #189 came from; the other chained branches take theirs from coverageSequences() or classSequences(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jakejackson1
force-pushed
the
fix/170-class-rule-positions
branch
from
September 17, 2026 01:46
7168e2e to
46622c6
Compare
jakejackson1
force-pushed
the
fix/189-context-rule-sequences
branch
from
September 17, 2026 01:46
b3e5d2b to
597497d
Compare
Member
Author
|
Rebased: #190 was squash-merged into |
Member
Author
|
This PR merged into a base branch that had already been squash-merged, so its change never reached |
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 #189
What
A GSUB Type 5 rule has an input sequence and nothing else. When the rule belongs to an Arabic joining form (
isol/fina/fin2/fin3/medi/med2/init, scriptarab),TTFontFile::_getGSUBarray()turns each nested substitution into an entry forShaper\Arabic, with apreland apostl. Format 3 gave both as[]. Formats 1 and 2 passedcontextRule()the method's$backtrackGlyphsand$lookaheadGlyphsinstead. Type 5 never sets either, so these rules got the sequences of the last chained rule (Type 6 or Type 8) read for the script. The shaper then gave the form only where the text also held that unrelated rule's context.The fix is the one the issue names: the 5.1 and 5.2 calls no longer pass
[$backtrackGlyphs, $lookaheadGlyphs]. That argument was the only use ofcontextRule()'s$arabicparameter, so:prel/postlkeyscontextRule()built from itgsubContextRule()gives the Arabic entry the rule's ownbacktrack/lookahead, which are[]for every Type 5 rule$backtrackGlyphs = []; $lookaheadGlyphs = [];initialisation and its The parser hands an Arabic form's plain context rule the backtrack and lookahead of the last chained rule it read #189 comment are removed. Every branch that reads the two variables now sets them first (Type 8, Type 6 Formats 1–3).With the Type 5 branches no longer reading them, Type 6 Format 1's own sequences go straight into its
contextRule()call (a /simplify cleanup), so$backtrackGlyphs/$lookaheadGlyphsare only set fromcoverageSequences()/classSequences()right before they're used./simplify also suggested one helper for the Arabic joining-form entry that
gsubContextRule()andgsubReverseChainRule()both build. I skipped it: the duplication was there before this PR.Test font and shaping test
NotoSansArabic-GSUB5Form-Synthetic.ttfisNotoSansArabic-Joining-Subset.ttf(Noto Sans Arabic 2.012, OFL 1.1), changed in fontTools 4.59.2. Its GSUB is replaced by three lookups forDFLTandarab, in this order, and name IDs 1, 4 and 6 are renamed:0.
finaType 6 Format 3: beh →uni066E.finaafter beh and before U+08AD LOW ALEF (which doesn't join)initType 5 Format 1: beh →uni066E.initmediType 5 Format 2: beh →uni066E.mediBefore the fix, both Type 5 rules inherited lookup 0's
prel[beh]andpostl[lowalef].ContextRuleArabicFormTestrecords the drawn glyphs withTextRecordingMpdf. The output is in visual order, and PUA E002/E003/E004 =uni066E.fina/.medi/.init:hb-shape14.3.10628 06280628 E004uni0628 uni066E.init✓08AD E002 062808AD E002 E004uni08AD uni066E.fina uni066E.init✓08AD E002 0628 062808AD E002 E003 E004uni08AD uni066E.fina uni066E.medi uni066E.init✓HarfBuzz agrees with mPDF after the fix on all three runs. The third run covers the 5.2 call separately from the 5.1 call: the medial beh is followed by a beh, not a low alef.
GsubArrayTest::testAPlainContextRuleOfAnArabicFormCarriesTheLastChainedRulesSequencesis renamed totestAPlainContextRuleOfAnArabicFormHasNoBacktrackOrLookaheadand expects'prel' => [].Fixtures
Regenerated from cold: I deleted
tests/Mpdf/tmp/mpdf,tmp/mpdfandtmp/ttfontdata, then ranfontcache:update,otldump:update,shaping:updateandsubset:update all.git status -- tests/datashowed only the new font's five files. No existing fixture moved, which matches the issue's measurement.In the new font's fixtures:
fontcacheGSUB.arab.DFLT.json:rtlSUB→00628has forms 1/2/3 =0E002/0E004/0E003, withprel/postlonly on form 1, the chainedfinarule.shaping: the Arabic run's leading beh (before a tatweel) is drawn asE004, the initial form. It would be0628before the fix.Mutation check
With the argument put back (the pre-fix
src/TTFontFile.php) and caches cleared, 6 tests fail:ContextRuleArabicFormTest::testTheFormNeedsNoContextGsubArrayTest::testAPlainContextRuleOfAnArabicFormHasNoBacktrackOrLookaheadParserGoldenMasterTestforNotoSansArabic-GSUB5Form-SyntheticShapingGoldenMasterTestforNotoSansArabic-GSUB5Form-SyntheticMirroring upstream
Upstream has no
contextRule(). Inmpdf/mpdfdevelopment(744f75a),TTFontFile::_getGSUBarray()'s LookupType 5 branches build the Arabic entry inline:In both, the mirror changes
'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphsto'prel' => [], 'postl' => []. Upstream never initialises the two variables, so a 5.1/5.2 Arabic rule read before any chained rule also raises "Undefined variable" there, and this change removes that warning too. Upstream'sOtlDumponly reports these rules and has noprel/postl, so it doesn't change. The upstream PR also needs aCHANGELOG.mdentry. The test font andContextRuleArabicFormTestcan be taken over as they are.Base
Stacked on #195 (
fix/170-class-rule-positions) → #190 → #188 → #186 →gravitypdf. #177 and #178 were squash-merged intogravitypdf, and the stack below was rebased onto it. I rebased this branch withgit rebase --onto fix/170-class-rule-positions <old #195 head 6680d8b>and force-pushed it with a lease. There were no conflicts. #186, #188, #190 and #195 are still open, so the base staysfix/170-class-rule-positions.Verification
vendor/bin/phpunit(after the rebase): 2009 tests, 5801 assertions, 1 skipped (Match each class-based chained rule against its own backtrack and lookahead positions (#170) #195's branch: 1999 / 5781 / 1).git status -- tests/datais clean.vendor/bin/phpunit --group=snapshot: OK, 79 tests, 110 assertions.composer cs: clean.🤖 Generated with Claude Code