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
DFLT: init medi fina
arab
DFLT: init medi fina
cyrl
DFLT: init medi fina
dev2
DFLT: init medi fina
grek
DFLT: init medi fina
latn
DFLT: init medi fina
+
+

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 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