From ee7cb99c7b1ce46956a634842f80a964c2e056a2 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Thu, 17 Sep 2026 08:54:41 +1000 Subject: [PATCH 1/2] Fall back from a v2 Indic tag to its own original tag only (#192) OtlTags::script() walked the v2-to-original table from the run's own entry to the end, so a v2 tag the font did not offer reached the original tag of every script listed after it: Bengali was laid out with a font's deva lookups and the original specification's reordering, but Devanagari never reached beng. HarfBuzz asks for dev3, dev2 and deva for Devanagari (hb_ot_all_tags_from_script) and then DFLT, dflt and latn (hb_ot_layout_table_select_script), never another script's tag. The walk is now one lookup of the run's own original tag. NotoSansBengali-DevaScript-Synthetic makes it visible: a subset of Noto Sans Bengali with its GSUB replaced by a DFLT and a deva script, each with a 'locl' lookup giving KA a different glyph. KA drew deva's KHA; it now draws DFLT's GA, as hb-shape does. One existing fixture moves. NotoSansGurmukhiUI-GPOS71-Subset offers only gur2 and guru, so the golden master's Devanagari and Bengali runs used to reach guru and be reordered to the original specification. They now find no script and are left unshaped, as a run is in any font with no entry for its script. The font has no glyph for either run. Test font: NotoSansBengali-DevaScript-Synthetic (Noto Sans Bengali 3.011, OFL 1.1), built in fontTools 4.59.2. Co-Authored-By: Claude Opus 5 (1M context) --- src/Shaper/OtlTags.php | 17 +-- tests/Mpdf/IndicScriptFallbackTest.php | 39 ++++++ tests/Mpdf/Shaper/OtlTagsTest.php | 20 ++- .../NotoSansBengali-DevaScript-Synthetic.json | 92 +++++++++++++ .../NotoSansBengali-DevaScript-Synthetic.txt | 34 +++++ .../NotoSansBengali-DevaScript-Synthetic.txt | 92 +++++++++++++ .../NotoSansGurmukhiUI-GPOS71-Subset.txt | 12 +- .../NotoSansBengali-DevaScript-Synthetic.json | 123 ++++++++++++++++++ .../NotoSansBengali-DevaScript-Synthetic.ttf | Bin 0 -> 2448 bytes 9 files changed, 404 insertions(+), 25 deletions(-) create mode 100644 tests/Mpdf/IndicScriptFallbackTest.php create mode 100644 tests/data/fontcache/NotoSansBengali-DevaScript-Synthetic.json create mode 100644 tests/data/otldump/NotoSansBengali-DevaScript-Synthetic.txt create mode 100644 tests/data/shaping/NotoSansBengali-DevaScript-Synthetic.txt create mode 100644 tests/data/subset/NotoSansBengali-DevaScript-Synthetic.json create mode 100644 tests/data/ttf/NotoSansBengali-DevaScript-Synthetic.ttf diff --git a/src/Shaper/OtlTags.php b/src/Shaper/OtlTags.php index 56576e8d6..d6107071c 100644 --- a/src/Shaper/OtlTags.php +++ b/src/Shaper/OtlTags.php @@ -16,11 +16,6 @@ class OtlTags /** * The v2 Indic and Myanmar tags, each with the original tag that a font made to the earlier * specification offers instead. - * - * Order is load-bearing. A v2 tag the font does not offer is tried against its own original tag - * and then against the original tags of every entry after it, not only its own: a Bengali run in - * a font offering neither bng2 nor beng is laid out as deva if the font has that. That is #192, - * kept here as it was. */ private static $originalIndicTags = [ 'bng2' => 'beng', @@ -60,14 +55,10 @@ public static function script(array $ScriptLang, $scripttag, $scriptblock, $shap return [$scripttag, false]; } - if ($shaper) { - $reached = false; - foreach (self::$originalIndicTags as $v2 => $original) { - $reached = $reached || $v2 === $scripttag; - if ($reached && isset($ScriptLang[$original])) { - return [$original, true]; - } - } + // Only the run's own original tag: another Indic script's lookups and reordering are no fit + // for it, and a font offering nothing for the script is laid out by its default entry + if ($shaper && isset(self::$originalIndicTags[$scripttag], $ScriptLang[self::$originalIndicTags[$scripttag]])) { + return [self::$originalIndicTags[$scripttag], true]; } foreach (['DFLT', 'dflt', 'latn'] as $fallback) { diff --git a/tests/Mpdf/IndicScriptFallbackTest.php b/tests/Mpdf/IndicScriptFallbackTest.php new file mode 100644 index 000000000..5336897d1 --- /dev/null +++ b/tests/Mpdf/IndicScriptFallbackTest.php @@ -0,0 +1,39 @@ + 'utf-8', + 'fontDir' => [__DIR__ . '/../data/ttf'], + 'fontdata' => ['notosansbengalidevascriptsynthetic' => [ + 'R' => 'NotoSansBengali-DevaScript-Synthetic.ttf', + 'useOTL' => 0xFF, + ]], + 'default_font' => 'notosansbengalidevascriptsynthetic', + ]); + $mpdf->WriteHTML(sprintf('

&#x%04X;

', self::KA)); + + $drawn = array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8'))); + + $this->assertSame([self::GA], $drawn); + } + +} diff --git a/tests/Mpdf/Shaper/OtlTagsTest.php b/tests/Mpdf/Shaper/OtlTagsTest.php index eefdb4f24..c4d8cb281 100644 --- a/tests/Mpdf/Shaper/OtlTagsTest.php +++ b/tests/Mpdf/Shaper/OtlTagsTest.php @@ -55,14 +55,22 @@ public function testTheOriginalIndicTagIsNotTriedWithoutAShaper() } /** - * Pins #192 as it stands: a v2 tag reaches the original tags listed after its own, but never - * those before it. + * @dataProvider otherIndicScripts */ - public function testAV2TagFallsThroughToTheOriginalTagsListedAfterItsOwn() + public function testAV2TagFallsBackToItsOwnOriginalTagAndNoOtherScriptsOriginalTag(array $offered, $scripttag, $scriptblock, array $expected) { - $this->assertSame(['deva', true], OtlTags::script(['deva' => 'DFLT', 'DFLT' => 'DFLT'], 'bng2', Ucdn::SCRIPT_BENGALI, 'I', 0xFF)); - $this->assertSame(['mymr', true], OtlTags::script(['mymr' => 'DFLT'], 'tml2', Ucdn::SCRIPT_TAMIL, 'I', 0xFF)); - $this->assertSame(['DFLT', false], OtlTags::script(['beng' => 'DFLT', 'DFLT' => 'DFLT'], 'dev2', Ucdn::SCRIPT_DEVANAGARI, 'I', 0xFF)); + $this->assertSame($expected, OtlTags::script($offered, $scripttag, $scriptblock, 'I', 0xFF)); + } + + public function otherIndicScripts() + { + return [ + 'Bengali, where the font offers deva' => [['deva' => 'DFLT', 'DFLT' => 'DFLT'], 'bng2', Ucdn::SCRIPT_BENGALI, ['DFLT', false]], + 'Devanagari, where the font offers gujr' => [['gujr' => 'DFLT', 'DFLT' => 'DFLT'], 'dev2', Ucdn::SCRIPT_DEVANAGARI, ['DFLT', false]], + 'Tamil, where the font offers only mymr' => [['mymr' => 'DFLT'], 'tml2', Ucdn::SCRIPT_TAMIL, ['', false]], + 'Devanagari, where the font offers beng' => [['beng' => 'DFLT', 'DFLT' => 'DFLT'], 'dev2', Ucdn::SCRIPT_DEVANAGARI, ['DFLT', false]], + 'Bengali, where the font offers beng and deva' => [['deva' => 'DFLT', 'beng' => 'DFLT'], 'bng2', Ucdn::SCRIPT_BENGALI, ['beng', true]], + ]; } public function testATagThatIsNotAV2IndicTagHasNoOriginalToFallBackTo() diff --git a/tests/data/fontcache/NotoSansBengali-DevaScript-Synthetic.json b/tests/data/fontcache/NotoSansBengali-DevaScript-Synthetic.json new file mode 100644 index 000000000..b13f93c35 --- /dev/null +++ b/tests/data/fontcache/NotoSansBengali-DevaScript-Synthetic.json @@ -0,0 +1,92 @@ +{ + "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.", + "fullName": "NotoSansBengali-DevaScript-Synthetic", + "mtx": { + "GSUBScriptLang": { + "DFLT": "DFLT ", + "deva": "DFLT " + }, + "GSUBFeatures": { + "DFLT": { + "DFLT": { + "locl": [ + 0 + ] + } + }, + "deva": { + "DFLT": { + "locl": [ + 1 + ] + } + } + }, + "GSUBLookups": [ + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 88 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 102 + ], + "MarkFilteringSet": "" + } + ], + "GPOSScriptLang": [], + "GPOSFeatures": [], + "GPOSLookups": [], + "MarkGlyphSets": [ + " 009CD", + " 009CD", + " 009CD", + " 009CD", + " 009CD", + " 009CD", + " 009CD" + ], + "rtlPUAstr": "", + "haskernGPOS": false, + "hassmallcapsGSUB": false + }, + "cache": { + "GDEFdata.json": { + "GlyphClassBases": " 00995| 00996| 00997| 009B7| 009BF| 025CC", + "GlyphClassMarks": " 009CD", + "GlyphClassLigatures": "", + "GlyphClassComponents": "", + "MarkGlyphSets": [ + " 009CD", + " 009CD", + " 009CD", + " 009CD", + " 009CD", + " 009CD", + " 009CD" + ], + "MarkAttachmentType": [] + }, + "GSUB.dat": "114 bytes, sha256 db414921ffcf0b37c47e2d31afc271c25f46f9c9037a44e957346ab2cb8d203c", + "GSUBdata.json": [ + [ + { + "2453": 0 + } + ], + [ + { + "2453": 0 + } + ] + ] + } +} diff --git a/tests/data/otldump/NotoSansBengali-DevaScript-Synthetic.txt b/tests/data/otldump/NotoSansBengali-DevaScript-Synthetic.txt new file mode 100644 index 000000000..bb08c18bd --- /dev/null +++ b/tests/data/otldump/NotoSansBengali-DevaScript-Synthetic.txt @@ -0,0 +1,34 @@ +

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

+
◌্
+

Mark Glyph Set class: 2

+
◌্
+

Mark Glyph Set class: 3

+
◌্
+

Mark Glyph Set class: 4

+
◌্
+

Mark Glyph Set class: 5

+
◌্
+

Mark Glyph Set class: 6

+
◌্
+

GSUB Tables

+

GSUB Scripts & Languages

+
+
DFLT
DFLT: locl
deva
DFLT: locl
+
+
GPOS table not defined
+ +=== detail: script deva language DFLT === +

GSUB Tables

+
Lookup #1 [tag: locl]
Subtable #0
LookupType 1: Single Substitution Subtable
U+0995   ক  » »   খ  U+0996
+
GPOS table not defined
diff --git a/tests/data/shaping/NotoSansBengali-DevaScript-Synthetic.txt b/tests/data/shaping/NotoSansBengali-DevaScript-Synthetic.txt new file mode 100644 index 000000000..2cf00a3c5 --- /dev/null +++ b/tests/data/shaping/NotoSansBengali-DevaScript-Synthetic.txt @@ -0,0 +1,92 @@ +=== 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 => 0997 09CD 09BF 09B7 group=CMCC +0x80 0995 09CD 09B7 09BF => 0997 09CD 09BF 09B7 group=CMCC +0xFF +kern -liga 0995 09CD 09B7 09BF => 0997 09CD 09BF 09B7 group=CMCC +=== 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 0995 0996 0997 09B7 09BF 09CD 25CC => 0020 0997 0996 0997 09BF 09B7 09CD 25CC group=SCCCCCMC +0x80 0020 0995 0996 0997 09B7 09BF 09CD 25CC => 0020 0997 0996 0997 09BF 09B7 09CD 25CC group=SCCCCCMC +0xFF +kern -liga 0020 0995 0996 0997 09B7 09BF 09CD 25CC => 0020 0997 0996 0997 09BF 09B7 09CD 25CC group=SCCCCCMC diff --git a/tests/data/shaping/NotoSansGurmukhiUI-GPOS71-Subset.txt b/tests/data/shaping/NotoSansGurmukhiUI-GPOS71-Subset.txt index 3a53cd963..582d0fed0 100644 --- a/tests/data/shaping/NotoSansGurmukhiUI-GPOS71-Subset.txt +++ b/tests/data/shaping/NotoSansGurmukhiUI-GPOS71-Subset.txt @@ -27,13 +27,13 @@ 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 +0xFF 0915 094D 0937 093F => 0915 094D 0937 093F group=CCCC +0x80 0915 094D 0937 093F => 0915 094D 0937 093F group=CCCC +0xFF +kern -liga 0915 094D 0937 093F => 0915 094D 0937 093F 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 +0xFF 0995 09CD 09B7 09BF => 0995 09CD 09B7 09BF group=CCCC +0x80 0995 09CD 09B7 09BF => 0995 09CD 09B7 09BF group=CCCC +0xFF +kern -liga 0995 09CD 09B7 09BF => 0995 09CD 09B7 09BF 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 diff --git a/tests/data/subset/NotoSansBengali-DevaScript-Synthetic.json b/tests/data/subset/NotoSansBengali-DevaScript-Synthetic.json new file mode 100644 index 000000000..466d4e103 --- /dev/null +++ b/tests/data/subset/NotoSansBengali-DevaScript-Synthetic.json @@ -0,0 +1,123 @@ +{ + "_": "Generated by composer subset:update. See tests/Mpdf/Fonts/SubsetGoldenMaster.php.", + "useOTL=0x00": { + "characters": 9, + "makeSubset": { + "bytes": 2172, + "sha256": "2b070c5507b72f3099900f22ce28d7233fe455e9eb3069a2393a040808104b5f", + "tables": { + "OS/2": "96 bytes, checksum 0x805479C8, sha256 9cdd48b09cc447f76c442e442f291973e5714da75917e3f81368e260671a2172", + "cmap": "108 bytes, checksum 0x394D146C, sha256 ab74a71cdee3ef547f433849232e56931cce629a6fb644c9be66e18832c4790d", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "1004 bytes, checksum 0x17873991, sha256 b147e21d29e0207fad56ab068b6224b76a19c0a1dbfc102a33c92d69f2e10283", + "head": "54 bytes, checksum 0x1E5EBE3E, sha256 88143d8e9e2e4d4440cfb237ead4cc255604a4c758bf1ebec75ae5c97c58c366", + "hhea": "36 bytes, checksum 0x068903B5, sha256 e7330e6f902fd111c3cb0932e4da6c2d5ee8208a28a8ba30bd33515f007c69c8", + "hmtx": "36 bytes, checksum 0x111C007C, sha256 793b6cad091178302ca7acf847b6a98b3f1d3fb83671c6456914464e17a3dd3e", + "loca": "20 bytes, checksum 0x047C054C, sha256 d7a03b8d662bbcbf0886ecac8efc8edeea8a1144b9c08cf4f003b45e9922c56a", + "maxp": "32 bytes, checksum 0x00120045, sha256 9b5303cdff58e0fc16dbdeec5fc8df4437bd1daf3b8feb4d9bc5371f666cbd60", + "name": "554 bytes, checksum 0x304F4847, sha256 53d37780d25d3cedee36a7f6c2fd301a886b31282044132e21354a6e3b26f6b3", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUni": 65535, + "defaultWidth": 600, + "codeToGlyph": "9 entries, sha256 27676bbb4e70064f70b3bcbe0ddaa10526e80cd0f75ace6dd28e137393622478" + }, + "makeSubsetSIP": { + "bytes": 2152, + "sha256": "3cb6d8e0f46dd78bbaafbb7e54431cf28b9ae189c945bf58745d141bd3b045b9", + "tables": { + "OS/2": "96 bytes, checksum 0x6633D504, sha256 b29abfebe280b7b9247efba33c975d35734f180921ca523e732af21026e0f8c8", + "cmap": "88 bytes, checksum 0x0046009F, sha256 a51feaf8ac92130c6dbb6928e930ab98438f8538e2d11d389bc59faa982dcf4a", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "1004 bytes, checksum 0x17873991, sha256 3e3813ae7fcb41995ce434a59ec7809da764e031159772f810ee8ef726af543b", + "head": "54 bytes, checksum 0x1E5EBE3E, sha256 baf1018a1085793358d2e46b1de6304ce7f7ec6d730a000538c3536eceacda45", + "hhea": "36 bytes, checksum 0x068903B5, sha256 e7330e6f902fd111c3cb0932e4da6c2d5ee8208a28a8ba30bd33515f007c69c8", + "hmtx": "36 bytes, checksum 0x111C007C, sha256 9ffe6237ef9b4c84c931452a84f34f09c417f51e5ac1ef50c9cdd7864cfaa26f", + "loca": "20 bytes, checksum 0x03E0051A, sha256 a5c051ad4bb0091359bd0da187ad4cfc7a4794bb4e8ce4d647a8ece4dcaa05fd", + "maxp": "32 bytes, checksum 0x00120045, sha256 9b5303cdff58e0fc16dbdeec5fc8df4437bd1daf3b8feb4d9bc5371f666cbd60", + "name": "554 bytes, checksum 0x30484847, sha256 c27a8885ea6d4c75cf1f0edaa35fa4a7ec4dc8289dca79390087230b14df4e21", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUniChar": 65535, + "defaultWidth": 600 + }, + "repackageTTF": { + "bytes": 2224, + "sha256": "4cfdf86d9b0563df70e3eb76f44e02b417c02b3ac6a1ec8f319b40cee3ffd42c", + "tables": { + "OS/2": "96 bytes, checksum 0x805479C8, sha256 9cdd48b09cc447f76c442e442f291973e5714da75917e3f81368e260671a2172", + "cmap": "92 bytes, checksum 0x5EC8EEB7, sha256 96f224410f3d7266dae94d9626b249591d84eab39e6e3653531088e1a0790b8d", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "996 bytes, checksum 0xB3639DAF, sha256 c9f3a1edbb1e3aee2c874d0401a77a00fd372d34321f0adbf8eef95ba179eaae", + "head": "54 bytes, checksum 0x1E5EBE3E, sha256 ac184bee7cb4c3d6b5c44640c820c5146d7b739974ce403bdcee79280bef965f", + "hhea": "36 bytes, checksum 0x068903B5, sha256 e7330e6f902fd111c3cb0932e4da6c2d5ee8208a28a8ba30bd33515f007c69c8", + "hmtx": "36 bytes, checksum 0x111C007C, sha256 793b6cad091178302ca7acf847b6a98b3f1d3fb83671c6456914464e17a3dd3e", + "loca": "20 bytes, checksum 0x04740542, sha256 3bce5cbaf580b8c53a539a8644b950d6c140731d00189bfaa6a721c8cfadf87b", + "maxp": "32 bytes, checksum 0x00120045, sha256 9b5303cdff58e0fc16dbdeec5fc8df4437bd1daf3b8feb4d9bc5371f666cbd60", + "name": "554 bytes, checksum 0x304F4847, sha256 53d37780d25d3cedee36a7f6c2fd301a886b31282044132e21354a6e3b26f6b3", + "post": "108 bytes, checksum 0x8B7ABECC, sha256 f00be54574ac6dd29adbf678f195657fd56d5fd05bcc9338426d891783123f0c" + }, + "maxUni": 0 + } + }, + "useOTL=0xFF": { + "characters": 9, + "makeSubset": { + "bytes": 2172, + "sha256": "2b070c5507b72f3099900f22ce28d7233fe455e9eb3069a2393a040808104b5f", + "tables": { + "OS/2": "96 bytes, checksum 0x805479C8, sha256 9cdd48b09cc447f76c442e442f291973e5714da75917e3f81368e260671a2172", + "cmap": "108 bytes, checksum 0x394D146C, sha256 ab74a71cdee3ef547f433849232e56931cce629a6fb644c9be66e18832c4790d", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "1004 bytes, checksum 0x17873991, sha256 b147e21d29e0207fad56ab068b6224b76a19c0a1dbfc102a33c92d69f2e10283", + "head": "54 bytes, checksum 0x1E5EBE3E, sha256 88143d8e9e2e4d4440cfb237ead4cc255604a4c758bf1ebec75ae5c97c58c366", + "hhea": "36 bytes, checksum 0x068903B5, sha256 e7330e6f902fd111c3cb0932e4da6c2d5ee8208a28a8ba30bd33515f007c69c8", + "hmtx": "36 bytes, checksum 0x111C007C, sha256 793b6cad091178302ca7acf847b6a98b3f1d3fb83671c6456914464e17a3dd3e", + "loca": "20 bytes, checksum 0x047C054C, sha256 d7a03b8d662bbcbf0886ecac8efc8edeea8a1144b9c08cf4f003b45e9922c56a", + "maxp": "32 bytes, checksum 0x00120045, sha256 9b5303cdff58e0fc16dbdeec5fc8df4437bd1daf3b8feb4d9bc5371f666cbd60", + "name": "554 bytes, checksum 0x304F4847, sha256 53d37780d25d3cedee36a7f6c2fd301a886b31282044132e21354a6e3b26f6b3", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUni": 65535, + "defaultWidth": 600, + "codeToGlyph": "9 entries, sha256 27676bbb4e70064f70b3bcbe0ddaa10526e80cd0f75ace6dd28e137393622478" + }, + "makeSubsetSIP": { + "bytes": 2152, + "sha256": "3cb6d8e0f46dd78bbaafbb7e54431cf28b9ae189c945bf58745d141bd3b045b9", + "tables": { + "OS/2": "96 bytes, checksum 0x6633D504, sha256 b29abfebe280b7b9247efba33c975d35734f180921ca523e732af21026e0f8c8", + "cmap": "88 bytes, checksum 0x0046009F, sha256 a51feaf8ac92130c6dbb6928e930ab98438f8538e2d11d389bc59faa982dcf4a", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "1004 bytes, checksum 0x17873991, sha256 3e3813ae7fcb41995ce434a59ec7809da764e031159772f810ee8ef726af543b", + "head": "54 bytes, checksum 0x1E5EBE3E, sha256 baf1018a1085793358d2e46b1de6304ce7f7ec6d730a000538c3536eceacda45", + "hhea": "36 bytes, checksum 0x068903B5, sha256 e7330e6f902fd111c3cb0932e4da6c2d5ee8208a28a8ba30bd33515f007c69c8", + "hmtx": "36 bytes, checksum 0x111C007C, sha256 9ffe6237ef9b4c84c931452a84f34f09c417f51e5ac1ef50c9cdd7864cfaa26f", + "loca": "20 bytes, checksum 0x03E0051A, sha256 a5c051ad4bb0091359bd0da187ad4cfc7a4794bb4e8ce4d647a8ece4dcaa05fd", + "maxp": "32 bytes, checksum 0x00120045, sha256 9b5303cdff58e0fc16dbdeec5fc8df4437bd1daf3b8feb4d9bc5371f666cbd60", + "name": "554 bytes, checksum 0x30484847, sha256 c27a8885ea6d4c75cf1f0edaa35fa4a7ec4dc8289dca79390087230b14df4e21", + "post": "32 bytes, checksum 0xFF9F0032, sha256 4fdc83d5e42fe44650fe86f15a645d5cc926d07355ba945e1ede42737607bf83" + }, + "maxUniChar": 65535, + "defaultWidth": 600 + }, + "repackageTTF": { + "bytes": 2232, + "sha256": "585844e6acc37a9037e0d3d813e567ffeb33304b49d4a619f1224cc3b232fb05", + "tables": { + "OS/2": "96 bytes, checksum 0x805479C8, sha256 9cdd48b09cc447f76c442e442f291973e5714da75917e3f81368e260671a2172", + "cmap": "100 bytes, checksum 0x5EC8EEE4, sha256 82f1e3137c11b2b5df509fae504c72128e559311b143b570891f83b225628bcc", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "996 bytes, checksum 0xB3639DAF, sha256 c9f3a1edbb1e3aee2c874d0401a77a00fd372d34321f0adbf8eef95ba179eaae", + "head": "54 bytes, checksum 0x1E5EBE3E, sha256 cac6f72be16f931cb5f0d3e6f0f999f860bc27f97291492a29f504c68832668f", + "hhea": "36 bytes, checksum 0x068903B5, sha256 e7330e6f902fd111c3cb0932e4da6c2d5ee8208a28a8ba30bd33515f007c69c8", + "hmtx": "36 bytes, checksum 0x111C007C, sha256 793b6cad091178302ca7acf847b6a98b3f1d3fb83671c6456914464e17a3dd3e", + "loca": "20 bytes, checksum 0x04740542, sha256 3bce5cbaf580b8c53a539a8644b950d6c140731d00189bfaa6a721c8cfadf87b", + "maxp": "32 bytes, checksum 0x00120045, sha256 9b5303cdff58e0fc16dbdeec5fc8df4437bd1daf3b8feb4d9bc5371f666cbd60", + "name": "554 bytes, checksum 0x304F4847, sha256 53d37780d25d3cedee36a7f6c2fd301a886b31282044132e21354a6e3b26f6b3", + "post": "108 bytes, checksum 0x8B7ABECC, sha256 f00be54574ac6dd29adbf678f195657fd56d5fd05bcc9338426d891783123f0c" + }, + "maxUni": 0 + } + } +} diff --git a/tests/data/ttf/NotoSansBengali-DevaScript-Synthetic.ttf b/tests/data/ttf/NotoSansBengali-DevaScript-Synthetic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..23d34aef7bf8ce361fbc4d526cf71c6a34b1a52d GIT binary patch literal 2448 zcmb7GZERCj7=F*WTf0JOcW2wnM%&%>bExa*Zrg2)v2LT|Ynw803MngPY$aPa_FbEo#~6_qgYk!wm>7eYXrezfCK@$<{E@`yB0p$-&TZ#_XpHCPp7-;- z?{nUB?m3qj5pAH8B+*Ewe_%VM=})4%PodqJ+515Hx!@CiB9jyH{P5mz?BwY3M?`E4 z@_2qGH<$hB*UON){v+r$%P539K zpfEfmy^ZlU$iAtW#pAM*9)tXjkf&$!xthh=G{&bO+h%gd=cs`OU>CAl%*{+hh6gi5 zh6sG_o||1*eD2Zrt|9dS*r!R%P2!)OZoYK+pmob1BuPI*Lti}k+N*;8_~|Fd%2!Ii zOHt`0L<4c5MZ-AhY-tL85OxiAeO$$;_a;5IiTbHdmqn)ATMbXK7pNAvp5>xiyDF4r zDaJ6PTN)Fw)V~#(;o%HjA=Q7ais5^99!5O_-^h#LRWcxEmX)ZT)#xZ!_*shaQ{>?D zc>+)BC1R{MX85=vC9%HM#uVN^xa) zWpJhIE3J|r9&61;1w0=x4=b=s>>KtQB4z>f2EU4IMxq6|MfSJIEm`(r={(a)AM>j> z6LEeRx!Y-)UF95JR#K>$f)u7C^$_)XwDx$+ZmX@*V3jIWD*cDdPP$yK(zi~hYlq9_ zJmYk_UjhFkl%Vc2pzItv*A3Ao+`jrd)gd>)@`Q>r+!ZE1hI679@yH;;FWySxLL%*`*y zl#t3?UTe6sx!&kY?r7WT_qhDc&5FOvT-R16lAl2OA#Y)An@UQSUsjrH6D+iKb z(!#x~00lw=q=1&RIpVQIGFbs>47#~u6myCzal2P$zj!B}8VQU|hHOg5hWNnUof*I1 z(X_$gwwN8(Zl_~ouWPHN(b_XJa995HLrrzHTvLt~w&b?0&zplupEIn~#CqN7gc@sb z%8jbk++eD2YP`2^gFTvbcitDSbI1c@A(Oky%dV#}OQ3fE{R==J1$sGdM+rR!BWjMi z3fRV=KAknC;ssu|C@lRmhD6 zQ+pEj{Vm;%aa%&Q+EP0rmS)xZykld$aY?K?s|yVF#O0p)P-ohQk4wk%1uWGN9Qw*-ryjJr&Ec0P2A+DdFFdexG`@SW#pIG_3p|){#O(VH`gV1O zI<_j^5jonVS6-Zj^i%n|_;xc0gMAP*_nt_$ZadL0hYx95`@?Pc{EhUmI8ACy;4k2< z@QhNBDm=&e@_Ch)a7KPt<#G0~K$WkhCRVKS2I!y0Zk?q$TBdoNDw8xti}>PV6v3;3 z8-->9bO>IHcvTvK=PVtCHn02k(h}$tbn~;p1Ur-D9f^%iWa8CK)p ziRYi@*#B2g$t><#n=V)I*nm=4E0O_JJh-X< E0x!O`Z2$lO literal 0 HcmV?d00001 From b495d170eeea97978bce23c1c06365960e4d4e8e Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Thu, 17 Sep 2026 08:58:23 +1000 Subject: [PATCH 2/2] Look a v2 tag's original tag up once, and name the fallback cases for what they cover (#192) Co-Authored-By: Claude Opus 5 (1M context) --- src/Shaper/OtlTags.php | 5 +++-- tests/Mpdf/Shaper/OtlTagsTest.php | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Shaper/OtlTags.php b/src/Shaper/OtlTags.php index d6107071c..4d08247e9 100644 --- a/src/Shaper/OtlTags.php +++ b/src/Shaper/OtlTags.php @@ -57,8 +57,9 @@ public static function script(array $ScriptLang, $scripttag, $scriptblock, $shap // Only the run's own original tag: another Indic script's lookups and reordering are no fit // for it, and a font offering nothing for the script is laid out by its default entry - if ($shaper && isset(self::$originalIndicTags[$scripttag], $ScriptLang[self::$originalIndicTags[$scripttag]])) { - return [self::$originalIndicTags[$scripttag], true]; + $original = isset(self::$originalIndicTags[$scripttag]) ? self::$originalIndicTags[$scripttag] : ''; + if ($shaper && $original && isset($ScriptLang[$original])) { + return [$original, true]; } foreach (['DFLT', 'dflt', 'latn'] as $fallback) { diff --git a/tests/Mpdf/Shaper/OtlTagsTest.php b/tests/Mpdf/Shaper/OtlTagsTest.php index c4d8cb281..3120bc8cc 100644 --- a/tests/Mpdf/Shaper/OtlTagsTest.php +++ b/tests/Mpdf/Shaper/OtlTagsTest.php @@ -55,14 +55,14 @@ public function testTheOriginalIndicTagIsNotTriedWithoutAShaper() } /** - * @dataProvider otherIndicScripts + * @dataProvider originalTagFallbacks */ public function testAV2TagFallsBackToItsOwnOriginalTagAndNoOtherScriptsOriginalTag(array $offered, $scripttag, $scriptblock, array $expected) { $this->assertSame($expected, OtlTags::script($offered, $scripttag, $scriptblock, 'I', 0xFF)); } - public function otherIndicScripts() + public function originalTagFallbacks() { return [ 'Bengali, where the font offers deva' => [['deva' => 'DFLT', 'DFLT' => 'DFLT'], 'bng2', Ucdn::SCRIPT_BENGALI, ['DFLT', false]],