From ee7cb99c7b1ce46956a634842f80a964c2e056a2 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Thu, 17 Sep 2026 08:54:41 +1000 Subject: [PATCH 1/4] 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/4] 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]], From 91b06bc1b0aafb41870294254e4b253708738cff Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Thu, 17 Sep 2026 09:02:48 +1000 Subject: [PATCH 3/4] Select ZHH, ZHS and ZHT for a Chinese region in the lang attribute (#191) OtlTags::language() fell back from the language subtag to the language and region run together, 'zhhk', but the only region keys in Ucdn::$ot_languages are written with a hyphen, 'zh-hk'. The branch could never match, so zh-HK, zh-TW, zh-MO, zh-CN and zh-SG all laid text out with the script's DFLT entry. It now joins them with the hyphen. No font in tests/data/ttf or packages/ has a Chinese language system. NotoSansTC-RegionLangSys-Synthetic is a subset of Noto Sans TC whose GSUB offers hani with a 'locl' lookup under each of ZHH, ZHS and ZHT, giving the same character a different glyph under each. For every row of the issue's table and zh-Hant-HK, mPDF now draws the glyph hb-shape --language draws. No existing fixture moves: the golden masters set no language. HarfBuzz also reads the script subtag, takes ZHS for bare zh, and ZHTM then ZHH for Macao. That is #201, and OtlTagsTest pins zh, zh-Hant and zh-Hans as they stand. Test font: NotoSansTC-RegionLangSys-Synthetic (Noto Sans TC 2.004, OFL 1.1), built in fontTools 4.59.2. Co-Authored-By: Claude Opus 5 (1M context) --- src/Shaper/OtlTags.php | 5 +- tests/Mpdf/ChineseRegionLangSysTest.php | 59 +++++++++ tests/Mpdf/Shaper/OtlTagsTest.php | 33 +++++ .../NotoSansTC-RegionLangSys-Synthetic.json | 92 +++++++++++++ .../NotoSansTC-RegionLangSys-Synthetic.txt | 17 +++ .../NotoSansTC-RegionLangSys-Synthetic.txt | 92 +++++++++++++ .../NotoSansTC-RegionLangSys-Synthetic.json | 123 ++++++++++++++++++ .../NotoSansTC-RegionLangSys-Synthetic.ttf | Bin 0 -> 1860 bytes 8 files changed, 418 insertions(+), 3 deletions(-) create mode 100644 tests/Mpdf/ChineseRegionLangSysTest.php create mode 100644 tests/data/fontcache/NotoSansTC-RegionLangSys-Synthetic.json create mode 100644 tests/data/otldump/NotoSansTC-RegionLangSys-Synthetic.txt create mode 100644 tests/data/shaping/NotoSansTC-RegionLangSys-Synthetic.txt create mode 100644 tests/data/subset/NotoSansTC-RegionLangSys-Synthetic.json create mode 100644 tests/data/ttf/NotoSansTC-RegionLangSys-Synthetic.ttf diff --git a/src/Shaper/OtlTags.php b/src/Shaper/OtlTags.php index 4d08247e9..149ad7a05 100644 --- a/src/Shaper/OtlTags.php +++ b/src/Shaper/OtlTags.php @@ -101,9 +101,8 @@ public static function language($ietf, $available) if ($lang != '' && isset(Ucdn::$ot_languages[$lang])) { $langsys = Ucdn::$ot_languages[$lang]; - } elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$lang . $country])) { - // Never matches: the region keys are written 'zh-hk' (#191) - $langsys = Ucdn::$ot_languages[$lang . $country]; + } elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$lang . '-' . $country])) { + $langsys = Ucdn::$ot_languages[$lang . '-' . $country]; } else { $langsys = 'DFLT'; } diff --git a/tests/Mpdf/ChineseRegionLangSysTest.php b/tests/Mpdf/ChineseRegionLangSysTest.php new file mode 100644 index 000000000..7dde9fd5c --- /dev/null +++ b/tests/Mpdf/ChineseRegionLangSysTest.php @@ -0,0 +1,59 @@ + ['zh-HK', self::ZHH], + 'zh-Hant-HK' => ['zh-Hant-HK', self::ZHH], + 'zh-TW' => ['zh-TW', self::ZHT], + 'zh-MO' => ['zh-MO', self::ZHT], + 'zh-CN' => ['zh-CN', self::ZHS], + 'zh-SG' => ['zh-SG', self::ZHS], + 'lower case' => ['zh-tw', self::ZHT], + ]; + } + + /** + * @dataProvider regions + */ + public function testTheRegionSelectsTheLanguageSystem($lang, $expected) + { + $mpdf = new TextRecordingMpdf([ + 'mode' => 'utf-8', + 'fontDir' => [__DIR__ . '/../data/ttf'], + 'fontdata' => ['notosanstcregionlangsyssynthetic' => [ + 'R' => 'NotoSansTC-RegionLangSys-Synthetic.ttf', + 'useOTL' => 0xFF, + ]], + 'default_font' => 'notosanstcregionlangsyssynthetic', + ]); + $mpdf->WriteHTML(sprintf('

&#x%04X;

', $lang, self::BONE)); + + $drawn = array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8'))); + + $this->assertSame([$expected], $drawn); + } + +} diff --git a/tests/Mpdf/Shaper/OtlTagsTest.php b/tests/Mpdf/Shaper/OtlTagsTest.php index 3120bc8cc..a421cb2fd 100644 --- a/tests/Mpdf/Shaper/OtlTagsTest.php +++ b/tests/Mpdf/Shaper/OtlTagsTest.php @@ -123,4 +123,37 @@ public function languages() ]; } + /** + * @dataProvider chineseRegions + */ + public function testAChineseRegionSelectsItsLanguageSystem($ietf, $expected) + { + $this->assertSame($expected, OtlTags::language($ietf, 'DFLT ZHH ZHS ZHT ')); + } + + public function chineseRegions() + { + return [ + 'Hong Kong' => ['zh-HK', 'ZHH '], + 'Hong Kong, after the script' => ['zh-Hant-HK', 'ZHH '], + 'Taiwan' => ['zh-TW', 'ZHT '], + 'Macao' => ['zh-MO', 'ZHT '], + 'China' => ['zh-CN', 'ZHS '], + 'Singapore' => ['zh-SG', 'ZHS '], + 'lower case' => ['zh-tw', 'ZHT '], + 'a region the table has no entry for' => ['zh-US', 'DFLT'], + ]; + } + + /** + * Pins #201 as it stands: without a region, Chinese has no language system, where HarfBuzz + * takes ZHS for zh and zh-Hans and ZHT for zh-Hant. + */ + public function testChineseWithoutARegionHasNoLanguageSystem() + { + $this->assertSame('DFLT', OtlTags::language('zh', 'DFLT ZHH ZHS ZHT ')); + $this->assertSame('DFLT', OtlTags::language('zh-Hant', 'DFLT ZHH ZHS ZHT ')); + $this->assertSame('DFLT', OtlTags::language('zh-Hans', 'DFLT ZHH ZHS ZHT ')); + } + } diff --git a/tests/data/fontcache/NotoSansTC-RegionLangSys-Synthetic.json b/tests/data/fontcache/NotoSansTC-RegionLangSys-Synthetic.json new file mode 100644 index 000000000..8ddbee6f7 --- /dev/null +++ b/tests/data/fontcache/NotoSansTC-RegionLangSys-Synthetic.json @@ -0,0 +1,92 @@ +{ + "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.", + "fullName": "NotoSansTC-RegionLangSys-Synthetic", + "mtx": { + "GSUBScriptLang": { + "hani": "DFLT ZHH ZHS ZHT " + }, + "GSUBFeatures": { + "hani": { + "ZHH ": { + "locl": [ + 0 + ] + }, + "ZHS ": { + "locl": [ + 1 + ] + }, + "ZHT ": { + "locl": [ + 2 + ] + } + } + }, + "GSUBLookups": [ + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 124 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 138 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 152 + ], + "MarkFilteringSet": "" + } + ], + "GPOSScriptLang": [], + "GPOSFeatures": [], + "GPOSLookups": [], + "MarkGlyphSets": [], + "rtlPUAstr": "", + "haskernGPOS": false, + "hassmallcapsGSUB": false + }, + "cache": { + "GDEFdata.json": { + "GlyphClassBases": " 04E00| 04E09| 04E8C| 09AA8", + "GlyphClassMarks": "", + "GlyphClassLigatures": "", + "GlyphClassComponents": "", + "MarkGlyphSets": [], + "MarkAttachmentType": [] + }, + "GSUB.dat": "164 bytes, sha256 806298eba96771ffa947e91279efff39c5b091473cd2d52e8caf0c74bf95a2a0", + "GSUBdata.json": [ + [ + { + "39592": 0 + } + ], + [ + { + "39592": 0 + } + ], + [ + { + "39592": 0 + } + ] + ] + } +} diff --git a/tests/data/otldump/NotoSansTC-RegionLangSys-Synthetic.txt b/tests/data/otldump/NotoSansTC-RegionLangSys-Synthetic.txt new file mode 100644 index 000000000..194dc7412 --- /dev/null +++ b/tests/data/otldump/NotoSansTC-RegionLangSys-Synthetic.txt @@ -0,0 +1,17 @@ +

GDEF table

+

Glyph classes

+

Glyph class 1

+
Base glyph (single character, spacing glyph)
+
一 三 二 骨
+

GSUB Tables

+

GSUB Scripts & Languages

+
+
hani
ZHH : locl
ZHS : locl
ZHT : locl
+
+
GPOS table not defined
+ +=== detail: script hani language DFLT === +

GSUB Tables

+
This font's GSUB script "hani" offers no language system "DFLT". It has: ZHH, ZHS, ZHT
+ +
GPOS table not defined
diff --git a/tests/data/shaping/NotoSansTC-RegionLangSys-Synthetic.txt b/tests/data/shaping/NotoSansTC-RegionLangSys-Synthetic.txt new file mode 100644 index 000000000..ad76bec74 --- /dev/null +++ b/tests/data/shaping/NotoSansTC-RegionLangSys-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 +0x80 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC +0xFF +kern -liga 0628 0640 0645 0644 0627 => 0628 0640 0645 0644 0627 group=CCCCC +=== 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 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 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 0A38 0A3F group=CCCC +0x80 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A38 0A3F group=CCCC +0xFF +kern -liga 0A15 0A4D 0A38 0A3F => 0A15 0A4D 0A38 0A3F 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 => 1780 17D2 1781 17C1 group=CCCC +0x80 1780 17D2 1781 17C1 => 1780 17D2 1781 17C1 group=CCCC +0xFF +kern -liga 1780 17D2 1781 17C1 => 1780 17D2 1781 17C1 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 4E00 4E09 4E8C 9AA8 => 0020 4E00 4E09 4E8C 9AA8 group=SCCCC +0x80 0020 4E00 4E09 4E8C 9AA8 => 0020 4E00 4E09 4E8C 9AA8 group=SCCCC +0xFF +kern -liga 0020 4E00 4E09 4E8C 9AA8 => 0020 4E00 4E09 4E8C 9AA8 group=SCCCC diff --git a/tests/data/subset/NotoSansTC-RegionLangSys-Synthetic.json b/tests/data/subset/NotoSansTC-RegionLangSys-Synthetic.json new file mode 100644 index 000000000..5946e1a04 --- /dev/null +++ b/tests/data/subset/NotoSansTC-RegionLangSys-Synthetic.json @@ -0,0 +1,123 @@ +{ + "_": "Generated by composer subset:update. See tests/Mpdf/Fonts/SubsetGoldenMaster.php.", + "useOTL=0x00": { + "characters": 6, + "makeSubset": { + "bytes": 1664, + "sha256": "a1abcfed401e3fbadad0a4d06b5c89cba642e450b0e345d7af41bd332f9eecf5", + "tables": { + "OS/2": "96 bytes, checksum 0x553CF586, sha256 4feb4a7666c967ad3bedc80ca0480d43b41a41ffe07c31028a6ef52218d38859", + "cmap": "100 bytes, checksum 0x506935CE, sha256 617bb8af96e99dca7a32eb0759763fceba3a5e07a1ced67d44c6ef500e154263", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "456 bytes, checksum 0xA597EF24, sha256 fc281ecb0dd8d40dfcf54cc27fea6f485a3ce29b31ad44a858037941a97e99ab", + "head": "54 bytes, checksum 0x26853AD1, sha256 45d88851434214798d0d8b28231c529a7605d9ea1bd550274e60e9d68585adc0", + "hhea": "36 bytes, checksum 0x087602F7, sha256 b77957ff705ba9eb769b43f6e686e2652d488fd9997e2bb142f0a393a164f499", + "hmtx": "24 bytes, checksum 0x14680159, sha256 2f03ee1162e724d677550e1eff1b811c49271a8317e699eb03cc6f7e93232863", + "loca": "14 bytes, checksum 0x016600D0, sha256 d3868833ca7cd5facd47d789fdfa8e947052e749acd0ed29963710cf0d6c9ff8", + "maxp": "32 bytes, checksum 0x000F0051, sha256 277ce9daa2e509e2027ecde2a2ace2ebfb7456a1a4649389f6c4c6b477d40007", + "name": "618 bytes, checksum 0x32424A7A, sha256 843256959a3f193a9c9b4b1991e6342af742cf0658dae6febbf7dfacf3c50595", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUni": 65535, + "defaultWidth": 1000, + "codeToGlyph": "6 entries, sha256 249e8ea21e7631ac3f0c3cb3635abb85a5fa6606b6c9c93af3943ffe458bc252" + }, + "makeSubsetSIP": { + "bytes": 1648, + "sha256": "0cc56fb4c1c1c9680f3dde8cf88cf9da35d5af339f0c44a71976e9abc4b92230", + "tables": { + "OS/2": "96 bytes, checksum 0x5910D1E4, sha256 6f8275c184ad73eed089c9ae3dab631cdca809361b253ce8f23e4bf592ce780d", + "cmap": "82 bytes, checksum 0x00420076, sha256 f6d9593a66846b174499232d5c653827aca29fb4afa95c9d48a8b1a05f33bb84", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "456 bytes, checksum 0xA597EF24, sha256 fc281ecb0dd8d40dfcf54cc27fea6f485a3ce29b31ad44a858037941a97e99ab", + "head": "54 bytes, checksum 0x26853AD1, sha256 bb97a5e85cf7f4157a7389f729781b0ccf97c4ce78c1c64f078108df0d7f2ede", + "hhea": "36 bytes, checksum 0x087602F7, sha256 b77957ff705ba9eb769b43f6e686e2652d488fd9997e2bb142f0a393a164f499", + "hmtx": "24 bytes, checksum 0x14680159, sha256 2f03ee1162e724d677550e1eff1b811c49271a8317e699eb03cc6f7e93232863", + "loca": "14 bytes, checksum 0x016600D0, sha256 d3868833ca7cd5facd47d789fdfa8e947052e749acd0ed29963710cf0d6c9ff8", + "maxp": "32 bytes, checksum 0x000F0051, sha256 277ce9daa2e509e2027ecde2a2ace2ebfb7456a1a4649389f6c4c6b477d40007", + "name": "618 bytes, checksum 0x323B4A7A, sha256 1c9f67517afb47c153afb54af026607704646d2f3e3908c8e0bdf007f288f974", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUniChar": 65535, + "defaultWidth": 1000 + }, + "repackageTTF": { + "bytes": 1640, + "sha256": "f371f741569fcf26d365f58f366cedd2ed2a1d4193f97e39821c56f29877a52b", + "tables": { + "OS/2": "96 bytes, checksum 0x553CF586, sha256 4feb4a7666c967ad3bedc80ca0480d43b41a41ffe07c31028a6ef52218d38859", + "cmap": "84 bytes, checksum 0x9CAAE953, sha256 7099224c1a20cbdb0864041864d44a0fe6956d49b0759c97206a25d1b121f722", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "450 bytes, checksum 0x9F9AF520, sha256 8a6a9e3195830fdde21cc912fe50b04e40ef1b07d484fa55dfc90f061eeea27d", + "head": "54 bytes, checksum 0x26853AD1, sha256 085f676512d6f2f026e236b323e322ea539b32a88c026fae06545be7fce0f011", + "hhea": "36 bytes, checksum 0x087602F4, sha256 3b8df10855749dc2b94ca33deacd41aa368f250bd21bfe9561090e4dece3ed54", + "hmtx": "18 bytes, checksum 0x094000C9, sha256 d6dcf7d9a71a1793b58afdde15a319c3308777bb8a77dcb1561617fcdb87c4b6", + "loca": "14 bytes, checksum 0x015F00CA, sha256 2190e466db26bc483532782d3ea3b3fda85f12db1ef10acfa41e2aff62e640d8", + "maxp": "32 bytes, checksum 0x000F0051, sha256 277ce9daa2e509e2027ecde2a2ace2ebfb7456a1a4649389f6c4c6b477d40007", + "name": "618 bytes, checksum 0x32424A7A, sha256 843256959a3f193a9c9b4b1991e6342af742cf0658dae6febbf7dfacf3c50595", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUni": 0 + } + }, + "useOTL=0xFF": { + "characters": 6, + "makeSubset": { + "bytes": 1664, + "sha256": "a1abcfed401e3fbadad0a4d06b5c89cba642e450b0e345d7af41bd332f9eecf5", + "tables": { + "OS/2": "96 bytes, checksum 0x553CF586, sha256 4feb4a7666c967ad3bedc80ca0480d43b41a41ffe07c31028a6ef52218d38859", + "cmap": "100 bytes, checksum 0x506935CE, sha256 617bb8af96e99dca7a32eb0759763fceba3a5e07a1ced67d44c6ef500e154263", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "456 bytes, checksum 0xA597EF24, sha256 fc281ecb0dd8d40dfcf54cc27fea6f485a3ce29b31ad44a858037941a97e99ab", + "head": "54 bytes, checksum 0x26853AD1, sha256 45d88851434214798d0d8b28231c529a7605d9ea1bd550274e60e9d68585adc0", + "hhea": "36 bytes, checksum 0x087602F7, sha256 b77957ff705ba9eb769b43f6e686e2652d488fd9997e2bb142f0a393a164f499", + "hmtx": "24 bytes, checksum 0x14680159, sha256 2f03ee1162e724d677550e1eff1b811c49271a8317e699eb03cc6f7e93232863", + "loca": "14 bytes, checksum 0x016600D0, sha256 d3868833ca7cd5facd47d789fdfa8e947052e749acd0ed29963710cf0d6c9ff8", + "maxp": "32 bytes, checksum 0x000F0051, sha256 277ce9daa2e509e2027ecde2a2ace2ebfb7456a1a4649389f6c4c6b477d40007", + "name": "618 bytes, checksum 0x32424A7A, sha256 843256959a3f193a9c9b4b1991e6342af742cf0658dae6febbf7dfacf3c50595", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUni": 65535, + "defaultWidth": 1000, + "codeToGlyph": "6 entries, sha256 249e8ea21e7631ac3f0c3cb3635abb85a5fa6606b6c9c93af3943ffe458bc252" + }, + "makeSubsetSIP": { + "bytes": 1648, + "sha256": "0cc56fb4c1c1c9680f3dde8cf88cf9da35d5af339f0c44a71976e9abc4b92230", + "tables": { + "OS/2": "96 bytes, checksum 0x5910D1E4, sha256 6f8275c184ad73eed089c9ae3dab631cdca809361b253ce8f23e4bf592ce780d", + "cmap": "82 bytes, checksum 0x00420076, sha256 f6d9593a66846b174499232d5c653827aca29fb4afa95c9d48a8b1a05f33bb84", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "456 bytes, checksum 0xA597EF24, sha256 fc281ecb0dd8d40dfcf54cc27fea6f485a3ce29b31ad44a858037941a97e99ab", + "head": "54 bytes, checksum 0x26853AD1, sha256 bb97a5e85cf7f4157a7389f729781b0ccf97c4ce78c1c64f078108df0d7f2ede", + "hhea": "36 bytes, checksum 0x087602F7, sha256 b77957ff705ba9eb769b43f6e686e2652d488fd9997e2bb142f0a393a164f499", + "hmtx": "24 bytes, checksum 0x14680159, sha256 2f03ee1162e724d677550e1eff1b811c49271a8317e699eb03cc6f7e93232863", + "loca": "14 bytes, checksum 0x016600D0, sha256 d3868833ca7cd5facd47d789fdfa8e947052e749acd0ed29963710cf0d6c9ff8", + "maxp": "32 bytes, checksum 0x000F0051, sha256 277ce9daa2e509e2027ecde2a2ace2ebfb7456a1a4649389f6c4c6b477d40007", + "name": "618 bytes, checksum 0x323B4A7A, sha256 1c9f67517afb47c153afb54af026607704646d2f3e3908c8e0bdf007f288f974", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUniChar": 65535, + "defaultWidth": 1000 + }, + "repackageTTF": { + "bytes": 1648, + "sha256": "9791a9f8655fb19c326771aeb0030f7528ad4535e0359f0921d2996d5cfc13c3", + "tables": { + "OS/2": "96 bytes, checksum 0x553CF586, sha256 4feb4a7666c967ad3bedc80ca0480d43b41a41ffe07c31028a6ef52218d38859", + "cmap": "92 bytes, checksum 0x9CAAE980, sha256 e4692eb00f3df3286c3849816405f4499d6d2b1303588d995548e412dc50bbc0", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "450 bytes, checksum 0x9F9AF520, sha256 8a6a9e3195830fdde21cc912fe50b04e40ef1b07d484fa55dfc90f061eeea27d", + "head": "54 bytes, checksum 0x26853AD1, sha256 265b581a78ab6c32946ab7fdea796e29af28e7538ec71bf89f46ac44f51d6685", + "hhea": "36 bytes, checksum 0x087602F4, sha256 3b8df10855749dc2b94ca33deacd41aa368f250bd21bfe9561090e4dece3ed54", + "hmtx": "18 bytes, checksum 0x094000C9, sha256 d6dcf7d9a71a1793b58afdde15a319c3308777bb8a77dcb1561617fcdb87c4b6", + "loca": "14 bytes, checksum 0x015F00CA, sha256 2190e466db26bc483532782d3ea3b3fda85f12db1ef10acfa41e2aff62e640d8", + "maxp": "32 bytes, checksum 0x000F0051, sha256 277ce9daa2e509e2027ecde2a2ace2ebfb7456a1a4649389f6c4c6b477d40007", + "name": "618 bytes, checksum 0x32424A7A, sha256 843256959a3f193a9c9b4b1991e6342af742cf0658dae6febbf7dfacf3c50595", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUni": 0 + } + } +} diff --git a/tests/data/ttf/NotoSansTC-RegionLangSys-Synthetic.ttf b/tests/data/ttf/NotoSansTC-RegionLangSys-Synthetic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5ea726ebb3946c29d0f69913a818a9df3bba2240 GIT binary patch literal 1860 zcmbVNT})I*6#iyrxx1^t0xoQ|{#@kOfC!7Af>J@$tssbtZA0qM^1E7=-6+!48e^h0 zq_Ihx*fbzbP182UG(q}Q-w1tbW2${^w9!7Oed`0Jv1RY}o4bp##`K|gGv}P|oHO5? zId|qR1b{4@gTlVutvh!i4+db2(_64Rc%Wr!@AZ#~uM&UR8QkDMu=UABK)gkKq%RVR zOE+3?6hbzM0q@2}K6{EqhJ_iX8G-EP7%z zc@ciDe?-Tj_a=pLX8!HSVb9hlQ0g%~xO=1Isn!qg-2M}l$#FHSblnD|_VVF8$GCN$ zypK_day4r+*`c&hw#UFexNMJJ>EDk-{USa>1`GH=>Tb2Av^WI6*h%lw#l;vCDUuXH zb8AZ{w!@sfreaCH2gkTDb&ih8HS!*n34@jO<9@orW^6&Hj@vs>)Ly<%J;MtJxqwX8 zDQTM>=x}#jn!J`w()%1rKKNG7{52CFIG9?A(Z#0%71)d*hVX!{4D_>O7uYeyj=9O~ z0p83l%oUppCOSIx+q_AHxG=YJ?~Cu+J?d|_PP?6T0!u5>1+)r49_ZV%R?6m@tF zuj_8IilRD>Wrex6&Zy1x8cxF~D=jVa7x-)I>+1w5%WCtzhT*J|Wlgfq*CaRjvZZs= zrq$9%-!jh6FX~i#0*M_vFUguVt2ZF#133l0y$v$kxoTsnYpL5Rkt&fW3S^a(IDI~M zQK7r5@ya&`9GzD`X_DT?^YL}w^7#2JqU5(7a>eY~v*LT}-lK;h>*(q=E|(HnMOi|) zmWWTcZTrkWHdgJ-$#ql=4pv@XvfT7+^O!3$Z`aDa<=G9{ujY#z8+lv!IMKt-2#1^Y z6i3Py*hrMJC9-iRZ7ZIkU(>b&tzvcB&cHFzo3;)5Z=oE0SjY3}M>T55w%)e Date: Thu, 17 Sep 2026 09:07:12 +1000 Subject: [PATCH 4/4] Build the region key once, and draw one region per language system (#191) OtlTagsTest covers every row of the issue's table; the shaping test needs only show that each of ZHH, ZHS and ZHT reaches GSUB. Co-Authored-By: Claude Opus 5 (1M context) --- src/Shaper/OtlTags.php | 6 ++++-- tests/Mpdf/ChineseRegionLangSysTest.php | 4 ---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Shaper/OtlTags.php b/src/Shaper/OtlTags.php index 149ad7a05..1d86972aa 100644 --- a/src/Shaper/OtlTags.php +++ b/src/Shaper/OtlTags.php @@ -99,10 +99,12 @@ public static function language($ietf, $available) $country = strtolower($tags[2]); } + $region = $lang . '-' . $country; + if ($lang != '' && isset(Ucdn::$ot_languages[$lang])) { $langsys = Ucdn::$ot_languages[$lang]; - } elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$lang . '-' . $country])) { - $langsys = Ucdn::$ot_languages[$lang . '-' . $country]; + } elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$region])) { + $langsys = Ucdn::$ot_languages[$region]; } else { $langsys = 'DFLT'; } diff --git a/tests/Mpdf/ChineseRegionLangSysTest.php b/tests/Mpdf/ChineseRegionLangSysTest.php index 7dde9fd5c..be97538a8 100644 --- a/tests/Mpdf/ChineseRegionLangSysTest.php +++ b/tests/Mpdf/ChineseRegionLangSysTest.php @@ -26,12 +26,8 @@ public function regions() { return [ 'zh-HK' => ['zh-HK', self::ZHH], - 'zh-Hant-HK' => ['zh-Hant-HK', self::ZHH], 'zh-TW' => ['zh-TW', self::ZHT], - 'zh-MO' => ['zh-MO', self::ZHT], 'zh-CN' => ['zh-CN', self::ZHS], - 'zh-SG' => ['zh-SG', self::ZHS], - 'lower case' => ['zh-tw', self::ZHT], ]; }