diff --git a/src/Shaper/Arabic.php b/src/Shaper/Arabic.php
index dad7134af..fca5d4bbd 100644
--- a/src/Shaper/Arabic.php
+++ b/src/Shaper/Arabic.php
@@ -302,13 +302,16 @@ private static function glyphs($char, $type, &$chars, $i, $scriptTag, $usetags,
if ($retk != -1) {
$match = true;
// If GSUB includes a Backtrack or Lookahead condition (e.g. font ArabicTypesetting)
+ // The walk over ignored glyphs stops at the edge of the run, and a position it runs out before
+ // reaching is not held, as in HarfBuzz's match_backtrack() and match_lookahead(). Past the edge
+ // the glyph is null, which inList() finds in any pattern from PHP 8, so the walk would not end.
if (isset($arabGlyphs[$char]['prel'][$retk]) && $arabGlyphs[$char]['prel'][$retk]) {
$ig = 1;
foreach ($arabGlyphs[$char]['prel'][$retk] as $k => $v) { // $k starts 0, 1...
if (!isset($chars[$i - $ig - $k])) {
$match = false;
} elseif (!GlyphString::inList($v, $chars[$i - $ig - $k])) {
- while (GlyphString::inList($arabGlyphs[$char]['ignore'][$retk], $chars[$i - $ig - $k])) { // ignore
+ while (isset($chars[$i - $ig - $k]) && GlyphString::inList($arabGlyphs[$char]['ignore'][$retk], $chars[$i - $ig - $k])) {
$ig++;
}
if (!isset($chars[$i - $ig - $k])) {
@@ -325,7 +328,7 @@ private static function glyphs($char, $type, &$chars, $i, $scriptTag, $usetags,
if (!isset($chars[$i + $ig + $k])) {
$match = false;
} elseif (!GlyphString::inList($v, $chars[$i + $ig + $k])) {
- while (GlyphString::inList($arabGlyphs[$char]['ignore'][$retk], $chars[$i + $ig + $k])) { // ignore
+ while (isset($chars[$i + $ig + $k]) && GlyphString::inList($arabGlyphs[$char]['ignore'][$retk], $chars[$i + $ig + $k])) {
$ig++;
}
if (!isset($chars[$i + $ig + $k])) {
diff --git a/tests/Mpdf/ArabicContextEdgeTest.php b/tests/Mpdf/ArabicContextEdgeTest.php
new file mode 100644
index 000000000..07c7c5417
--- /dev/null
+++ b/tests/Mpdf/ArabicContextEdgeTest.php
@@ -0,0 +1,90 @@
+ 'utf-8',
+ 'fontDir' => [__DIR__ . '/../data/ttf'],
+ 'fontdata' => ['notosansarabiccontextedgesynthetic' => [
+ 'R' => 'NotoSansArabic-ContextEdge-Synthetic.ttf',
+ 'useOTL' => 0xFF,
+ ]],
+ 'default_font' => 'notosansarabiccontextedgesynthetic',
+ ]);
+ $mpdf->WriteHTML('
' . $html . '
');
+
+ return array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8')));
+ }
+
+ public function dataRuns()
+ {
+ return [
+ 'a word ending in a mark, where the lookahead runs out' => [
+ [self::BEH, self::BEH, self::FATHA],
+ [self::FATHA, self::BEH, self::BEH],
+ ],
+ 'a word starting with a mark, where the backtrack runs out' => [
+ [self::FATHA, self::BEH, self::BEH],
+ [self::BEH, self::BEH, self::FATHA],
+ ],
+ 'the lookahead met past the mark' => [
+ [self::BEH, self::BEH, self::FATHA, self::LOW_ALEF],
+ [self::LOW_ALEF, self::FATHA, self::DOTLESS_BEH_FINAL, self::BEH],
+ ],
+ 'the backtrack met past the mark' => [
+ [self::LOW_ALEF, self::FATHA, self::BEH, self::BEH],
+ [self::BEH, self::DOTLESS_BEH_INITIAL, self::FATHA, self::LOW_ALEF],
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider dataRuns
+ */
+ public function testAFormsContextIsMatchedUpToTheEdgeOfTheRun($codepoints, $expected)
+ {
+ $this->assertSame($expected, $this->drawn($codepoints));
+ }
+
+}
diff --git a/tests/Mpdf/Fixtures/arabic-shape.php b/tests/Mpdf/Fixtures/arabic-shape.php
new file mode 100644
index 000000000..4d267e322
--- /dev/null
+++ b/tests/Mpdf/Fixtures/arabic-shape.php
@@ -0,0 +1,34 @@
+
+ */
+
+require __DIR__ . '/../../../vendor/autoload.php';
+
+set_time_limit(5);
+error_reporting(E_ERROR);
+
+list($runs, $usetags, $marks) = json_decode(base64_decode($argv[1]), true);
+
+$forms = [];
+foreach ($runs as $name => $run) {
+ list($hexes, $glyphs) = $run;
+
+ $info = [];
+ foreach ($hexes as $hex) {
+ $info[] = ['hex' => $hex, 'uni' => hexdec($hex)];
+ }
+
+ \Mpdf\Shaper\Arabic::shape($info, $glyphs, $marks, $usetags, 'arab');
+
+ foreach ($info as $char) {
+ $forms[$name][] = [$char['hex'], $char['form']];
+ }
+}
+
+echo json_encode($forms);
diff --git a/tests/Mpdf/Shaper/ArabicTest.php b/tests/Mpdf/Shaper/ArabicTest.php
index 513cd289a..097cad861 100644
--- a/tests/Mpdf/Shaper/ArabicTest.php
+++ b/tests/Mpdf/Shaper/ArabicTest.php
@@ -512,6 +512,93 @@ public function dataContextNamingAPlaneSixteenGlyph()
];
}
+ /**
+ * The walk over the glyphs a chained rule's lookup ignores read past the edge of the run: a notice
+ * for each glyph on PHP 7, and from PHP 8 a walk that never returned (#204). Here the notice is what
+ * fails, so a regression cannot hang the suite.
+ *
+ * @dataProvider dataContextWalkedToTheEdgeOfTheRun
+ */
+ public function testAFormsContextWalkReadsNothingPastTheEdgeOfTheRun($hexes, $glyphs, $expected)
+ {
+ set_error_handler(function ($number, $message, $file, $line) {
+ throw new \ErrorException($message, 0, $number, $file, $line);
+ });
+
+ try {
+ $forms = $this->shape($hexes, self::ALL_FORMS, 'arab', self::FATHA, $glyphs);
+ } finally {
+ restore_error_handler();
+ }
+
+ $this->assertSame($expected, $forms);
+ }
+
+ /**
+ * The same cases, all in one child process under a time limit, for a regression that reads past the
+ * edge without a notice. The limit is the child's own rather than a `timeout` around it, so it holds
+ * on Windows too, and the child reports nothing short of a fatal error, so a walk warning on every
+ * step cannot fill the stderr pipe and leave it blocked instead of timed out.
+ */
+ public function testAFormsContextWalkReturnsAtTheEdgeOfTheRun()
+ {
+ $runs = [];
+ $expected = [];
+ foreach ($this->dataContextWalkedToTheEdgeOfTheRun() as $name => $case) {
+ $runs[$name] = [$case[0], $case[1]];
+ $expected[$name] = $case[2];
+ }
+
+ // base64, because Windows argument quoting does not survive the JSON's double quotes
+ $arg = base64_encode(json_encode([$runs, self::ALL_FORMS, ' ' . self::FATHA]));
+ $command = escapeshellarg(PHP_BINARY) . ' -d display_errors=stderr '
+ . escapeshellarg(__DIR__ . '/../Fixtures/arabic-shape.php') . ' ' . $arg;
+
+ $process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, null, null, ['bypass_shell' => true]);
+ $output = stream_get_contents($pipes[1]);
+ $errors = stream_get_contents($pipes[2]);
+ fclose($pipes[1]);
+ fclose($pipes[2]);
+ $this->assertSame(0, proc_close($process), $errors);
+
+ $this->assertSame($expected, json_decode($output, true));
+ }
+
+ public function dataContextWalkedToTheEdgeOfTheRun()
+ {
+ // The forms are the presentation forms rather than names, because the run is left holding them
+ // and shape() reads each back as hex
+ $ignoreFatha = '((?:(?: ' . self::FATHA . '))*)';
+
+ return [
+ 'a backtrack that runs out at the start of the run' => [
+ [self::FATHA, self::DAL],
+ [self::DAL => ['0FEA9', '0FEAA', 'prel' => [0 => [self::BEH]], 'ignore' => [0 => $ignoreFatha]]],
+ [[self::FATHA, 0], [self::DAL, 0]],
+ ],
+ 'a lookahead that runs out at the end of the run' => [
+ [self::BEH, self::FATHA],
+ [self::BEH => ['0FE8F', 'postl' => [0 => [self::DAL]], 'ignore' => [0 => $ignoreFatha]]],
+ [[self::BEH, 0], [self::FATHA, 0]],
+ ],
+ 'a lookahead whose second position runs out after the first is met' => [
+ [self::BEH, self::FATHA, self::DAL, self::FATHA],
+ [self::BEH => ['0FE8F', '0FE90', '0FE91', 'postl' => [2 => [self::DAL, self::DAL]], 'ignore' => [2 => $ignoreFatha]]],
+ [[self::BEH, 0], [self::FATHA, 0], [self::DAL, 0], [self::FATHA, 0]],
+ ],
+ 'a backtrack met past the ignored glyphs' => [
+ [self::BEH, self::FATHA, self::DAL],
+ [self::DAL => ['0FEA9', '0FEAA', 'prel' => [1 => [self::BEH]], 'ignore' => [1 => $ignoreFatha]]],
+ [[self::BEH, 0], [self::FATHA, 0], ['0FEAA', 1]],
+ ],
+ 'a lookahead met past the ignored glyphs' => [
+ [self::BEH, self::FATHA, self::DAL],
+ [self::BEH => ['0FE8F', '0FE90', '0FE91', 'postl' => [2 => [self::DAL]], 'ignore' => [2 => $ignoreFatha]]],
+ [['0FE91', 2], [self::FATHA, 0], [self::DAL, 0]],
+ ],
+ ];
+ }
+
/**
* @return array one [hex, form] pair per character, in logical order
*/
diff --git a/tests/data/fontcache/NotoSansArabic-ContextEdge-Synthetic.json b/tests/data/fontcache/NotoSansArabic-ContextEdge-Synthetic.json
new file mode 100644
index 000000000..07d46f738
--- /dev/null
+++ b/tests/data/fontcache/NotoSansArabic-ContextEdge-Synthetic.json
@@ -0,0 +1,319 @@
+{
+ "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.",
+ "fullName": "NotoSansArabicContextEdgeSynthetic-Regular",
+ "mtx": {
+ "GSUBScriptLang": {
+ "DFLT": "DFLT ",
+ "arab": "DFLT ",
+ "cyrl": "DFLT ",
+ "dev2": "DFLT ",
+ "grek": "DFLT ",
+ "latn": "DFLT "
+ },
+ "GSUBFeatures": {
+ "DFLT": {
+ "DFLT": {
+ "init": [
+ 0
+ ],
+ "medi": [
+ 1
+ ],
+ "fina": [
+ 2
+ ]
+ }
+ },
+ "arab": {
+ "DFLT": {
+ "init": [
+ 0
+ ],
+ "medi": [
+ 1
+ ],
+ "fina": [
+ 2
+ ]
+ }
+ },
+ "cyrl": {
+ "DFLT": {
+ "init": [
+ 0
+ ],
+ "medi": [
+ 1
+ ],
+ "fina": [
+ 2
+ ]
+ }
+ },
+ "dev2": {
+ "DFLT": {
+ "init": [
+ 0
+ ],
+ "medi": [
+ 1
+ ],
+ "fina": [
+ 2
+ ]
+ }
+ },
+ "grek": {
+ "DFLT": {
+ "init": [
+ 0
+ ],
+ "medi": [
+ 1
+ ],
+ "fina": [
+ 2
+ ]
+ }
+ },
+ "latn": {
+ "DFLT": {
+ "init": [
+ 0
+ ],
+ "medi": [
+ 1
+ ],
+ "fina": [
+ 2
+ ]
+ }
+ }
+ },
+ "GSUBLookups": [
+ {
+ "Type": 6,
+ "Flag": 8,
+ "SubtableCount": 1,
+ "Subtables": [
+ 122
+ ],
+ "MarkFilteringSet": ""
+ },
+ {
+ "Type": 1,
+ "Flag": 0,
+ "SubtableCount": 1,
+ "Subtables": [
+ 148
+ ],
+ "MarkFilteringSet": ""
+ },
+ {
+ "Type": 6,
+ "Flag": 8,
+ "SubtableCount": 1,
+ "Subtables": [
+ 162
+ ],
+ "MarkFilteringSet": ""
+ },
+ {
+ "Type": 1,
+ "Flag": 0,
+ "SubtableCount": 1,
+ "Subtables": [
+ 194
+ ],
+ "MarkFilteringSet": ""
+ },
+ {
+ "Type": 1,
+ "Flag": 0,
+ "SubtableCount": 1,
+ "Subtables": [
+ 208
+ ],
+ "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": " 0064E| 0E005",
+ "GlyphClassLigatures": "",
+ "GlyphClassComponents": "",
+ "MarkGlyphSets": [
+ " 0E005"
+ ],
+ "MarkAttachmentType": []
+ },
+ "GPOS.dat": "234 bytes, sha256 0f363351c5ea276d72ca75127977045024304d1f2b19b04e8cfded715c737a13",
+ "GPOSdata.json": [
+ [
+ {
+ "57349": 0
+ }
+ ],
+ [
+ {
+ "57349": 0
+ }
+ ]
+ ],
+ "GSUB.arab.DFLT.json": {
+ "rtlSUB": {
+ "00628": {
+ "2": "0E004",
+ "prel": {
+ "2": [
+ "008AD"
+ ]
+ },
+ "ignore": {
+ "2": "((?:(?: 0064E| 0E005))*)",
+ "1": "((?:(?: 0064E| 0E005))*)"
+ },
+ "3": "0E003",
+ "1": "0E002",
+ "postl": {
+ "1": [
+ "008AD"
+ ]
+ }
+ }
+ },
+ "finals": "0E002 ",
+ "rphf": [],
+ "half": [],
+ "pref": [],
+ "blwf": [],
+ "pstf": []
+ },
+ "GSUB.dat": "220 bytes, sha256 3ac7cb0c04aead3a9fc0c5f6c607b54931c16a81c5d5edb41654ceb5d58d593e",
+ "GSUBdata.json": [
+ [
+ {
+ "1576": 0
+ }
+ ],
+ [
+ {
+ "1576": 0
+ }
+ ],
+ [
+ {
+ "1576": 0
+ }
+ ],
+ [
+ {
+ "1576": 0
+ }
+ ],
+ [
+ {
+ "1576": 0
+ }
+ ]
+ ]
+ }
+}
diff --git a/tests/data/otldump/NotoSansArabic-ContextEdge-Synthetic.txt b/tests/data/otldump/NotoSansArabic-ContextEdge-Synthetic.txt
new file mode 100644
index 000000000..2388457a5
--- /dev/null
+++ b/tests/data/otldump/NotoSansArabic-ContextEdge-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
arab
cyrl
dev2
grek
latn
+
+GPOS Tables
+GPOS Scripts & Languages
+
+
DFLT
arab
cyrl
dev2
grek
latn
+
+
+=== detail: script latn language DFLT ===
+GSUB Tables
+Lookup #0 [tag: init]
Ignoring: Mark Glyphs
Subtable #0
LookupType 6: Chaining Contextual Substitution Subtable
Format 3: Coverage-based Chaining Context Glyph Substitution
CONTEXT:
Backtrack #0: U+08AD
Input #0: ب
Substitution Position: 0
Lookup #3 [tag: init]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628 ࢭ ب » » ࢭ M+E004
Lookup #1 [tag: medi]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0628 ب » » M+E003
Lookup #2 [tag: fina]
Ignoring: Mark Glyphs
Subtable #0
LookupType 6: Chaining Contextual Substitution Subtable
Format 3: Coverage-based Chaining Context Glyph Substitution
CONTEXT:
Input #0: ب
Lookahead #0: U+08AD
Substitution Position: 0
Lookup #4 [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-ContextEdge-Synthetic.txt b/tests/data/shaping/NotoSansArabic-ContextEdge-Synthetic.txt
new file mode 100644
index 000000000..b22550262
--- /dev/null
+++ b/tests/data/shaping/NotoSansArabic-ContextEdge-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 064E 08AD E000 E001 E002 => 0020 0627 0628 064E 08AD E000 E001 E002 group=SCCMCCCC gpos={"7":{"kashida":2}}
+0x80 0020 0627 0628 064E 08AD E000 E001 E002 => 0020 0627 0628 064E 08AD E000 E001 E002 group=SCCMCCCC gpos={"7":{"kashida":2}}
+0xFF +kern -liga 0020 0627 0628 064E 08AD E000 E001 E002 => 0020 0627 0628 064E 08AD E000 E001 E002 group=SCCMCCCC gpos={"7":{"kashida":2}}
+=== font characters 1 ===
+0xFF E003 E004 E005 => E003 E004 E005 group=CCM gpos={"2":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}}
+0x80 E003 E004 E005 => E003 E004 E005 group=CCM gpos={"2":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}}
+0xFF +kern -liga E003 E004 E005 => E003 E004 E005 group=CCM gpos={"2":{"BaseWidth":269,"XPlacement":31,"YPlacement":-3}}
diff --git a/tests/data/subset/NotoSansArabic-ContextEdge-Synthetic.json b/tests/data/subset/NotoSansArabic-ContextEdge-Synthetic.json
new file mode 100644
index 000000000..6e5a80249
--- /dev/null
+++ b/tests/data/subset/NotoSansArabic-ContextEdge-Synthetic.json
@@ -0,0 +1,129 @@
+{
+ "_": "Generated by composer subset:update. See tests/Mpdf/Fonts/SubsetGoldenMaster.php.",
+ "useOTL=0x00": {
+ "characters": 6,
+ "makeSubset": {
+ "bytes": 1576,
+ "sha256": "dcdb96f57cd383eedf19465cdd6f6cdfac3e3eb7a1d109c9af772573a29b6ea4",
+ "tables": {
+ "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86",
+ "cmap": "100 bytes, checksum 0x0A15122E, sha256 01206629b9b1976eb2133f58b90acc80bd992c2eea0522dae67e318aa0b4dac8",
+ "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940",
+ "glyf": "348 bytes, checksum 0x4B193AFD, sha256 72af017879d7b5cb74705e75432c75cf858d4a91d8cc6c666297a05e40cf35f6",
+ "head": "54 bytes, checksum 0x273BB4EB, sha256 953af44a9ea0f49777df3b92e5801c83bdbb3f3c5e582361c5c10e62881dba63",
+ "hhea": "36 bytes, checksum 0x09D800EF, sha256 ebabb47c05c5494a10ab408895203118d43d45389ca43a4c8e36f5f3708f4e55",
+ "hmtx": "32 bytes, checksum 0x0CF9015E, sha256 3d78bf2b079ef8409631c4226d8441771b9cf4a9bbd474aa6f5fb01ab00a8af1",
+ "loca": "18 bytes, checksum 0x01580114, sha256 08d4157aa56b51232a60cdc5bec49509a2b55e0edafda07c36dc3711fe5f910e",
+ "maxp": "32 bytes, checksum 0x00100067, sha256 d7ee222097d2365abb6565f7458e7529d7d9a5c13b9d12b1c5813bd75c4d06e1",
+ "name": "602 bytes, checksum 0x351B4D7C, sha256 a3def774242f05f98ccd86071e838297b51d6977d62d5050843374956fa48320",
+ "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUni": 65535,
+ "defaultWidth": 600,
+ "codeToGlyph": "6 entries, sha256 cd05832cc20763d2baec888f97c96504b92c9dd87781018003995892069fd4d0"
+ },
+ "makeSubsetSIP": {
+ "bytes": 1560,
+ "sha256": "7977ab83391dc8ac1b598f1e9bf535f9d051a22b0e42680b8f907f093187a957",
+ "tables": {
+ "OS/2": "96 bytes, checksum 0x69A2D689, sha256 6d12188d8c750cd88b4abc92ca8f6b60d03839c85f9c99660729749453539cba",
+ "cmap": "82 bytes, checksum 0x00420076, sha256 f6d9593a66846b174499232d5c653827aca29fb4afa95c9d48a8b1a05f33bb84",
+ "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940",
+ "glyf": "348 bytes, checksum 0x4B193AFD, sha256 5cb26f14c78f967800052329067816047ff222ffc04cbc2d661399249d184bcb",
+ "head": "54 bytes, checksum 0x273BB4EB, sha256 3a7a54b8884ebe1bf346527bf67e96ebf138691f42f3e6ec487ed3afcc6ce789",
+ "hhea": "36 bytes, checksum 0x09D800EF, sha256 ebabb47c05c5494a10ab408895203118d43d45389ca43a4c8e36f5f3708f4e55",
+ "hmtx": "32 bytes, checksum 0x0CF9015E, sha256 702795e2dc162157c10308bbcf49a107d874b680074596789fd4951ef157c2b6",
+ "loca": "18 bytes, checksum 0x014C0112, sha256 8c32e6c969ca9367469dfb6601142a14bfea4db0b1974fd58937239b098f958e",
+ "maxp": "32 bytes, checksum 0x00100067, sha256 d7ee222097d2365abb6565f7458e7529d7d9a5c13b9d12b1c5813bd75c4d06e1",
+ "name": "602 bytes, checksum 0x35144D7C, sha256 9a414e77ed3ff545e08019d6eb0c1a54fa3874cc0737e4cac1843d94cc2549c7",
+ "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUniChar": 65535,
+ "defaultWidth": 600
+ },
+ "repackageTTF": {
+ "bytes": 2076,
+ "sha256": "f985836fe626eee4bdae90aa75c296d01ef00d1ff1d3ba83540cdeda385061d5",
+ "tables": {
+ "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86",
+ "cmap": "80 bytes, checksum 0x176604B5, sha256 e6150b2b386d85b244823a2f1782890e4abb76097e8949950e045f68300847a4",
+ "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940",
+ "glyf": "712 bytes, checksum 0xA188F2CF, sha256 be0e31a63f04eca9c536c5a128bfb0af8305bd8b968093ac56201d9728ac9f40",
+ "head": "54 bytes, checksum 0x273BB4EB, sha256 f516b17a24d254b5893183e6e7db0e333666dda711ca9a853b2d5070179d854f",
+ "hhea": "36 bytes, checksum 0x09D800F3, sha256 6ca77e5fbd56daf3cfb1b6fa175777fc37efb2dee3484e9b4eafc9b013685f37",
+ "hmtx": "48 bytes, checksum 0x14C501C4, sha256 beb829ee0b3b845cb6e98594d9558a6718caa5a6a5d0c51d3c14156ccc575788",
+ "loca": "26 bytes, checksum 0x04D2041D, sha256 7d7cfa93d31416192329dad07396fa208fb36dea393e3ec49d552e916798d08b",
+ "maxp": "32 bytes, checksum 0x00140067, sha256 9cbbb1d75753b1ffc75c0c0b2bc62dbe39970d0952d3cafaba7080450a4909d1",
+ "name": "602 bytes, checksum 0x351B4D7C, sha256 a3def774242f05f98ccd86071e838297b51d6977d62d5050843374956fa48320",
+ "post": "161 bytes, checksum 0xA62A664B, sha256 5b8d5360786a810d8f5eb32a72054f9c8a81ffe63c5ce9dd6ad9f96a6082da2d",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUni": 0
+ }
+ },
+ "useOTL=0xFF": {
+ "characters": 12,
+ "makeSubset": {
+ "bytes": 1992,
+ "sha256": "6a0802412d24a3261579faacf2e1638afea571cc1fe668d9e44f7f90a084e983",
+ "tables": {
+ "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86",
+ "cmap": "116 bytes, checksum 0xEA2DF256, sha256 858fa3d0b930efbfd42a1a02aff15a6a53e516696c1f7bc7643f9112b9cb0b09",
+ "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940",
+ "glyf": "724 bytes, checksum 0x106683EE, sha256 9b7c70ea6d174f85dde0974c87cdad53be0ed647e330f09244f2aac062db1454",
+ "head": "54 bytes, checksum 0x273BB4EB, sha256 28ed64f53c5f8f7f20a77dc0441beba115080a6772e3b37aca7f8bacd9020fa6",
+ "hhea": "36 bytes, checksum 0x09D800F3, sha256 6ca77e5fbd56daf3cfb1b6fa175777fc37efb2dee3484e9b4eafc9b013685f37",
+ "hmtx": "48 bytes, checksum 0x14C501C4, sha256 beb829ee0b3b845cb6e98594d9558a6718caa5a6a5d0c51d3c14156ccc575788",
+ "loca": "26 bytes, checksum 0x04EA0432, sha256 ea45c3bbe7f5968b94088c5ac2fcef60612554631d8b53d2c4603ca73ac20598",
+ "maxp": "32 bytes, checksum 0x00140067, sha256 9cbbb1d75753b1ffc75c0c0b2bc62dbe39970d0952d3cafaba7080450a4909d1",
+ "name": "602 bytes, checksum 0x351B4D7C, sha256 a3def774242f05f98ccd86071e838297b51d6977d62d5050843374956fa48320",
+ "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUni": 65535,
+ "defaultWidth": 600,
+ "codeToGlyph": "12 entries, sha256 5ce80ef51cc1a796dc4aface13e6f0653c3d26b58e6d2741578f9daf9c817bfe"
+ },
+ "makeSubsetSIP": {
+ "bytes": 1972,
+ "sha256": "331771ee04bc7484c65904868479d2e1fed985fcafe3a017f356bfffacc112f7",
+ "tables": {
+ "OS/2": "96 bytes, checksum 0x69A2D68F, sha256 fd2a99f2195a1e56bb4e59d20af2b5720421965a32625525a1c755330d490c8e",
+ "cmap": "94 bytes, checksum 0x006000B5, sha256 75724bdb0aad5c8bd651f10a19b526bbc16e46dd6bce95ecbe992a14008013d8",
+ "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940",
+ "glyf": "724 bytes, checksum 0x106983F0, sha256 6b70d49a1702b20d6542845a3da8b2916299b00c6d4403e98f4efad27f65385b",
+ "head": "54 bytes, checksum 0x273BB4EB, sha256 f713df5e403f19107633d9f312e2f67b49ea083792e67fa5fbcfc9ee1515f5f7",
+ "hhea": "36 bytes, checksum 0x09D800F3, sha256 6ca77e5fbd56daf3cfb1b6fa175777fc37efb2dee3484e9b4eafc9b013685f37",
+ "hmtx": "48 bytes, checksum 0x14C501C4, sha256 878b29797358aa6ba5b230946534b0f7f7de475126f154c144cee1d37866e512",
+ "loca": "26 bytes, checksum 0x03F2034C, sha256 5b9e234c7070bf60ac5622aa013fabe4d9e43cbdf2d879a11095dcfd3bc3ebc8",
+ "maxp": "32 bytes, checksum 0x00140067, sha256 9cbbb1d75753b1ffc75c0c0b2bc62dbe39970d0952d3cafaba7080450a4909d1",
+ "name": "602 bytes, checksum 0x35144D7C, sha256 9a414e77ed3ff545e08019d6eb0c1a54fa3874cc0737e4cac1843d94cc2549c7",
+ "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUniChar": 65535,
+ "defaultWidth": 600
+ },
+ "repackageTTF": {
+ "bytes": 2104,
+ "sha256": "c6820acff4d4cdfd8fe7b052ab1620e76a6b3970f8210316437a3d8d30fe9b28",
+ "tables": {
+ "OS/2": "96 bytes, checksum 0x83C35E30, sha256 c8d44e1e3e21744fb6447d75613e7d094658598ae11cd4ecd6ea51953585ed86",
+ "cmap": "108 bytes, checksum 0xECAFEFC8, sha256 fb705da3b20a90b7cda468287c78e3ae892a6894c7ae4bb018c5a3196efc9666",
+ "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940",
+ "glyf": "712 bytes, checksum 0xA188F2CF, sha256 be0e31a63f04eca9c536c5a128bfb0af8305bd8b968093ac56201d9728ac9f40",
+ "head": "54 bytes, checksum 0x273BB4EB, sha256 69365b1031c0ea9b54f8880029e54cea827e5d51a0acc915ebdb46e0bb5da720",
+ "hhea": "36 bytes, checksum 0x09D800F3, sha256 6ca77e5fbd56daf3cfb1b6fa175777fc37efb2dee3484e9b4eafc9b013685f37",
+ "hmtx": "48 bytes, checksum 0x14C501C4, sha256 beb829ee0b3b845cb6e98594d9558a6718caa5a6a5d0c51d3c14156ccc575788",
+ "loca": "26 bytes, checksum 0x04D2041D, sha256 7d7cfa93d31416192329dad07396fa208fb36dea393e3ec49d552e916798d08b",
+ "maxp": "32 bytes, checksum 0x00140067, sha256 9cbbb1d75753b1ffc75c0c0b2bc62dbe39970d0952d3cafaba7080450a4909d1",
+ "name": "602 bytes, checksum 0x351B4D7C, sha256 a3def774242f05f98ccd86071e838297b51d6977d62d5050843374956fa48320",
+ "post": "161 bytes, checksum 0xA62A664B, sha256 5b8d5360786a810d8f5eb32a72054f9c8a81ffe63c5ce9dd6ad9f96a6082da2d",
+ "prep": "7 bytes, checksum 0x68068C85, sha256 907c4106ff9989c1805827d5581bdf107a253b098cb71eb374f5e47c35604452"
+ },
+ "maxUni": 0
+ }
+ }
+}
diff --git a/tests/data/ttf/NotoSansArabic-ContextEdge-Synthetic.ttf b/tests/data/ttf/NotoSansArabic-ContextEdge-Synthetic.ttf
new file mode 100644
index 000000000..63407de00
Binary files /dev/null and b/tests/data/ttf/NotoSansArabic-ContextEdge-Synthetic.ttf differ