From 501a546194b686c8fec5dcce16b50fef33313b35 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Thu, 17 Sep 2026 08:12:49 +1000 Subject: [PATCH 1/2] Give an Arabic joining form's plain context rule no backtrack or lookahead (#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) --- src/TTFontFile.php | 24 +- tests/Mpdf/ContextRuleArabicFormTest.php | 81 +++++ tests/Mpdf/Fonts/GsubArrayTest.php | 8 +- .../NotoSansArabic-GSUB5Form-Synthetic.json | 278 ++++++++++++++++++ .../NotoSansArabic-GSUB5Form-Synthetic.txt | 27 ++ .../NotoSansArabic-GSUB5Form-Synthetic.txt | 96 ++++++ .../NotoSansArabic-GSUB5Form-Synthetic.json | 129 ++++++++ .../NotoSansArabic-GSUB5Form-Synthetic.ttf | Bin 0 -> 2564 bytes 8 files changed, 619 insertions(+), 24 deletions(-) create mode 100644 tests/Mpdf/ContextRuleArabicFormTest.php create mode 100644 tests/data/fontcache/NotoSansArabic-GSUB5Form-Synthetic.json create mode 100644 tests/data/otldump/NotoSansArabic-GSUB5Form-Synthetic.txt create mode 100644 tests/data/shaping/NotoSansArabic-GSUB5Form-Synthetic.txt create mode 100644 tests/data/subset/NotoSansArabic-GSUB5Form-Synthetic.json create mode 100644 tests/data/ttf/NotoSansArabic-GSUB5Form-Synthetic.ttf diff --git a/src/TTFontFile.php b/src/TTFontFile.php index 9b9444b78..e3bf90e57 100644 --- a/src/TTFontFile.php +++ b/src/TTFontFile.php @@ -2412,11 +2412,6 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag) { $volt = []; - // A Type 5 Format 1 or 2 rule reads these without setting them, so hands whatever the last - // chained rule left in them to the Arabic shaper (#189). - $backtrackGlyphs = []; - $lookaheadGlyphs = []; - foreach ($lul as $i => $tag) { $this->reportGSUBlookupStart($Lookup, $i, $tag); @@ -2471,9 +2466,7 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag) [], $inputGlyphs, [], - count($inputGlyphs), - ['', '', ''], - [$backtrackGlyphs, $lookaheadGlyphs] + count($inputGlyphs) ))); } } @@ -2494,8 +2487,7 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag) $this->classInputGlyphs($subtable['InputClasses'], $inputClass, $rule), [], $rule['InputGlyphCount'], - $class0excl, - [$backtrackGlyphs, $lookaheadGlyphs] + $class0excl ))); } } @@ -2590,17 +2582,11 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag) * @param string[] $class0excl For a class-based rule, every glyph some class of the input, * backtrack and lookahead Class Definitions names, which is what each * one's class 0 excludes - * @param array $arabic [backtrack, lookahead] for the entry an Arabic joining form's rule - * becomes, where that is not the rule's own: see _getGSUBarray() * * @return array */ - private function contextRule($index, array $records, array $backtrack, array $input, array $lookahead, $nInput, array $class0excl = ['', '', ''], $arabic = null) + private function contextRule($index, array $records, array $backtrack, array $input, array $lookahead, $nInput, array $class0excl = ['', '', '']) { - if ($arabic === null) { - $arabic = [$backtrack, $lookahead]; - } - return [ 'index' => $index, 'records' => $records, @@ -2609,8 +2595,6 @@ private function contextRule($index, array $records, array $backtrack, array $in 'lookahead' => $lookahead, 'nInput' => $nInput, 'class0excl' => $class0excl, - 'prel' => $arabic[0], - 'postl' => $arabic[1], ]; } @@ -2767,7 +2751,7 @@ protected function gsubContextRule(array $Lookup, $i, $c, $tag, $scripttag, $ign } if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { - $volt[] = ['match' => $lookupGlyphs[0], 'replace' => implode(" ", $luss['substitute']), 'tag' => $tag, 'prel' => $rule['prel'], 'postl' => $rule['postl'], 'ignore' => $ignore]; + $volt[] = ['match' => $lookupGlyphs[0], 'replace' => implode(" ", $luss['substitute']), 'tag' => $tag, 'prel' => $rule['backtrack'], 'postl' => $rule['lookahead'], 'ignore' => $ignore]; } else { $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; } diff --git a/tests/Mpdf/ContextRuleArabicFormTest.php b/tests/Mpdf/ContextRuleArabicFormTest.php new file mode 100644 index 000000000..0652e4ff0 --- /dev/null +++ b/tests/Mpdf/ContextRuleArabicFormTest.php @@ -0,0 +1,81 @@ + 'utf-8', + 'fontDir' => [__DIR__ . '/../data/ttf'], + 'fontdata' => ['notosansarabicgsub5formsynthetic' => [ + 'R' => 'NotoSansArabic-GSUB5Form-Synthetic.ttf', + 'useOTL' => 0xFF, + ]], + 'default_font' => 'notosansarabicgsub5formsynthetic', + ]); + $mpdf->WriteHTML('

' . $html . '

'); + + return array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8'))); + } + + public function dataRuns() + { + return [ + 'an initial form from Format 1, with nothing before it' => [ + [self::BEH, self::BEH], + [self::BEH, self::DOTLESS_BEH_INITIAL], + ], + 'an initial form from Format 1, before the chained rule\'s final form' => [ + [self::BEH, self::BEH, self::LOW_ALEF], + [self::LOW_ALEF, self::DOTLESS_BEH_FINAL, self::DOTLESS_BEH_INITIAL], + ], + 'a medial form from Format 2, with no low alef after it' => [ + [self::BEH, self::BEH, self::BEH, self::LOW_ALEF], + [self::LOW_ALEF, self::DOTLESS_BEH_FINAL, self::DOTLESS_BEH_MEDIAL, self::DOTLESS_BEH_INITIAL], + ], + ]; + } + + /** + * @dataProvider dataRuns + */ + public function testTheFormNeedsNoContext($codepoints, $expected) + { + $this->assertSame($expected, $this->drawn($codepoints)); + } + +} diff --git a/tests/Mpdf/Fonts/GsubArrayTest.php b/tests/Mpdf/Fonts/GsubArrayTest.php index 8fbaaf8d2..8b7961579 100644 --- a/tests/Mpdf/Fonts/GsubArrayTest.php +++ b/tests/Mpdf/Fonts/GsubArrayTest.php @@ -54,10 +54,10 @@ public function testTheDumpReportsEachClassRuleWithItsOwnPositions() } /** - * A Type 5 rule has no backtrack or lookahead, yet where it belongs to an Arabic joining form the - * parser hands the shaper the sequences of the chained rule it read last (#189). + * A Type 5 rule has no backtrack or lookahead, so where it belongs to an Arabic joining form the + * shaper gets none, not those of the chained rule read before it (#189). */ - public function testAPlainContextRuleOfAnArabicFormCarriesTheLastChainedRulesSequences() + public function testAPlainContextRuleOfAnArabicFormHasNoBacktrackOrLookahead() { $parser = $this->withGdef(new TTFontFile($this->cache(), 'win')); @@ -78,7 +78,7 @@ public function testAPlainContextRuleOfAnArabicFormCarriesTheLastChainedRulesSeq $volt = $parser->_getGSUBarray($Lookup, [0 => 'init', 2 => 'init'], 'arab'); - $this->assertSame(['match' => '00041', 'replace' => '00044', 'tag' => 'init', 'prel' => ['00043'], 'postl' => [], 'ignore' => '()'], end($volt)); + $this->assertSame(['match' => '00041', 'replace' => '00044', 'tag' => 'init', 'prel' => [], 'postl' => [], 'ignore' => '()'], end($volt)); } private function chainedClassRules() diff --git a/tests/data/fontcache/NotoSansArabic-GSUB5Form-Synthetic.json b/tests/data/fontcache/NotoSansArabic-GSUB5Form-Synthetic.json new file mode 100644 index 000000000..111bb6fe7 --- /dev/null +++ b/tests/data/fontcache/NotoSansArabic-GSUB5Form-Synthetic.json @@ -0,0 +1,278 @@ +{ + "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.", + "fullName": "NotoSansArabicGSUB5FormSynthetic-Regular", + "mtx": { + "GSUBScriptLang": { + "DFLT": "DFLT ", + "arab": "DFLT " + }, + "GSUBFeatures": { + "DFLT": { + "DFLT": { + "fina": [ + 0 + ], + "init": [ + 1 + ], + "medi": [ + 2 + ] + } + }, + "arab": { + "DFLT": { + "fina": [ + 0 + ], + "init": [ + 1 + ], + "medi": [ + 2 + ] + } + } + }, + "GSUBLookups": [ + { + "Type": 6, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 100 + ], + "MarkFilteringSet": "" + }, + { + "Type": 5, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 134 + ], + "MarkFilteringSet": "" + }, + { + "Type": 5, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 162 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 202 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 216 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 230 + ], + "MarkFilteringSet": "" + } + ], + "GPOSScriptLang": { + "DFLT": "DFLT ", + "arab": "DFLT ", + "cyrl": "DFLT ", + "dev2": "DFLT ", + "grek": "DFLT ", + "latn": "DFLT " + }, + "GPOSFeatures": { + "DFLT": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "arab": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "cyrl": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "dev2": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "grek": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + }, + "latn": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + } + }, + "GPOSLookups": [ + { + "Type": 4, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 102 + ], + "MarkFilteringSet": "" + }, + { + "Type": 6, + "Flag": 16, + "SubtableCount": 1, + "Subtables": [ + 194 + ], + "MarkFilteringSet": 0 + } + ], + "MarkGlyphSets": [ + " 0E005" + ], + "rtlPUAstr": "\\x{0E002}-\\x{0E004}", + "haskernGPOS": false, + "hassmallcapsGSUB": false + }, + "cache": { + "GDEFdata.json": { + "GlyphClassBases": " 00020| 00627| 008AD| 0E000| 0E001| 0E002| 0E003| 0E004", + "GlyphClassMarks": " 0E005", + "GlyphClassLigatures": "", + "GlyphClassComponents": "", + "MarkGlyphSets": [ + " 0E005" + ], + "MarkAttachmentType": [] + }, + "GPOS.dat": "234 bytes, sha256 0f363351c5ea276d72ca75127977045024304d1f2b19b04e8cfded715c737a13", + "GPOSdata.json": [ + [ + { + "57349": 0 + } + ], + [ + { + "57349": 0 + } + ] + ], + "GSUB.arab.DFLT.json": { + "rtlSUB": { + "00628": { + "1": "0E002", + "prel": { + "1": [ + "00628" + ] + }, + "postl": { + "1": [ + "008AD" + ] + }, + "ignore": { + "1": "()", + "2": "()", + "3": "()" + }, + "2": "0E004", + "3": "0E003" + } + }, + "finals": "0E002 ", + "rphf": [], + "half": [], + "pref": [], + "blwf": [], + "pstf": [] + }, + "GSUB.dat": "242 bytes, sha256 233543ea71d43c7342390dd0ebd1a6fa9a480924f1ea1ef2e97cf4873a7fbd84", + "GSUBdata.json": [ + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ], + [ + { + "1576": 0 + } + ] + ] + } +} diff --git a/tests/data/otldump/NotoSansArabic-GSUB5Form-Synthetic.txt b/tests/data/otldump/NotoSansArabic-GSUB5Form-Synthetic.txt new file mode 100644 index 000000000..908812490 --- /dev/null +++ b/tests/data/otldump/NotoSansArabic-GSUB5Form-Synthetic.txt @@ -0,0 +1,27 @@ +

GDEF table

+

Glyph classes

+

Glyph class 1

+
Base glyph (single character, spacing glyph)
+
ا ࢭ     
+

Glyph class 3

+
Mark glyph (non-spacing combining glyph)
+
◌
+

Mark Glyph Sets

+

Mark Glyph Set class: 0

+
◌
+

GSUB Tables

+

GSUB Scripts & Languages

+
+
DFLT
DFLT: fina init medi
arab
DFLT: fina init medi
+
+

GPOS Tables

+

GPOS Scripts & Languages

+
+
DFLT
DFLT: mark mkmk
arab
DFLT: mark mkmk
cyrl
DFLT: mark mkmk
dev2
DFLT: mark mkmk
grek
DFLT: mark mkmk
latn
DFLT: mark mkmk
+
+ +=== detail: script arab language DFLT === +

GSUB Tables

+
Lookup #0 [tag: fina]
Subtable #0
LookupType 6: Chaining Contextual Substitution Subtable
Format 3: Coverage-based Chaining Context Glyph Substitution
CONTEXT:
Backtrack #0: U+0628
Input #0:  ب 
Lookahead #0: U+08AD
Substitution Position: 0
Lookup #3 [tag: fina]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628  ب ب  » »  ب   M+E002
Lookup #1 [tag: init]
Subtable #0
LookupType 5: Contextual Substitution Subtable
Format 1: Context Substitution
SubRule: 0
CONTEXT:
Input #0:  ب 
Substitution Position: 0
Lookup #4 [tag: init]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628   ب  » »     M+E004
Lookup #2 [tag: medi]
Subtable #0
LookupType 5: Contextual Substitution Subtable
Format 2: Class-based Context Glyph Substitution
Input Class: 1
Rule: 0
CONTEXT:
Input #0:  ب 
Substitution Position: 0
Lookup #5 [tag: medi]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628   ب  » »     M+E003
+

GPOS Tables

+
Lookup #0 [tag: mark]
Subtable #0
LookupType 4: MarkToBase attachment
Marks: ◌
Bases: ا      ࢭ
Example(s):    ا                  ࢭ  
Lookup #1 [tag: mkmk]
Ignoring: Marks outside Mark Glyph Set[0]
Subtable #0
LookupType 6: MarkToMark attachment
Marks: ◌
Bases: ◌
Example(s): ◌  
diff --git a/tests/data/shaping/NotoSansArabic-GSUB5Form-Synthetic.txt b/tests/data/shaping/NotoSansArabic-GSUB5Form-Synthetic.txt new file mode 100644 index 000000000..46b19791d --- /dev/null +++ b/tests/data/shaping/NotoSansArabic-GSUB5Form-Synthetic.txt @@ -0,0 +1,96 @@ +=== latin === +0xFF 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +0x80 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +0xFF +kern -liga 0041 0056 0041 0054 0061 0072 => 0041 0056 0041 0054 0061 0072 group=CCCCCC +=== cyrillic === +0xFF 0416 0430 0439 => 0416 0430 0439 group=CCC +0x80 0416 0430 0439 => 0416 0430 0439 group=CCC +0xFF +kern -liga 0416 0430 0439 => 0416 0430 0439 group=CCC +=== greek === +0xFF 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +0x80 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +0xFF +kern -liga 03B1 03B2 03C2 => 03B1 03B2 03C2 group=CCC +=== hiragana === +0xFF 3042 3043 3044 => 3042 3043 3044 group=CCC +0x80 3042 3043 3044 => 3042 3043 3044 group=CCC +0xFF +kern -liga 3042 3043 3044 => 3042 3043 3044 group=CCC +=== arabic === +0xFF 0628 0640 0645 0644 0627 => E004 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +0x80 0628 0640 0645 0644 0627 => E004 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +0xFF +kern -liga 0628 0640 0645 0644 0627 => E004 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}} +=== syriac === +0xFF 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +0x80 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +0xFF +kern -liga 0710 0712 0713 0715 => 0710 0712 0713 0715 group=CCCC +=== nko === +0xFF 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +0x80 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +0xFF +kern -liga 07CA 07CB 07CC => 07CA 07CB 07CC group=CCC +=== devanagari === +0xFF 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +0x80 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +0xFF +kern -liga 0915 094D 0937 093F => 0915 094D 093F 0937 group=CCCC +=== bengali === +0xFF 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +0x80 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +0xFF +kern -liga 0995 09CD 09B7 09BF => 0995 09CD 09BF 09B7 group=CCCC +=== gurmukhi === +0xFF 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +0x80 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +0xFF +kern -liga 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A3F 0A38 group=CCCC +=== tamil === +0xFF 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +0x80 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +0xFF +kern -liga 0B95 0BCD 0BB7 0BBF => 0B95 0BCD 0BB7 0BBF group=CCCC +=== malayalam === +0xFF 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +0x80 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +0xFF +kern -liga 0D15 0D4D 0D37 0D3F => 0D15 0D4D 0D37 0D3F group=CCCC +=== sinhala === +0xFF 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +0x80 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +0xFF +kern -liga 0D9A 0DCA 0DBB 0DBB => 0D9A 0DCA 0DBB 0DBB group=CCCC +=== khmer === +0xFF 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +0x80 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +0xFF +kern -liga 1780 17D2 1781 17C1 => 17C1 1780 17D2 1781 group=CCCC +=== thai === +0xFF 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +0x80 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +0xFF +kern -liga 0E01 0E34 0E48 0E23 => 0E01 0E34 0E48 0E23 group=CCCC +=== lao === +0xFF 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +0x80 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +0xFF +kern -liga 0E81 0EB4 0E8D => 0E81 0EB4 0E8D group=CCC +=== myanmar === +0xFF 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +0x80 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +0xFF +kern -liga 1000 103A 1039 1001 => 1000 103A 1039 1001 group=CCCC +=== new tai lue === +0xFF 1980 19B0 1981 => 1980 19B0 1981 group=CCC +0x80 1980 19B0 1981 => 1980 19B0 1981 group=CCC +0xFF +kern -liga 1980 19B0 1981 => 1980 19B0 1981 group=CCC +=== cham === +0xFF AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +0x80 AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +0xFF +kern -liga AA00 AA33 AA01 => AA00 AA33 AA01 group=CCC +=== tai tham === +0xFF 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +0x80 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +0xFF +kern -liga 1A20 1A60 1A21 => 1A20 1A60 1A21 group=CCC +=== mixed scripts === +0xFF 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +0x80 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +0xFF +kern -liga 0041 0628 0915 0042 => 0041 0628 0915 0042 group=CCCC +=== spaced === +0xFF 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +0x80 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +0xFF +kern -liga 0041 0020 0628 0020 0042 => 0041 0020 0628 0020 0042 group=CSCSC +=== font characters 0 === +0xFF 0020 0627 0628 08AD E000 E001 E002 E003 => 0020 0627 0628 08AD E000 E001 E002 E003 group=SCCCCCCC gpos={"6":{"kashida":2}} +0x80 0020 0627 0628 08AD E000 E001 E002 E003 => 0020 0627 0628 08AD E000 E001 E002 E003 group=SCCCCCCC gpos={"6":{"kashida":2}} +0xFF +kern -liga 0020 0627 0628 08AD E000 E001 E002 E003 => 0020 0627 0628 08AD E000 E001 E002 E003 group=SCCCCCCC gpos={"6":{"kashida":2}} +=== font characters 1 === +0xFF E004 E005 => E004 E005 group=CM gpos={"1":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}} +0x80 E004 E005 => E004 E005 group=CM gpos={"1":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}} +0xFF +kern -liga E004 E005 => E004 E005 group=CM gpos={"1":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}} diff --git a/tests/data/subset/NotoSansArabic-GSUB5Form-Synthetic.json b/tests/data/subset/NotoSansArabic-GSUB5Form-Synthetic.json new file mode 100644 index 000000000..d22479446 --- /dev/null +++ b/tests/data/subset/NotoSansArabic-GSUB5Form-Synthetic.json @@ -0,0 +1,129 @@ +{ + "_": "Generated by composer subset:update. See tests/Mpdf/Fonts/SubsetGoldenMaster.php.", + "useOTL=0x00": { + "characters": 5, + "makeSubset": { + "bytes": 1504, + "sha256": "f4fdd245fc504f91d29aa1d36d25bf23f52fe641b72b15b2fdaed6a680d2fcc4", + "tables": { + "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86", + "cmap": "92 bytes, checksum 0x0EEF06F5, sha256 ae6fb5dc97aecd1fc550113941bb8dd17c1e315a921037dd742091775397192d", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "304 bytes, checksum 0x7C5B4089, sha256 dda0aee857654d74b832394e016fc1e1e7710819c3dd061b666da107b0c39414", + "head": "54 bytes, checksum 0x46EA9EA3, sha256 cbb13f84aeb4bc2a151fc56903ba6630710ff42a0221bc6906f2448cebaa5cd4", + "hhea": "36 bytes, checksum 0x29871F54, sha256 b6790f08d7b49267a846ddaa6976fa1ae4f2c482adfd5ca104166fe4ec48ca93", + "hmtx": "28 bytes, checksum 0x0CF90144, sha256 5a0c3e3815967f5179a662960145cd59d1377b1065498bb99d512c035752a67d", + "loca": "16 bytes, checksum 0x00D00114, sha256 82987de0caadd775301bb7f92dc6663a42f1351074a37aaa9e8f43ddd2629de3", + "maxp": "32 bytes, checksum 0x0058052C, sha256 712fd6ee38e7ed3e4f3271a00a1ea02f9588b526df499378ee6c2db8aeed859e", + "name": "590 bytes, checksum 0x32D14B7E, sha256 6f9cda0e4d4ba9540d0577c856ff741507bd754cc02fd0458f25bc4384698b52", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUni": 65535, + "defaultWidth": 600, + "codeToGlyph": "5 entries, sha256 e330c81f30e51088fd0070ff21260ad702c20dd4487cd3b50ac48068aee467ca" + }, + "makeSubsetSIP": { + "bytes": 1492, + "sha256": "3597e5849f5bbdc804e1760189c3fbd33f05dc7885b36c63ef551e6fd8a9baff", + "tables": { + "OS/2": "96 bytes, checksum 0x69A2D688, sha256 69a32b780b72e95ba9dfa002a1b8a2d0019efbe42ba74209c65095bc15e06732", + "cmap": "80 bytes, checksum 0x002C0083, sha256 3d0e58f1892397e1e94f3d0f75c6038f1212ed4d46a99eef47bc722bc366ee41", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "304 bytes, checksum 0x7C5B4089, sha256 184dd6f6301fa823051edcb9ea0c09851b5c5fd96ef911f898c134000990f13b", + "head": "54 bytes, checksum 0x46EA9EA3, sha256 da78ebc30f918b92dffae4ba54332a58ba40685d424626ded224e4b0486b0c81", + "hhea": "36 bytes, checksum 0x29871F54, sha256 b6790f08d7b49267a846ddaa6976fa1ae4f2c482adfd5ca104166fe4ec48ca93", + "hmtx": "28 bytes, checksum 0x0CF90144, sha256 cd581a635bc9a3a20a5f498d3842038b3b5acfb25d1187221fab16a8cf9430ee", + "loca": "16 bytes, checksum 0x00C40114, sha256 b012200316381b41dc293cfa1f58f92eb79c195dbc958907b7309a3fcab6d76e", + "maxp": "32 bytes, checksum 0x0058052C, sha256 712fd6ee38e7ed3e4f3271a00a1ea02f9588b526df499378ee6c2db8aeed859e", + "name": "590 bytes, checksum 0x32CA4B7E, sha256 f90f89fb915f6df91ba20cca75fe74cd14b13f91214149ea9be3b728e6d6b2f4", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUniChar": 65535, + "defaultWidth": 600 + }, + "repackageTTF": { + "bytes": 1992, + "sha256": "2733c4c6d88c604fcca928312f2ed7f294e790e64cf952fcd07d4ba064db3009", + "tables": { + "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86", + "cmap": "72 bytes, checksum 0x06420F70, sha256 e29b8cf3f1e636faa27f210c98884c21505fdd6b6b8acf965ef0b86eafa0c480", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "668 bytes, checksum 0xD2CBF85C, sha256 64df708dd07ef438ab78c2fa6ec037cb9d2ec39a026eeecb018460dbe43ab527", + "head": "54 bytes, checksum 0x46EA9EA3, sha256 301a6f9e7dc4cd406ad7250e6ebd6c819f3d4f839b1c340d7295dd620460532f", + "hhea": "36 bytes, checksum 0x29871F58, sha256 d5c8aa00ac210ae41f8cae1f3201846d77c0ac04d04e11627ee19851bdfdcd08", + "hmtx": "44 bytes, checksum 0x14C501AA, sha256 af3326b0ee31557a2ee01c72ee59dac76903c6a84f8a077b74b99271f2ba42f5", + "loca": "24 bytes, checksum 0x036E041D, sha256 0a4cc922874d893e9926bbbf0eece0202d5cab7ec6724963cafca63946eec457", + "maxp": "32 bytes, checksum 0x005C052C, sha256 26689100d5138bb752af4293a4bbb556b534aa9d94134a46edd960ec269019ec", + "name": "590 bytes, checksum 0x32D14B7E, sha256 6f9cda0e4d4ba9540d0577c856ff741507bd754cc02fd0458f25bc4384698b52", + "post": "151 bytes, checksum 0xB924F86C, sha256 4baf6477b5a1ef33145e61b8c7bb9c828d6b17629e10af34eaf47d20e2978446", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUni": 0 + } + }, + "useOTL=0xFF": { + "characters": 11, + "makeSubset": { + "bytes": 1920, + "sha256": "1f970b9545ee2fb139c012a537448808c4590c7effdfc58a7a936edf94b44b50", + "tables": { + "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86", + "cmap": "108 bytes, checksum 0xEF02E71C, sha256 78a92931bee3fd46f5a9c5415eedaa0f295efa03625ca4afa41c4480a898243b", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "680 bytes, checksum 0x41A9897B, sha256 56ca9bb7742204cbdc96a05b2385d3fa81b1179034b69c093b9aa272a3e64d52", + "head": "54 bytes, checksum 0x46EA9EA3, sha256 34c7bb3f1f15e1bcff72d5ec4cd8526a5c6dcd484124e091f7c8ccb55b332c7c", + "hhea": "36 bytes, checksum 0x29871F58, sha256 d5c8aa00ac210ae41f8cae1f3201846d77c0ac04d04e11627ee19851bdfdcd08", + "hmtx": "44 bytes, checksum 0x14C501AA, sha256 af3326b0ee31557a2ee01c72ee59dac76903c6a84f8a077b74b99271f2ba42f5", + "loca": "24 bytes, checksum 0x03800432, sha256 2cb2965a2d09fea2eb65e980cd64ac413ab3d2cfcd850e65997ad5fef232b95b", + "maxp": "32 bytes, checksum 0x005C052C, sha256 26689100d5138bb752af4293a4bbb556b534aa9d94134a46edd960ec269019ec", + "name": "590 bytes, checksum 0x32D14B7E, sha256 6f9cda0e4d4ba9540d0577c856ff741507bd754cc02fd0458f25bc4384698b52", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUni": 65535, + "defaultWidth": 600, + "codeToGlyph": "11 entries, sha256 4976a91e70c6d83b58a35c5bf0afec8d29c446258274744a3317930df09fa365" + }, + "makeSubsetSIP": { + "bytes": 1904, + "sha256": "286b25d292d3372d1ce1abed6b578d197cd7b63075b8f575184f08ca4a4f5684", + "tables": { + "OS/2": "96 bytes, checksum 0x69A2D68E, sha256 d3e6c0234f8db691dbb73991a5141f8184691d245eda464e73fcfd0645daf3a7", + "cmap": "92 bytes, checksum 0x005600B0, sha256 5febc775230d1d4c2ff1203cc3ba03258d79c52651c9eb23a521082d323ea414", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "680 bytes, checksum 0x41AB897C, sha256 78d00159157d7c704efacfa95bba99beebb11d2b1c4313852a6f55f06936c1af", + "head": "54 bytes, checksum 0x46EA9EA3, sha256 3545e46340e5ee740e00cef07d31694654f7157a48f88eef6fa8456a0616a280", + "hhea": "36 bytes, checksum 0x29871F58, sha256 d5c8aa00ac210ae41f8cae1f3201846d77c0ac04d04e11627ee19851bdfdcd08", + "hmtx": "44 bytes, checksum 0x14C501AA, sha256 1bcad69d16f9d4b7cdb1a2e30f1e1b5172902317c43c2383364b125bc9287e90", + "loca": "24 bytes, checksum 0x02D2038E, sha256 276b53cc6f655d4a16fd4168880b6fba91e36f6b119e3f1b5905dd0725dba35c", + "maxp": "32 bytes, checksum 0x005C052C, sha256 26689100d5138bb752af4293a4bbb556b534aa9d94134a46edd960ec269019ec", + "name": "590 bytes, checksum 0x32CA4B7E, sha256 f90f89fb915f6df91ba20cca75fe74cd14b13f91214149ea9be3b728e6d6b2f4", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUniChar": 65535, + "defaultWidth": 600 + }, + "repackageTTF": { + "bytes": 2020, + "sha256": "79220e4158e2fd618c233b23c77194e0062bf4c6581bc874e1f773dfe4b4e293", + "tables": { + "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86", + "cmap": "100 bytes, checksum 0xF793DE7F, sha256 0dbe9edb138b44d6281c2fb3bff43096c4966deba0e380e4bed0d7e8887d0cba", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "668 bytes, checksum 0xD2CBF85C, sha256 64df708dd07ef438ab78c2fa6ec037cb9d2ec39a026eeecb018460dbe43ab527", + "head": "54 bytes, checksum 0x46EA9EA3, sha256 d17965018acdace96214a0b6245317e968ecfa19183925f30e5e4d6c0e6ea9ad", + "hhea": "36 bytes, checksum 0x29871F58, sha256 d5c8aa00ac210ae41f8cae1f3201846d77c0ac04d04e11627ee19851bdfdcd08", + "hmtx": "44 bytes, checksum 0x14C501AA, sha256 af3326b0ee31557a2ee01c72ee59dac76903c6a84f8a077b74b99271f2ba42f5", + "loca": "24 bytes, checksum 0x036E041D, sha256 0a4cc922874d893e9926bbbf0eece0202d5cab7ec6724963cafca63946eec457", + "maxp": "32 bytes, checksum 0x005C052C, sha256 26689100d5138bb752af4293a4bbb556b534aa9d94134a46edd960ec269019ec", + "name": "590 bytes, checksum 0x32D14B7E, sha256 6f9cda0e4d4ba9540d0577c856ff741507bd754cc02fd0458f25bc4384698b52", + "post": "151 bytes, checksum 0xB924F86C, sha256 4baf6477b5a1ef33145e61b8c7bb9c828d6b17629e10af34eaf47d20e2978446", + "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452" + }, + "maxUni": 0 + } + } +} diff --git a/tests/data/ttf/NotoSansArabic-GSUB5Form-Synthetic.ttf b/tests/data/ttf/NotoSansArabic-GSUB5Form-Synthetic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a336b9f30f7a71cd61fd712c72eb803240fa8fac GIT binary patch literal 2564 zcmbtWYfMvD9RHtt+uKqeg>oq`Tg$7UBE>?Bf-ev#DguJ2bBFKru&dq2D&VFVS@v6_khm}1&@!@L1QKK zm_x2`Ff{1>cIOEqqZu|GMvn5hOm*`Wa zB-OBz>P0GLJft^+H>i8XSu!GkGYUtyGck%j@`|RVP(&_{bMXmub_VEka+zlmF&NJ& zH=&d^N}h5Bw2L%g_6c5vyVNF1s7<{=PD2Yh#Xgv(L;f4Jp)X)LM$Sw{)6p}MP6pyN zFym#NORF%?#N(1GXDWV!=.tp+J0SUwT)Qy&{%WlqOiL|f?@`ilM#x~LRQq60R4 z@cuVqR8f7J$STSzE6OVB6K~JIDSY!+l`C`p8f5^e9NUbvqY1Sp`=GvCXC-Umv!WLo z?5dVxx4Rr#r&8m!=jfU!>FRYUdbh4veH(JJR~Sp%H!3A_bL)J1Zf0g7$LNgzqJAV^ zCmXpjzs|c^@zuJ^>zqoCo}rW!SAq5W>vA)|+wC>E;-!Mg4@15c_Fd_@nf?>szx&wq zfhu#lX{W(r@|`@U=X9qPdWzix8wy?91I~t?+PvKAU3K|Be}1-g)#|L;Ee>zetKpdF zAqTBXdacXVH0eSHUV=p6RDEOsn@&$ZU9}}evu|3Doa|eJ9ncmS+QwNAvPzkaz4%AE=ncZ=dV(Dg*4L>iZEJCyG>@JP;Pr# zw*)+v>h0UMdthG>Z>DGCA4vORHx@CmBieHujvU?L_{^c}PN%Lr2z%HWU(0(yb7PP5 zEs!G4$A6a80p9(F6wv3<5;X6{^GRC4Nxqq+RcfMpN!mdBg*QnXsX&}b(i(M%_h}nN zXq=`fN+UEx!xSSId8r!B3u+G}gBUwt6+?4T7c3+66r}yqwvi?=4nr2D3Fs>%Mhut} zR8JnfLqKMWNjgkbm=&QAq+yJVcmypBY67w#FoLiefz_%dK6@YyOZ3Ji7A^`<5A`Bb zAGHD{3S1W?QxcEkj!C3$P($!OCh@(9p&C2gOAVOci8lahA-@GrOZ}{+{~|2dvakOW zm+K+iy@-=*I3g>;tFRF1g2x4aOQTy}gXI-?ID^EEicJ*1056Yvu(L@Oicp0?7= Date: Thu, 17 Sep 2026 08:16:53 +1000 Subject: [PATCH 2/2] Hand a chained rule's own sequences straight to contextRule() (#189) 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) --- src/TTFontFile.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/TTFontFile.php b/src/TTFontFile.php index e3bf90e57..957316dcc 100644 --- a/src/TTFontFile.php +++ b/src/TTFontFile.php @@ -2509,15 +2509,12 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag) $inputGlyphs[0] = $firstInputGlyph; ksort($inputGlyphs); - $backtrackGlyphs = $rule['BacktrackGlyphCount'] ? $rule['BacktrackGlyphs'] : []; - $lookaheadGlyphs = $rule['LookaheadGlyphCount'] ? $rule['LookaheadGlyphs'] : []; - $this->addTo($volt, $this->gsubContextRule($Lookup, $i, $c, $tag, $scripttag, $ignore, $this->contextRule( $rctr, $rule['SubstLookupRecord'], - $backtrackGlyphs, + $rule['BacktrackGlyphCount'] ? $rule['BacktrackGlyphs'] : [], $inputGlyphs, - $lookaheadGlyphs, + $rule['LookaheadGlyphCount'] ? $rule['LookaheadGlyphs'] : [], count($inputGlyphs) ))); }