Skip to content

Give an Arabic joining form's plain context rule no backtrack or lookahead (#189) - #196

Merged
jakejackson1 merged 2 commits into
fix/170-class-rule-positionsfrom
fix/189-context-rule-sequences
Sep 17, 2026
Merged

jakejackson1 merged 2 commits into
fix/170-class-rule-positionsfrom
fix/189-context-rule-sequences

Conversation

@jakejackson1

@jakejackson1 jakejackson1 commented Sep 16, 2026

Copy link
Copy Markdown
Member

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, script arab), TTFontFile::_getGSUBarray() turns each nested substitution into an entry for Shaper\Arabic, with a prel and a postl. Format 3 gave both as []. Formats 1 and 2 passed contextRule() the method's $backtrackGlyphs and $lookaheadGlyphs instead. 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 of contextRule()'s $arabic parameter, so:

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/$lookaheadGlyphs are only set from coverageSequences()/classSequences() right before they're used.

/simplify also suggested one helper for the Arabic joining-form entry that gsubContextRule() and gsubReverseChainRule() both build. I skipped it: the duplication was there before this PR.

Test font and shaping test

NotoSansArabic-GSUB5Form-Synthetic.ttf is NotoSansArabic-Joining-Subset.ttf (Noto Sans Arabic 2.012, OFL 1.1), changed in fontTools 4.59.2. Its GSUB is replaced by three lookups for DFLT and arab, in this order, and name IDs 1, 4 and 6 are renamed:
0. fina Type 6 Format 3: beh → uni066E.fina after beh and before U+08AD LOW ALEF (which doesn't join)

  1. init Type 5 Format 1: beh → uni066E.init
  2. medi Type 5 Format 2: beh → uni066E.medi

Before the fix, both Type 5 rules inherited lookup 0's prel [beh] and postl [lowalef].

ContextRuleArabicFormTest records the drawn glyphs with TextRecordingMpdf. The output is in visual order, and PUA E002/E003/E004 = uni066E.fina/.medi/.init:

Run (logical) Before After hb-shape 14.3.1
beh beh 0628 0628 0628 E004 uni0628 uni066E.init
beh beh lowalef 08AD E002 0628 08AD E002 E004 uni08AD uni066E.fina uni066E.init
beh beh beh lowalef 08AD E002 0628 0628 08AD E002 E003 E004 uni08AD 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::testAPlainContextRuleOfAnArabicFormCarriesTheLastChainedRulesSequences is renamed to testAPlainContextRuleOfAnArabicFormHasNoBacktrackOrLookahead and expects 'prel' => [].

Fixtures

Regenerated from cold: I deleted tests/Mpdf/tmp/mpdf, tmp/mpdf and tmp/ttfontdata, then ran fontcache:update, otldump:update, shaping:update and subset:update all. git status -- tests/data showed only the new font's five files. No existing fixture moved, which matches the issue's measurement.

In the new font's fixtures:

  • fontcache GSUB.arab.DFLT.json: rtlSUB00628 has forms 1/2/3 = 0E002/0E004/0E003, with prel/postl only on form 1, the chained fina rule.
  • shaping: the Arabic run's leading beh (before a tatweel) is drawn as E004, the initial form. It would be 0628 before the fix.

Mutation check

With the argument put back (the pre-fix src/TTFontFile.php) and caches cleared, 6 tests fail:

  • all three data sets of ContextRuleArabicFormTest::testTheFormNeedsNoContext
  • GsubArrayTest::testAPlainContextRuleOfAnArabicFormHasNoBacktrackOrLookahead
  • ParserGoldenMasterTest for NotoSansArabic-GSUB5Form-Synthetic
  • ShapingGoldenMasterTest for NotoSansArabic-GSUB5Form-Synthetic

Mirroring upstream

Upstream has no contextRule(). In mpdf/mpdf development (744f75a), TTFontFile::_getGSUBarray()'s LookupType 5 branches build the Arabic entry inline:

  • Format 1 (around line 2513)
  • Format 2 (around line 2581)
$volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore];

In both, the mirror changes 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs to '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's OtlDump only reports these rules and has no prel/postl, so it doesn't change. The upstream PR also needs a CHANGELOG.md entry. The test font and ContextRuleArabicFormTest can be taken over as they are.

Base

Stacked on #195 (fix/170-class-rule-positions) → #190#188#186gravitypdf. #177 and #178 were squash-merged into gravitypdf, and the stack below was rebased onto it. I rebased this branch with git 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 stays fix/170-class-rule-positions.

Verification

🤖 Generated with Claude Code

@jakejackson1 jakejackson1 added bug Something isn't working create-upstream-pr labels Sep 16, 2026
@jakejackson1
jakejackson1 force-pushed the fix/170-class-rule-positions branch from 6680d8b to 7168e2e Compare September 16, 2026 22:21
@jakejackson1
jakejackson1 force-pushed the fix/189-context-rule-sequences branch from a22ca5c to b3e5d2b Compare September 16, 2026 22:22
jakejackson1 and others added 2 commits September 17, 2026 11:40
…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
jakejackson1 force-pushed the fix/170-class-rule-positions branch from 7168e2e to 46622c6 Compare September 17, 2026 01:46
@jakejackson1
jakejackson1 force-pushed the fix/189-context-rule-sequences branch from b3e5d2b to 597497d Compare September 17, 2026 01:46
@jakejackson1

Copy link
Copy Markdown
Member Author

Rebased: #190 was squash-merged into gravitypdf (f9b3c0e), so this stack still carried its pre-squash commits. This branch is rebased onto its rebased parent with --onto; no conflicts and the base is unchanged. Suite, snapshots and cs pass on the new head, with no fixture movement.

@jakejackson1
jakejackson1 merged commit f7b591c into fix/170-class-rule-positions Sep 17, 2026
27 checks passed
@jakejackson1

Copy link
Copy Markdown
Member Author

This PR merged into a base branch that had already been squash-merged, so its change never reached gravitypdf. It's carried to gravitypdf by #215.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working create-upstream-pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant