diff --git a/src/OtlDump.php b/src/OtlDump.php
index 177cb9a88..f5246ce6a 100644
--- a/src/OtlDump.php
+++ b/src/OtlDump.php
@@ -433,14 +433,6 @@ protected function gsubReverseChainRule(array $Lookup, $i, $c, $tag, $scripttag,
return [];
}
- /**
- * The dump reads each class-based rule's positions fresh, where the parser does not (#170).
- */
- protected function keepsEarlierRulePositions()
- {
- return false;
- }
-
/**
* One substitution, as a row of the report.
*
diff --git a/src/TTFontFile.php b/src/TTFontFile.php
index 86ff29b43..9b9444b78 100644
--- a/src/TTFontFile.php
+++ b/src/TTFontFile.php
@@ -2412,8 +2412,8 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag)
{
$volt = [];
- // Kept across rules and lookups rather than read fresh for each: see keepsEarlierRulePositions().
- // A Type 5 Format 1 or 2 rule hands whatever these last held to the Arabic shaper (#189).
+ // 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 = [];
@@ -2544,19 +2544,7 @@ function _getGSUBarray(array $Lookup, $lul, $scripttag)
for ($cscrule = 0; $cscrule < $cscs['ChainSubClassRuleCnt']; $cscrule++) {
$rule = $cscs['ChainSubClassRule'][$cscrule];
- if (!$rule['BacktrackGlyphCount'] || !$this->keepsEarlierRulePositions()) {
- $backtrackGlyphs = [];
- }
- for ($gcl = 0; $gcl < $rule['BacktrackGlyphCount']; $gcl++) {
- $backtrackGlyphs[$gcl] = $this->classGlyphs($subtable['BacktrackClasses'], $rule['Backtrack'][$gcl]);
- }
-
- if (!$rule['LookaheadGlyphCount'] || !$this->keepsEarlierRulePositions()) {
- $lookaheadGlyphs = [];
- }
- for ($gcl = 0; $gcl < $rule['LookaheadGlyphCount']; $gcl++) {
- $lookaheadGlyphs[$gcl] = $this->classGlyphs($subtable['LookaheadClasses'], $rule['Lookahead'][$gcl]);
- }
+ list($backtrackGlyphs, $lookaheadGlyphs) = $this->classSequences($subtable, $rule);
$this->addTo($volt, $this->gsubContextRule($Lookup, $i, $c, $tag, $scripttag, $ignore, $this->contextRule(
$cscrule,
@@ -2638,6 +2626,25 @@ private function coverageSequences(array $subtable)
];
}
+ /**
+ * @return array [backtrack, lookahead] of one class-based chained rule (Type 6 Format 2), one
+ * "|"-joined glyph string per position
+ */
+ private function classSequences(array $subtable, array $rule)
+ {
+ $backtrack = [];
+ for ($gcl = 0; $gcl < $rule['BacktrackGlyphCount']; $gcl++) {
+ $backtrack[$gcl] = $this->classGlyphs($subtable['BacktrackClasses'], $rule['Backtrack'][$gcl]);
+ }
+
+ $lookahead = [];
+ for ($gcl = 0; $gcl < $rule['LookaheadGlyphCount']; $gcl++) {
+ $lookahead[$gcl] = $this->classGlyphs($subtable['LookaheadClasses'], $rule['Lookahead'][$gcl]);
+ }
+
+ return [$backtrack, $lookahead];
+ }
+
/**
* The input sequence of a class-based rule: the class its rule set is for, then the class of each
* position after the first.
@@ -2679,18 +2686,6 @@ private function addTo(array &$volt, array $entries)
}
}
- /**
- * Whether a class-based chained rule (Type 6 Format 2) that names fewer backtrack or lookahead
- * positions than the rule read before it keeps that rule's extra positions.
- *
- * The parser does, which is #170: the extra positions end up in the rule's matchback and counts,
- * and in what an Arabic joining form hands the shaper. Fixing it is returning false here.
- */
- protected function keepsEarlierRulePositions()
- {
- return true;
- }
-
/**
* The rules of a Single, Multiple, Alternate or Ligature substitution subtable (Types 1 to 4).
*
diff --git a/tests/Mpdf/ChainedClassRulePositionsTest.php b/tests/Mpdf/ChainedClassRulePositionsTest.php
new file mode 100644
index 000000000..888e14f27
--- /dev/null
+++ b/tests/Mpdf/ChainedClassRulePositionsTest.php
@@ -0,0 +1,82 @@
+ 'utf-8',
+ 'fontDir' => [__DIR__ . '/../data/ttf'],
+ 'fontdata' => ['notosansarabicgsub62positionssynthetic' => [
+ 'R' => 'NotoSansArabic-GSUB62Positions-Synthetic.ttf',
+ 'useOTL' => 0xFF,
+ ]],
+ 'default_font' => 'notosansarabicgsub62positionssynthetic',
+ ]);
+ $mpdf->WriteHTML('
' . $html . '
');
+
+ return array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8')));
+ }
+
+ public function dataRuns()
+ {
+ return [
+ 'the first rule, with two positions either side' => [
+ [self::ALEF, self::BEH, self::ALEF, self::LOW_ALEF, self::LOW_ALEF],
+ [self::LOW_ALEF, self::LOW_ALEF, self::ALEF_FINAL, self::BEH, self::ALEF],
+ ],
+ 'the second rule, where the first rule\'s second backtrack position does not hold' => [
+ [self::BEH, self::BEH, self::LOW_ALEF, self::LOW_ALEF],
+ [self::LOW_ALEF, self::LOW_ALEF, self::DOTLESS_BEH_FINAL, self::BEH],
+ ],
+ 'the second rule, where the first rule\'s second lookahead position does not hold' => [
+ [self::ALEF, self::BEH, self::BEH, self::LOW_ALEF],
+ [self::LOW_ALEF, self::DOTLESS_BEH_FINAL, self::BEH, self::ALEF],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider dataRuns
+ */
+ public function testEachRuleMatchesItsOwnPositions($codepoints, $expected)
+ {
+ $this->assertSame($expected, $this->drawn($codepoints));
+ }
+
+}
diff --git a/tests/Mpdf/Fonts/GsubArrayTest.php b/tests/Mpdf/Fonts/GsubArrayTest.php
index 860d1b06d..8fbaaf8d2 100644
--- a/tests/Mpdf/Fonts/GsubArrayTest.php
+++ b/tests/Mpdf/Fonts/GsubArrayTest.php
@@ -9,8 +9,7 @@
use Mpdf\TTFontFile;
/**
- * The GSUB walk the parser and the dump share, where the two part ways on purpose and no font in
- * tests/data/ttf shows it.
+ * The GSUB walk the parser and the dump share, over rule sets no font in tests/data/ttf has.
*
* The lookups are built by hand, in the shape readGSUBrules() leaves them: lookup 0 is a class-based
* chained context (Type 6 Format 2) whose two rules name first two backtrack positions, then one, and
@@ -20,21 +19,21 @@ class GsubArrayTest extends \Yoast\PHPUnitPolyfills\TestCases\TestCase
{
/**
- * The parser keeps the first rule's second backtrack position into the second rule (#170).
+ * The second rule matches the one backtrack position it names, not the first rule's second as
+ * well (#170).
*/
- public function testTheParserKeepsAnEarlierClassRulesExtraPositions()
+ public function testTheParserReadsEachClassRuleWithItsOwnPositions()
{
$parser = $this->withGdef(new TTFontFile($this->cache(), 'win'));
$volt = $parser->_getGSUBarray($this->chainedClassRules(), [0 => 'ccmp'], 'latn');
- $this->assertSame([2, 2], [$volt[0]['nBacktrack'], $volt[1]['nBacktrack']]);
- $this->assertSame('(00043)() (00043)() ', $volt[1]['matchback']);
+ $this->assertSame([2, 1], [$volt[0]['nBacktrack'], $volt[1]['nBacktrack']]);
+ $this->assertSame('(00043)() ', $volt[1]['matchback']);
}
/**
- * The dump reads each rule's positions fresh, so it reports the second rule with the one
- * backtrack position it names.
+ * So does the dump, which reports the second rule with the one backtrack position it names.
*/
public function testTheDumpReportsEachClassRuleWithItsOwnPositions()
{
@@ -56,8 +55,7 @@ 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. Kept as it was by the
- * walk the two share; see #189.
+ * parser hands the shaper the sequences of the chained rule it read last (#189).
*/
public function testAPlainContextRuleOfAnArabicFormCarriesTheLastChainedRulesSequences()
{
@@ -80,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', '00043'], 'postl' => [], 'ignore' => '()'], end($volt));
+ $this->assertSame(['match' => '00041', 'replace' => '00044', 'tag' => 'init', 'prel' => ['00043'], 'postl' => [], 'ignore' => '()'], end($volt));
}
private function chainedClassRules()
diff --git a/tests/data/fontcache/NotoSansArabic-GSUB62Positions-Synthetic.json b/tests/data/fontcache/NotoSansArabic-GSUB62Positions-Synthetic.json
new file mode 100644
index 000000000..a3bac07e4
--- /dev/null
+++ b/tests/data/fontcache/NotoSansArabic-GSUB62Positions-Synthetic.json
@@ -0,0 +1,226 @@
+{
+ "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.",
+ "fullName": "NotoSansArabicGSUB62PositionsSynthetic-Regular",
+ "mtx": {
+ "GSUBScriptLang": {
+ "DFLT": "DFLT ",
+ "arab": "DFLT "
+ },
+ "GSUBFeatures": {
+ "DFLT": {
+ "DFLT": {
+ "fina": [
+ 0
+ ]
+ }
+ },
+ "arab": {
+ "DFLT": {
+ "fina": [
+ 0
+ ]
+ }
+ }
+ },
+ "GSUBLookups": [
+ {
+ "Type": 6,
+ "Flag": 0,
+ "SubtableCount": 1,
+ "Subtables": [
+ 64
+ ],
+ "MarkFilteringSet": ""
+ },
+ {
+ "Type": 1,
+ "Flag": 0,
+ "SubtableCount": 1,
+ "Subtables": [
+ 174
+ ],
+ "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{0E000}\\x{0E002}",
+ "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": {
+ "00627": {
+ "1": "0E000",
+ "prel": {
+ "1": [
+ "00628",
+ "00627"
+ ]
+ },
+ "postl": {
+ "1": [
+ "008AD",
+ "008AD"
+ ]
+ },
+ "ignore": {
+ "1": "()"
+ }
+ },
+ "00628": {
+ "1": "0E002",
+ "prel": {
+ "1": [
+ "00628"
+ ]
+ },
+ "postl": {
+ "1": [
+ "008AD"
+ ]
+ },
+ "ignore": {
+ "1": "()"
+ }
+ }
+ },
+ "finals": "0E000 0E002 ",
+ "rphf": [],
+ "half": [],
+ "pref": [],
+ "blwf": [],
+ "pstf": []
+ },
+ "GSUB.dat": "192 bytes, sha256 7da285b0e28dc3dc7c919987056d9b0e39edcf78f337ef45ec3b10f9d8fc07ac",
+ "GSUBdata.json": [
+ [
+ {
+ "1575": 0,
+ "1576": 1
+ }
+ ],
+ [
+ {
+ "1575": 0,
+ "1576": 1
+ }
+ ]
+ ]
+ }
+}
diff --git a/tests/data/otldump/NotoSansArabic-GSUB62Positions-Synthetic.txt b/tests/data/otldump/NotoSansArabic-GSUB62Positions-Synthetic.txt
new file mode 100644
index 000000000..cd09e743a
--- /dev/null
+++ b/tests/data/otldump/NotoSansArabic-GSUB62Positions-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
+
+GPOS Tables
+GPOS Scripts & Languages
+
+
DFLT
arab
cyrl
dev2
grek
latn
+
+
+=== detail: script arab language DFLT ===
+GSUB Tables
+Lookup #0 [tag: fina]
Subtable #0
LookupType 6: Chaining Contextual Substitution Subtable
Format 2: Class-based Chaining Context Glyph Substitution
Input Class: 1
Rule: 0
CONTEXT:
Backtrack #1: U+0627
Backtrack #0: U+0628
Input #0: ا
Lookahead #0: U+08AD
Lookahead #1: U+08AD
Substitution Position: 0
Lookup #1 [tag: fina]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0627 اب اࢭࢭ » » اب ࢭࢭ M+E000
Input Class: 2
Rule: 0
CONTEXT:
Backtrack #0: U+0628
Input #0: ب
Lookahead #0: U+08AD
Substitution Position: 0
Lookup #1 [tag: fina]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628 ب بࢭ » » ب ࢭ M+E002
+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-GSUB62Positions-Synthetic.txt b/tests/data/shaping/NotoSansArabic-GSUB62Positions-Synthetic.txt
new file mode 100644
index 000000000..3320dea0b
--- /dev/null
+++ b/tests/data/shaping/NotoSansArabic-GSUB62Positions-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 => 0628 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}}
+0x80 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC gpos={"1":{"kashida":8}}
+0xFF +kern -liga 0628 0640 0645 0644 0627 => 0628 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={"4":{"kashida":2},"6":{"kashida":2}}
+0x80 0020 0627 0628 08AD E000 E001 E002 E003 => 0020 0627 0628 08AD E000 E001 E002 E003 group=SCCCCCCC gpos={"4":{"kashida":2},"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={"4":{"kashida":2},"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-GSUB62Positions-Synthetic.json b/tests/data/subset/NotoSansArabic-GSUB62Positions-Synthetic.json
new file mode 100644
index 000000000..ac95dfa96
--- /dev/null
+++ b/tests/data/subset/NotoSansArabic-GSUB62Positions-Synthetic.json
@@ -0,0 +1,129 @@
+{
+ "_": "Generated by composer subset:update. See tests/Mpdf/Fonts/SubsetGoldenMaster.php.",
+ "useOTL=0x00": {
+ "characters": 5,
+ "makeSubset": {
+ "bytes": 1540,
+ "sha256": "e77f3591d38195a8ca52bb36cf48ec32528a34cfb10492a8506e9804d7151ae3",
+ "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 0x46EA9EA2, sha256 dbb762ff579e225ee9b32a99c331cd90fff0f542dd6fe48ad92283744ad83d65",
+ "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": "626 bytes, checksum 0x36A04F5C, sha256 4ac8e50cdaf871921a459fd22a3d72cd1e297ec807b51608557d7d1e2145d1ba",
+ "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": 1528,
+ "sha256": "61015d0b42002421bfd8ab49d509ce9eec1f338b500b701f440dd4cd479fd00a",
+ "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 0x46EA9EA2, sha256 16d82b9b235e72a1e7f02b2948e351696a40866e8ce9bbc4ec0681bdd5dad2fc",
+ "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": "626 bytes, checksum 0x36994F5C, sha256 154baee591bcc5b123fb15fe83ce360d42974c8abfb09816a6f085e524001800",
+ "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUniChar": 65535,
+ "defaultWidth": 600
+ },
+ "repackageTTF": {
+ "bytes": 2028,
+ "sha256": "258c43c914b092e79f1b834433f86e189324544bab685b8b48760f6d33626cb7",
+ "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 0x46EA9EA2, sha256 680287d4e4cd4f6c7b3231566d790ba1780f4c1ea85a3dc8950599725a24a9cf",
+ "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": "626 bytes, checksum 0x36A04F5C, sha256 4ac8e50cdaf871921a459fd22a3d72cd1e297ec807b51608557d7d1e2145d1ba",
+ "post": "151 bytes, checksum 0xB924F86C, sha256 4baf6477b5a1ef33145e61b8c7bb9c828d6b17629e10af34eaf47d20e2978446",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUni": 0
+ }
+ },
+ "useOTL=0xFF": {
+ "characters": 11,
+ "makeSubset": {
+ "bytes": 1956,
+ "sha256": "8ad5f665839d42f00390e52aab3caa0bb3bdc35647e94d6f53a5a67d8c80a602",
+ "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 0x46EA9EA2, sha256 f6d6b03dc841e641f44185e8da3ef71a41da8c233c6f6193aebd1e38023cb360",
+ "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": "626 bytes, checksum 0x36A04F5C, sha256 4ac8e50cdaf871921a459fd22a3d72cd1e297ec807b51608557d7d1e2145d1ba",
+ "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": 1940,
+ "sha256": "19079b8cc29a85b04b96564eb2180ec12bbbdfeb126a45b9b22a3a481635d059",
+ "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 0x46EA9EA2, sha256 dccea7ea3b78b86d002a614d3117eb3b51ccbb49378cf37a18622638cc09cebc",
+ "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": "626 bytes, checksum 0x36994F5C, sha256 154baee591bcc5b123fb15fe83ce360d42974c8abfb09816a6f085e524001800",
+ "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUniChar": 65535,
+ "defaultWidth": 600
+ },
+ "repackageTTF": {
+ "bytes": 2056,
+ "sha256": "9ce11cff4be42fecbcbe0ff2bbba52059205ccb6957c9406e42ae78adba255a7",
+ "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 0x46EA9EA2, sha256 0b8c3ca999a2b17fbf2b3d563eb50321fe89783a5c2ca8f58cb91d6d0db34387",
+ "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": "626 bytes, checksum 0x36A04F5C, sha256 4ac8e50cdaf871921a459fd22a3d72cd1e297ec807b51608557d7d1e2145d1ba",
+ "post": "151 bytes, checksum 0xB924F86C, sha256 4baf6477b5a1ef33145e61b8c7bb9c828d6b17629e10af34eaf47d20e2978446",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUni": 0
+ }
+ }
+}
diff --git a/tests/data/ttf/NotoSansArabic-GSUB62Positions-Synthetic.ttf b/tests/data/ttf/NotoSansArabic-GSUB62Positions-Synthetic.ttf
new file mode 100644
index 000000000..7638d11e4
Binary files /dev/null and b/tests/data/ttf/NotoSansArabic-GSUB62Positions-Synthetic.ttf differ