From d5a87214b5c37e41fd953f9cf868e2013cbacf3c Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Tue, 15 Sep 2026 08:42:09 +1000 Subject: [PATCH] Read three keys for what they are, not for one shape each (#104) Three keys were read without checking they are there, each in a different entry shape than the read assumed. TTFontFile's 'locl' pre-pass and its joining-form branch both read ['replace'] on every entry carrying the tag, including the contextual entries that keep their replacements in ['rules'] and have no such key. Both now tell the two apart, as the loop between them already did. A joining form can be a Multiple Substitution - the dotless form of a letter and the dots to draw under it - and the whole replacement was pushed into the Private Use Area list as if it named one glyph. hexdec() raised PHP 8's invalid-characters deprecation on it and returned a number far outside E000-F8FF, so every glyph it named was dropped. The three places that collect these glyphs now share one method, which splits. Otl::GSUBsubstitute() carries a category onto the glyphs a substitution makes for the Indic, Khmer, Sinhala and Myanmar shapers, and had no branch for the South East Asian one, so a Tai Tham cluster that reaches the reorderer through a 'ccmp' ligature was read for a ['sea_category'] it had not got. What the read turns on is that the last glyph of the cluster came out of that pass, not how long the cluster is - Lanna Alif warns on a cluster of two and Noto Sans Tai Tham on one. Nothing moves on a page. The parser output is identical over 104 fonts but for rtlPUAstr on the two Arabic fonts, which each gain one PUA codepoint that mPDF's Arabic shaper cannot reach; the Tai Tham snapshot is byte-identical to the one the code before this writes. Test fonts: NotoSansDevanagari-ContextualLocl-Subset (4 glyphs of Noto Sans Devanagari 2.005, OFL 1.1) and NotoSansArabic-MultipleForm-Subset (8 glyphs of Noto Sans Arabic 2.012, OFL 1.1). Co-Authored-By: Claude Opus 5 (1M context) --- src/Otl.php | 6 + src/TTFontFile.php | 46 +++-- tests/Mpdf/Shaper/SeaTest.php | 103 ++++++++++ tests/Mpdf/TTFontFileTest.php | 62 ++++++ tests/Snapshots/TaiThamSakotSnapshotTest.php | 143 ++++++++++++++ .../NotoSansArabic-MultipleForm-Subset.json | 181 ++++++++++++++++++ ...oSansDevanagari-ContextualLocl-Subset.json | 70 +++++++ .../NotoSansArabic-MultipleForm-Subset.txt | 28 +++ ...toSansDevanagari-ContextualLocl-Subset.txt | 24 +++ tests/data/snapshots/tai-tham-sakot.pdf | Bin 0 -> 42725 bytes .../NotoSansArabic-MultipleForm-Subset.ttf | Bin 0 -> 3840 bytes ...toSansDevanagari-ContextualLocl-Subset.ttf | Bin 0 -> 2776 bytes 12 files changed, 647 insertions(+), 16 deletions(-) create mode 100644 tests/Mpdf/Shaper/SeaTest.php create mode 100644 tests/Snapshots/TaiThamSakotSnapshotTest.php create mode 100644 tests/data/fontcache/NotoSansArabic-MultipleForm-Subset.json create mode 100644 tests/data/fontcache/NotoSansDevanagari-ContextualLocl-Subset.json create mode 100644 tests/data/otldump/NotoSansArabic-MultipleForm-Subset.txt create mode 100644 tests/data/otldump/NotoSansDevanagari-ContextualLocl-Subset.txt create mode 100644 tests/data/snapshots/tai-tham-sakot.pdf create mode 100644 tests/data/ttf/NotoSansArabic-MultipleForm-Subset.ttf create mode 100644 tests/data/ttf/NotoSansDevanagari-ContextualLocl-Subset.ttf diff --git a/src/Otl.php b/src/Otl.php index 0237092b0..80cae4122 100644 --- a/src/Otl.php +++ b/src/Otl.php @@ -2592,6 +2592,9 @@ function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos = null) } elseif ($this->shaper == 'M') { $newOTLdata[$i]['myanmar_category'] = $this->OTLdata[$pos]['myanmar_category']; $newOTLdata[$i]['myanmar_position'] = $this->OTLdata[$pos]['myanmar_position']; + } elseif ($this->shaper == 'E') { + $newOTLdata[$i]['sea_category'] = $this->OTLdata[$pos]['sea_category']; + $newOTLdata[$i]['sea_position'] = $this->OTLdata[$pos]['sea_position']; } if (isset($this->OTLdata[$pos]['mask'])) { $newOTLdata[$i]['mask'] = $this->OTLdata[$pos]['mask']; @@ -2784,6 +2787,9 @@ function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos = null) } elseif ($this->shaper == 'M') { $newOTLdata[0]['myanmar_category'] = $this->OTLdata[$pos]['myanmar_category']; $newOTLdata[0]['myanmar_position'] = $this->OTLdata[$pos]['myanmar_position']; + } elseif ($this->shaper == 'E') { + $newOTLdata[0]['sea_category'] = $this->OTLdata[$pos]['sea_category']; + $newOTLdata[0]['sea_position'] = $this->OTLdata[$pos]['sea_position']; } if (isset($this->OTLdata[$pos]['mask'])) { $newOTLdata[0]['mask'] = $this->OTLdata[$pos]['mask']; diff --git a/src/TTFontFile.php b/src/TTFontFile.php index d8b579280..083cc51fe 100644 --- a/src/TTFontFile.php +++ b/src/TTFontFile.php @@ -1312,6 +1312,27 @@ function _getClassDefinitionTable($offset = 0) return $GlyphByClass; } + /** + * Add the Private Use Area glyphs a replacement names to the list magic_reverse_dir reads. + * + * A replacement is one glyph code where the substitution replaces one glyph with one, and a + * space-separated list where it replaces one with several - the dotless form of a letter and the + * dots to draw under it, say. + * + * @param string[] $rtlpua the list being built + * @param string $replace one glyph code, or several separated by spaces + */ + private function addPuaGlyphs(array &$rtlpua, $replace) + { + foreach (explode(' ', $replace) as $glyph) { + // Unanchored: a flattened contextual rule carries its backreferences in the same token as + // the glyph code, so '0FB93\2' names 0FB93 + if (preg_match('/(0[EF][A-F0-9]{3})/', $glyph, $matched)) { + $rtlpua[] = $matched[1]; + } + } + } + /** * GSUB - Glyph Substitution */ @@ -2122,7 +2143,9 @@ function _getGSUBtables() foreach ($volt as $v) { // isol fina fin2 fin3 medi med2 for Syriac // ISOLATED FORM :: FINAL :: INITIAL :: MEDIAL :: MED2 :: FIN2 :: FIN3 - if (strpos('isol fina init medi fin2 fin3 med2', $v['tag']) !== false) { + // A contextual entry carries the feature's own tag but keeps its replacements in + // ['rules'], so it has no ['replace'] for this branch to read + if (strpos('isol fina init medi fin2 fin3 med2', $v['tag']) !== false && !isset($v['context'])) { $key = $v['match']; $key = preg_replace('/[\(\)]*/', '', $key); @@ -2153,7 +2176,7 @@ function _getGSUBtables() if (isset($v['ignore']) && $v['ignore']) { $rtl[$key]['ignore'][$kk] = $v['ignore']; } - $rtlpua[] = $sub; + $this->addPuaGlyphs($rtlpua, $sub); } else { // Add any other glyphs which are in PUA if (isset($v['context']) && $v['context']) { @@ -2161,23 +2184,12 @@ function _getGSUBtables() $matchCount = count($vs['match']); for ($i = 0; $i < $matchCount; $i++) { if (isset($vs['replace'][$i]) && preg_match('/^0[A-F0-9]{4}$/', $vs['match'][$i])) { - if (preg_match('/^0[EF][A-F0-9]{3}$/', $vs['replace'][$i])) { - $rtlpua[] = $vs['replace'][$i]; - } + $this->addPuaGlyphs($rtlpua, $vs['replace'][$i]); } } } } else { - preg_match_all('/\((0[A-F0-9]{4})\)/', $v['match'], $m); - $matchCount = count($m[0]); - for ($i = 0; $i < $matchCount; $i++) { - $sb = explode(' ', $v['replace']); - foreach ($sb as $sbg) { - if (preg_match('/(0[EF][A-F0-9]{3})/', $sbg, $mr)) { - $rtlpua[] = $mr[1]; - } - } - } + $this->addPuaGlyphs($rtlpua, $v['replace']); } } } @@ -2213,9 +2225,11 @@ function _getGSUBtables() } // First get 'locl' substitutions (reversed!) + // Contextual entries are skipped here as they are above: their replacements are in + // ['rules'], not ['replace'] $loclsubs = []; foreach ($volt as $v) { - if (strpos('locl', $v['tag']) !== false) { + if (strpos('locl', $v['tag']) !== false && !isset($v['context'])) { $key = $v['match']; $key = preg_replace('/[\(\)]*/', '', $key); $sub = $v['replace']; diff --git a/tests/Mpdf/Shaper/SeaTest.php b/tests/Mpdf/Shaper/SeaTest.php new file mode 100644 index 000000000..535ba7415 --- /dev/null +++ b/tests/Mpdf/Shaper/SeaTest.php @@ -0,0 +1,103 @@ +drawn([self::SAKOT, self::HIGH_KA]); + + restore_error_handler(); + + $this->assertSame([], $raised); + $this->assertSame([self::DOTTED_CIRCLE, self::SAKOT_HIGH_KA], $drawn); + } + + /** + * The same Sakot inside a well-formed cluster, which is what the rule is for: the consonant in + * front of it is the base, no dotted circle is needed, and the pair after it is subscripted under + * it. + */ + public function testASakotInsideAClusterSubscriptsTheConsonantAfterIt() + { + $this->assertSame( + [self::HIGH_KA, self::SAKOT_HIGH_KA], + $this->drawn([self::HIGH_KA, self::SAKOT, self::HIGH_KA]) + ); + } + + /** + * A Sakot on its own ligates with nothing, so no glyph the categorising pass missed ever reaches + * the reorderer and the character is drawn as it was written. + */ + public function testASakotWithNothingAfterItIsDrawnAsItself() + { + $this->assertSame([self::SAKOT], $this->drawn([self::SAKOT])); + } + + /** + * @param int[] $codepoints + * + * @return int[] the codepoints of the line as it is handed to the drawing code, in visual order + */ + private function drawn($codepoints) + { + $html = ''; + foreach ($codepoints as $codepoint) { + $html .= sprintf('&#x%04X;', $codepoint); + } + + $mpdf = new TextRecordingMpdf(); + $mpdf->WriteHTML('

' . $html . '

'); + + return array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8'))); + } + +} diff --git a/tests/Mpdf/TTFontFileTest.php b/tests/Mpdf/TTFontFileTest.php index 829c8ad7d..715c525fb 100644 --- a/tests/Mpdf/TTFontFileTest.php +++ b/tests/Mpdf/TTFontFileTest.php @@ -83,6 +83,68 @@ public function testGetMetricsReadsMarkGlyphSetsCoverage() $this->assertSame([' 00DCA| 00DD2| 00DD3', ' 0E00A'], $this->ttf->MarkGlyphSets); } + /** + * The Indic pre-pass that indexes 'locl' substitutions by their replacement read ['replace'] on + * every entry tagged 'locl'. A contextual entry keeps its replacements in ['rules'] and has no + * such key, so it read null once per font load. + * + * The subset is the contextual 'locl' of Noto Sans Devanagari 2.005 (OFL 1.1): a chained context + * under the Santali language system whose backtrack is U+0905 or U+0906 and whose nested lookup + * substitutes a Santali nukta for U+093C. Nothing is indexed from it either way, so the + * diagnostic is all there was to fix. + */ + public function testAContextualLoclEntryIsNotReadForATopLevelReplacement() + { + $raised = $this->diagnosticsWhileParsing('NotoSansDevanagari-ContextualLocl-Subset.ttf'); + + $this->assertSame([], $raised); + $this->assertSame('NotoSansDevanagari-Regular', $this->ttf->fullName); + } + + /** + * A joining form can be a Multiple Substitution, so the replacement recorded for one can name more + * than one glyph. The whole string was pushed into the Private Use Area list as if it named one, + * and hexdec() raised PHP 8's invalid-characters deprecation on it and returned a number far + * outside the PUA, so every glyph in it was dropped. + * + * The subset is Noto Sans Arabic 2.012 (OFL 1.1) cut down to U+0628 and U+06CC. The Farsi Yeh's + * initial and medial forms are Multiple Substitutions replacing it with the dotless form and the + * pair of dots to draw under it - E007 here, which has no codepoint of its own and is the glyph + * the whole-string push lost. + */ + public function testAJoiningFormOfMoreThanOneGlyphContributesEachOfThem() + { + $raised = $this->diagnosticsWhileParsing('NotoSansArabic-MultipleForm-Subset.ttf'); + + $this->assertSame([], $raised); + $this->assertSame('\x{0E000}-\x{0E003}\x{0E005}-\x{0E007}', $this->ttf->rtlPUAstr); + } + + /** + * Parse a font, collecting every diagnostic PHP raised doing it. Deprecations are not converted to + * exceptions, so a handler is what sees them. + * + * @return string[] + */ + private function diagnosticsWhileParsing($file) + { + $raised = []; + + set_error_handler(function ($number, $message, $path, $line) use (&$raised) { + $raised[] = sprintf('%s in %s:%d', $message, basename($path), $line); + + return true; + }); + + try { + $this->ttf->getMetrics(__DIR__ . '/../data/ttf/' . $file, uniqid('', true), 0, false, false, 0xFF); + } finally { + restore_error_handler(); + } + + return $raised; + } + /** * debugfont mode validates each table's version as it goes, so it walks the byte offsets by a * different route than normal mode and has its own chance to lose its place diff --git a/tests/Snapshots/TaiThamSakotSnapshotTest.php b/tests/Snapshots/TaiThamSakotSnapshotTest.php new file mode 100644 index 000000000..4bc222f52 --- /dev/null +++ b/tests/Snapshots/TaiThamSakotSnapshotTest.php @@ -0,0 +1,143 @@ +mpdf and + * loading it with content + * + * @return void + * @internal Don't call any $this->mpdf->Output*() method + */ + public function generatePdf() + { + $this->mpdf = $this->createMpdf(['mode' => 'utf-8']); + $this->mpdf->WriteHTML($this->style() . $this->samples()); + } + + private function style() + { + ob_start(); + ?> + + +

mPDF

+

Tai Tham clusters joined by a Sakot

+

A Sakot subscripts the consonant after it, and the two are drawn as one glyph + the font substitutes for the pair. Written with no consonant in front of them they are a + broken cluster, and a dotted circle is inserted to stand in for the consonant that is + missing. Read the subscript form under each: the broken cluster draws the same one the + well-formed cluster does.

+ sample('Sakot, High Ka - no base, so a dotted circle carries the pair', self::SAKOT . self::HIGH_KA) + . $this->sample('High Ka, Sakot, High Ka - the same pair under a base, the form to match', self::HIGH_KA . self::SAKOT . self::HIGH_KA) + . $this->sample('High Ka alone - the base by itself', self::HIGH_KA) + . $this->sample('Sakot alone - nothing to subscript, drawn as it was written', self::SAKOT) + . $this->sample('Sakot, High Xa - the same rule one consonant over', self::SAKOT . self::HIGH_XA) + . $this->sample('Sakot, High Ka, Vowel Sign Aa - a vowel after the broken cluster', self::SAKOT . self::HIGH_KA . self::VOWEL_AA) + . $this->sample('High Ka, Sakot, High Ka, Vowel Sign Aa - the same vowel after the base', self::HIGH_KA . self::SAKOT . self::HIGH_KA . self::VOWEL_AA); + } + + private function sample($label, $text) + { + return '

' . $label . '

' + . '

' . $text . '

'; + } + + /** + * The document only shows anything if the broken cluster draws the subscript form the well-formed + * cluster draws, rather than the two characters it was written as. The glyph has no codepoint of + * its own, so the samples are compared with each other rather than by naming it. + * + * @return int[] the glyphs the sample was drawn with, left to right + */ + private function glyphsOf($text) + { + $mpdf = new TextRecordingMpdf(['mode' => 'utf-8']); + $mpdf->WriteHTML($this->style() . '

' . $text . '

'); + + return array_values(unpack('N*', mb_convert_encoding(end($mpdf->drawnText), 'UTF-32BE', 'UTF-8'))); + } + + /** + * The Sakot and the consonant after it become one glyph either way. In the broken cluster a dotted + * circle stands where the base would be, and the subscript form is the same one the base takes. + */ + public function testTheBrokenClusterDrawsTheSubscriptFormTheWellFormedClusterDraws() + { + $broken = $this->glyphsOf(self::SAKOT . self::HIGH_KA); + $wellFormed = $this->glyphsOf(self::HIGH_KA . self::SAKOT . self::HIGH_KA); + + $this->assertCount(2, $broken, 'the broken cluster draws something other than a stand-in and one subscript form'); + $this->assertSame(end($wellFormed), end($broken), 'the broken cluster draws a subscript form the well-formed one does not'); + $this->assertNotSame($this->glyphsOf(self::HIGH_KA)[0], $broken[0], 'the stand-in is the consonant rather than a dotted circle'); + } + + /** + * The Sakot is genuinely replaced rather than drawn alongside the form that replaces it. + */ + public function testTheSakotIsNotDrawnBesideTheFormThatReplacesIt() + { + $alone = $this->glyphsOf(self::SAKOT); + $subscripted = $this->glyphsOf(self::SAKOT . self::HIGH_KA); + + $this->assertNotContains($alone[0], $subscripted, 'the pair draws the Sakot rather than the form that replaces it'); + } +} diff --git a/tests/data/fontcache/NotoSansArabic-MultipleForm-Subset.json b/tests/data/fontcache/NotoSansArabic-MultipleForm-Subset.json new file mode 100644 index 000000000..a37c16ca2 --- /dev/null +++ b/tests/data/fontcache/NotoSansArabic-MultipleForm-Subset.json @@ -0,0 +1,181 @@ +{ + "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.", + "fullName": "NotoSansArabic-Regular", + "mtx": { + "GSUBScriptLang": { + "arab": "DFLT " + }, + "GSUBFeatures": { + "arab": { + "DFLT": { + "ccmp": [ + 0 + ], + "init": [ + 1 + ], + "medi": [ + 2 + ], + "fina": [ + 3 + ] + } + } + }, + "GSUBLookups": [ + { + "Type": 2, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 104 + ], + "MarkFilteringSet": "" + }, + { + "Type": 2, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 132 + ], + "MarkFilteringSet": "" + }, + { + "Type": 2, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 160 + ], + "MarkFilteringSet": "" + }, + { + "Type": 2, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 188 + ], + "MarkFilteringSet": "" + } + ], + "GPOSScriptLang": { + "arab": "DFLT " + }, + "GPOSFeatures": { + "arab": { + "DFLT": { + "mark": [ + 0 + ], + "mkmk": [ + 1 + ] + } + } + }, + "GPOSLookups": [ + { + "Type": 4, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 72 + ], + "MarkFilteringSet": "" + }, + { + "Type": 6, + "Flag": 16, + "SubtableCount": 1, + "Subtables": [ + 154 + ], + "MarkFilteringSet": 1 + } + ], + "MarkGlyphSets": [ + " 0E007", + " 0E006| 0E007" + ], + "rtlPUAstr": "\\x{0E000}-\\x{0E003}\\x{0E005}-\\x{0E007}", + "haskernGPOS": false, + "hassmallcapsGSUB": false + }, + "cache": { + "GDEFdata.json": { + "GlyphClassBases": " 00020| 0E000| 0E001| 0E002| 0E003| 0E005", + "GlyphClassMarks": " 0E006| 0E007", + "GlyphClassLigatures": "", + "GlyphClassComponents": "", + "MarkGlyphSets": [ + " 0E007", + " 0E006| 0E007" + ], + "MarkAttachmentType": [] + }, + "GPOS.dat": "214 bytes, sha256 6f1081afffd2d5c5a30b17e71f251c17a584618bfbc40346bc042504df84b694", + "GPOSdata.json": [ + [ + { + "57350": 0, + "57351": 1 + } + ], + [ + { + "57350": 0, + "57351": 1 + } + ] + ], + "GSUB.arab.DFLT.json": { + "rtlSUB": { + "006CC": { + "2": "0E003 0E007", + "3": "0E002 0E007", + "1": "0E005" + }, + "0E000": { + "2": "0E003", + "3": "0E002", + "1": "0E001" + } + }, + "finals": "0E001 0E005 ", + "rphf": [], + "half": [], + "pref": [], + "blwf": [], + "pstf": [] + }, + "GSUB.dat": "214 bytes, sha256 2c5dc6ca7b1b0726e8838189ee37155b1d356e15ca530ccf02d949c091ae1637", + "GSUBdata.json": [ + [ + { + "1576": 0 + } + ], + [ + { + "57344": 0, + "1740": 1 + } + ], + [ + { + "57344": 0, + "1740": 1 + } + ], + [ + { + "57344": 0, + "1740": 1 + } + ] + ] + } +} diff --git a/tests/data/fontcache/NotoSansDevanagari-ContextualLocl-Subset.json b/tests/data/fontcache/NotoSansDevanagari-ContextualLocl-Subset.json new file mode 100644 index 000000000..58500e0d1 --- /dev/null +++ b/tests/data/fontcache/NotoSansDevanagari-ContextualLocl-Subset.json @@ -0,0 +1,70 @@ +{ + "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.", + "fullName": "NotoSansDevanagari-Regular", + "mtx": { + "GSUBScriptLang": { + "dev2": "SAT " + }, + "GSUBFeatures": { + "dev2": { + "SAT ": { + "locl": [ + 0 + ] + } + } + }, + "GSUBLookups": [ + { + "Type": 6, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 64 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 98 + ], + "MarkFilteringSet": "" + } + ], + "GPOSScriptLang": [], + "GPOSFeatures": [], + "GPOSLookups": [], + "MarkGlyphSets": [], + "rtlPUAstr": "", + "haskernGPOS": false, + "hassmallcapsGSUB": false + }, + "cache": { + "GDEFdata.json": { + "GlyphClassBases": " 00905| 00906", + "GlyphClassMarks": " 0093C| 0E001", + "GlyphClassLigatures": "", + "GlyphClassComponents": "", + "MarkGlyphSets": [], + "MarkAttachmentType": [] + }, + "GPOS.dat": "16 bytes, sha256 db40218e56c29eac86a5130fe0abcb62caa3d8bba870ca1d05c9060ad63dd29f", + "GPOSdata.json": [], + "GSUB.dat": "110 bytes, sha256 7d8abc87c74095b51303e243a3d897e90358ea673a2f40725938cb9ddf569e05", + "GSUBdata.json": [ + [ + { + "2364": 0 + } + ], + [ + { + "2364": 0 + } + ] + ] + } +} diff --git a/tests/data/otldump/NotoSansArabic-MultipleForm-Subset.txt b/tests/data/otldump/NotoSansArabic-MultipleForm-Subset.txt new file mode 100644 index 000000000..5b6b5b688 --- /dev/null +++ b/tests/data/otldump/NotoSansArabic-MultipleForm-Subset.txt @@ -0,0 +1,28 @@ +

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

+
◌
+

Mark Glyph Set class: 1

+
◌ ◌
+

GSUB Tables

+

GSUB Scripts & Languages

+
+
arab
DFLT: ccmp init medi fina
+
+

GPOS Tables

+

GPOS Scripts & Languages

+
arab
DFLT: mark mkmk
+ + +=== detail: script arab language DFLT === +

GSUB Tables

+
Lookup #0 [tag: ccmp]
Subtable #0
LookupType 2: Multiple Substitution Subtable
U+0628   ب  » »    ◌  M+E000, M+E006
Lookup #1 [tag: init]
Subtable #0
LookupType 2: Multiple Substitution Subtable
M+E000     » »     M+E003
U+06CC   ی  » »    ◌  M+E003, M+E007
Lookup #2 [tag: medi]
Subtable #0
LookupType 2: Multiple Substitution Subtable
M+E000     » »     M+E002
U+06CC   ی  » »    ◌  M+E002, M+E007
Lookup #3 [tag: fina]
Subtable #0
LookupType 2: Multiple Substitution Subtable
M+E000     » »     M+E001
U+06CC   ی  » »     M+E005
+

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[1]
Subtable #0
LookupType 6: MarkToMark attachment
Marks: ◌ ◌
Bases: ◌ ◌
Example(s): ◌   ◌  
diff --git a/tests/data/otldump/NotoSansDevanagari-ContextualLocl-Subset.txt b/tests/data/otldump/NotoSansDevanagari-ContextualLocl-Subset.txt new file mode 100644 index 000000000..2d16e9485 --- /dev/null +++ b/tests/data/otldump/NotoSansDevanagari-ContextualLocl-Subset.txt @@ -0,0 +1,24 @@ +

GDEF table

+

Glyph classes

+

Glyph class 1

+
Base glyph (single character, spacing glyph)
+
अ आ
+

Glyph class 3

+
Mark glyph (non-spacing combining glyph)
+
◌़ ◌
+

GSUB Tables

+

GSUB Scripts & Languages

+
+
dev2
SAT : locl
+
+

GPOS Tables

+

GPOS Scripts & Languages

+
No entries in GPOS table.
+ + +=== detail: script dev2 language SAT === +

GSUB Tables

+
Lookup #0 [tag: locl]
Subtable #0
LookupType 6: Chaining Contextual Substitution Subtable
Format 3: Coverage-based Chaining Context Glyph Substitution
CONTEXT:
Backtrack #0: U+0905, U+0906
Input #0:  ◌़ 
Substitution Position: 0
Lookup #1 [tag: locl]
Subtable #0
LookupType 1: Single Substitution Subtable
U+093C   ◌़  » »   ◌  M+E001
+

GPOS Tables

+
This font's GPOS table offers no script "dev2". It has: none
+ diff --git a/tests/data/snapshots/tai-tham-sakot.pdf b/tests/data/snapshots/tai-tham-sakot.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a192b6a8758b51673a3b91193b18fca1d26a6da1 GIT binary patch literal 42725 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

Fw^d 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

`!n~;1m2dPcNn_W<^YMzVaV5ypXnZi`f-+(w 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 literal 0 HcmV?d00001 diff --git a/tests/data/ttf/NotoSansDevanagari-ContextualLocl-Subset.ttf b/tests/data/ttf/NotoSansDevanagari-ContextualLocl-Subset.ttf new file mode 100644 index 0000000000000000000000000000000000000000..240383e5103532dfc8b77e2976b372b593991ceb GIT binary patch literal 2776 zcmb7GU2IcT9RHuwyKe2iS=X_#CAZyJ2aIiYtm9(=x^=4zw2TfxLk49l?O^Td+OaLh z1Y;o4U}6jpCX$873&CXZfkYonN-`dssQvO>9g+M7_COK}`(FDX;<;W+GL@Tm78iyfmq2cqnjVjdqhbaAw;@-h zV)GfQq4yzQgRG}xsrceYpC5<+J))w;%yc$4eEHIS)aU+AWM<--r2X2=D(K9D}in~v2qn?H6f~0&cm;XdI>!RtDCEvdpxz9 z{cd-y%UddHhaMZ0yQpsv$wU?*Xwq7`vcJXdcoG!x!t|~KyR&`tvmvt* z%Uue_TE#9Es&>XpQYPj#@5np)AiC}wRzps5;h1cmUuW&U-wt9zJ>i0Cd z>n4xS3QvJ)9ga$w)UXA+)=7m*3@i! zwPM4;sxn7Opt7X6P~cH%<#JwY1s05mCLeaiI?31sbtn9l_Hrwy)`|X?it~32L^IM)f#VD5{+SNG0$m297G|-3CWXn^N zu(5Z3DZd1Q@!KxslX!FNUs}15lReDK4y0x&dbAbO4kHHISgJkfMDuAZJ+h~}6FnNu33a`l*QGEG6wmeols%%^(w2tQy=n|+hr6T>P)rC7idnk;Z5y5-Uqg+;|_3~_|wV77O zZbhwMUwkx zS=u&8#4iO-qw9gkL$v5w}5?+#R19oy=9N+2rt*t+WcpXk&R7_4TCOQBSk-kD