From ee7cb99c7b1ce46956a634842f80a964c2e056a2 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Thu, 17 Sep 2026 08:54:41 +1000 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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], ]; } From aa91d97ba831c2ee5298f9f1a2869bca0614b216 Mon Sep 17 00:00:00 2001 From: Jake Jackson Date: Thu, 17 Sep 2026 09:25:58 +1000 Subject: [PATCH 5/6] Select ZHS or ZHT from a Chinese script subtag, ZHS for zh alone, and ZHTM then ZHH for Macao (#201) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OtlTags::language() read the language subtag, then the language and region. Nothing mapped a script subtag and bare zh had no key, so zh, zh-Hans and zh-Hant laid text out with the script's DFLT entry, and a script that disagreed with its region took the region's tag: zh-Hans-HK got ZHH, zh-Hant-CN got ZHS. Macao got ZHT. Chinese now follows HarfBuzz 14.3.1's hb_ot_tags_from_complex_language(): Traditional in Hong Kong or Macao keeps the region's tag, then Hans is ZHS and Hant is ZHT whatever the region, then a hk, mo or tw subtag gives ZHH, ZHTM then ZHH, or ZHT, and anything else is ZHS. language() tries each candidate against the script in turn, so its return value and Otl.php are unchanged. zh on its own now selects ZHS where a font offers it. No font in tests/data/ttf or packages/ has a Chinese language system other than the synthetic test fonts, and no golden master or snapshot sets lang="zh". NotoSansTC-MacaoLangSys-Synthetic is #191's font with 四 under a fourth language system, ZHTM. For every case in ChineseLangSysTest mPDF draws the glyph hb-shape --language draws. Test font: NotoSansTC-MacaoLangSys-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 | 75 ++++++++--- tests/Mpdf/ChineseLangSysTest.php | 74 +++++++++++ tests/Mpdf/ChineseRegionLangSysTest.php | 55 -------- tests/Mpdf/Shaper/OtlTagsTest.php | 38 +++--- .../NotoSansTC-MacaoLangSys-Synthetic.json | 111 ++++++++++++++++ .../NotoSansTC-MacaoLangSys-Synthetic.txt | 17 +++ .../NotoSansTC-MacaoLangSys-Synthetic.txt | 92 +++++++++++++ .../NotoSansTC-MacaoLangSys-Synthetic.json | 123 ++++++++++++++++++ .../ttf/NotoSansTC-MacaoLangSys-Synthetic.ttf | Bin 0 -> 2064 bytes 9 files changed, 494 insertions(+), 91 deletions(-) create mode 100644 tests/Mpdf/ChineseLangSysTest.php delete mode 100644 tests/Mpdf/ChineseRegionLangSysTest.php create mode 100644 tests/data/fontcache/NotoSansTC-MacaoLangSys-Synthetic.json create mode 100644 tests/data/otldump/NotoSansTC-MacaoLangSys-Synthetic.txt create mode 100644 tests/data/shaping/NotoSansTC-MacaoLangSys-Synthetic.txt create mode 100644 tests/data/subset/NotoSansTC-MacaoLangSys-Synthetic.json create mode 100644 tests/data/ttf/NotoSansTC-MacaoLangSys-Synthetic.ttf diff --git a/src/Shaper/OtlTags.php b/src/Shaper/OtlTags.php index 1d86972aa..aaa4f48ea 100644 --- a/src/Shaper/OtlTags.php +++ b/src/Shaper/OtlTags.php @@ -77,7 +77,8 @@ public static function script(array $ScriptLang, $scripttag, $scriptblock, $shap * @param string $ietf The language of the text as an IETF tag, e.g. 'sr-Cyrl' or 'zh-Hant-HK' * @param string $available The language systems the script offers, as one string of tags * - * @return string The tag, 'DFLT' where the script offers that and not the language, or '' for neither + * @return string The first of the language's tags the script offers, 'DFLT' where it offers none of + * them and offers that, or '' for neither */ public static function language($ietf, $available) { @@ -85,35 +86,67 @@ public static function language($ietf, $available) return ''; } - $tags = $ietf ? explode('-', $ietf) : []; + $subtags = $ietf ? explode('-', strtolower($ietf)) : []; - $lang = isset($tags[0]) ? strtolower($tags[0]) : ''; - - // The region, where the second subtag is one; a third subtag always is, the second being - // the script - $country = ''; - if (isset($tags[1]) && strlen($tags[1]) == 2) { - $country = strtolower($tags[1]); + if (isset($subtags[0]) && $subtags[0] == 'zh') { + $candidates = self::chinese($subtags); + } elseif (isset($subtags[0]) && isset(Ucdn::$ot_languages[$subtags[0]])) { + $candidates = [Ucdn::$ot_languages[$subtags[0]]]; + } else { + $candidates = []; } - if (isset($tags[2]) && $tags[2]) { - $country = strtolower($tags[2]); + + foreach ($candidates as $langsys) { + if (strpos($available, $langsys) !== false) { + return $langsys; + } } - $region = $lang . '-' . $country; + return strpos($available, 'DFLT') !== false ? 'DFLT' : ''; + } - if ($lang != '' && isset(Ucdn::$ot_languages[$lang])) { - $langsys = Ucdn::$ot_languages[$lang]; - } elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$region])) { - $langsys = Ucdn::$ot_languages[$region]; - } else { - $langsys = 'DFLT'; + /** + * The language systems for Chinese, in the order HarfBuzz tries them + * (hb_ot_tags_from_complex_language() in hb-ot-tag-table.hh). + * + * The script subtag outranks the region, so zh-Hans-HK is Simplified, except that Traditional + * Chinese in Hong Kong or Macao keeps its region's tag. Macao has its own ZHTM, and where a font + * lacks it takes Hong Kong's ZHH before the default. A tag with neither is Simplified. + * + * The zh- keys of Ucdn::$ot_languages are not read: they hold one tag per region, and nothing for + * the script or for zh alone. + * + * @param string[] $subtags The lower-cased subtags of a tag whose language is zh + * + * @return string[] + */ + private static function chinese(array $subtags) + { + $regions = [ + 'hk' => ['ZHH '], + 'mo' => ['ZHTM', 'ZHH '], + 'tw' => ['ZHT '], + ]; + + $script = isset($subtags[1]) ? $subtags[1] : ''; + + if ($script == 'hant' && isset($subtags[2]) && ($subtags[2] == 'hk' || $subtags[2] == 'mo')) { + return $regions[$subtags[2]]; + } + if ($script == 'hans') { + return ['ZHS ']; + } + if ($script == 'hant') { + return ['ZHT ']; } - if (strpos($available, $langsys) !== false) { - return $langsys; + foreach ($regions as $region => $langsys) { + if (in_array($region, $subtags, true)) { + return $langsys; + } } - return strpos($available, 'DFLT') !== false ? 'DFLT' : ''; + return ['ZHS ']; } /** diff --git a/tests/Mpdf/ChineseLangSysTest.php b/tests/Mpdf/ChineseLangSysTest.php new file mode 100644 index 000000000..f78f1352b --- /dev/null +++ b/tests/Mpdf/ChineseLangSysTest.php @@ -0,0 +1,74 @@ +assertSame([$expected], $this->drawn('NotoSansTC-RegionLangSys-Synthetic.ttf', $lang)); + } + + public function languages() + { + return [ + 'zh-HK' => ['zh-HK', self::ZHH], + 'zh-TW' => ['zh-TW', self::ZHT], + 'zh-CN' => ['zh-CN', self::ZHS], + 'zh' => ['zh', self::ZHS], + 'zh-Hans' => ['zh-Hans', self::ZHS], + 'zh-Hant' => ['zh-Hant', self::ZHT], + 'zh-Hans-HK' => ['zh-Hans-HK', self::ZHS], + 'zh-Hant-CN' => ['zh-Hant-CN', self::ZHT], + 'zh-MO, where the font has no ZHTM' => ['zh-MO', self::ZHH], + ]; + } + + public function testMacaoSelectsZHTMWhereTheFontOffersIt() + { + $this->assertSame([self::ZHTM], $this->drawn('NotoSansTC-MacaoLangSys-Synthetic.ttf', 'zh-MO')); + } + + private function drawn($font, $lang) + { + $key = strtolower(str_replace('-', '', basename($font, '.ttf'))); + + $mpdf = new TextRecordingMpdf([ + 'mode' => 'utf-8', + 'fontDir' => [__DIR__ . '/../data/ttf'], + 'fontdata' => [$key => [ + 'R' => $font, + 'useOTL' => 0xFF, + ]], + 'default_font' => $key, + ]); + $mpdf->WriteHTML(sprintf('

&#x%04X;

', $lang, self::BONE)); + + return array_values(unpack('N*', mb_convert_encoding($mpdf->drawnText[0], 'UTF-32BE', 'UTF-8'))); + } + +} diff --git a/tests/Mpdf/ChineseRegionLangSysTest.php b/tests/Mpdf/ChineseRegionLangSysTest.php deleted file mode 100644 index be97538a8..000000000 --- a/tests/Mpdf/ChineseRegionLangSysTest.php +++ /dev/null @@ -1,55 +0,0 @@ - ['zh-HK', self::ZHH], - 'zh-TW' => ['zh-TW', self::ZHT], - 'zh-CN' => ['zh-CN', self::ZHS], - ]; - } - - /** - * @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 a421cb2fd..fbdc26eb2 100644 --- a/tests/Mpdf/Shaper/OtlTagsTest.php +++ b/tests/Mpdf/Shaper/OtlTagsTest.php @@ -124,36 +124,44 @@ public function languages() } /** - * @dataProvider chineseRegions + * @dataProvider chinese */ - public function testAChineseRegionSelectsItsLanguageSystem($ietf, $expected) + public function testChineseSelectsItsLanguageSystemTheWayHarfBuzzDoes($ietf, $expected) { - $this->assertSame($expected, OtlTags::language($ietf, 'DFLT ZHH ZHS ZHT ')); + $this->assertSame($expected, OtlTags::language($ietf, 'DFLT ZHH ZHS ZHT ZHTM ')); } - public function chineseRegions() + public function chinese() { return [ 'Hong Kong' => ['zh-HK', 'ZHH '], - 'Hong Kong, after the script' => ['zh-Hant-HK', 'ZHH '], 'Taiwan' => ['zh-TW', 'ZHT '], - 'Macao' => ['zh-MO', 'ZHT '], + 'Macao' => ['zh-MO', 'ZHTM'], 'China' => ['zh-CN', 'ZHS '], 'Singapore' => ['zh-SG', 'ZHS '], 'lower case' => ['zh-tw', 'ZHT '], - 'a region the table has no entry for' => ['zh-US', 'DFLT'], + 'a region with no language system of its own' => ['zh-US', 'ZHS '], + 'no script or region' => ['zh', 'ZHS '], + 'Simplified' => ['zh-Hans', 'ZHS '], + 'Traditional' => ['zh-Hant', 'ZHT '], + 'upper case' => ['ZH-HANT', 'ZHT '], + 'Simplified, in Hong Kong' => ['zh-Hans-HK', 'ZHS '], + 'Simplified, in Taiwan' => ['zh-Hans-TW', 'ZHS '], + 'Simplified, in Macao' => ['zh-Hans-MO', 'ZHS '], + 'Traditional, in China' => ['zh-Hant-CN', 'ZHT '], + 'Traditional, in Taiwan' => ['zh-Hant-TW', 'ZHT '], + 'Traditional, in Hong Kong' => ['zh-Hant-HK', 'ZHH '], + 'Traditional, in Macao' => ['zh-Hant-MO', 'ZHTM'], + 'another script, in Hong Kong' => ['zh-Latn-HK', 'ZHH '], + 'Min Nan, a retired tag' => ['zh-min-nan', 'ZHS '], ]; } - /** - * 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() + public function testMacaoFallsBackToHongKong() { - $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 ')); + $this->assertSame('ZHH ', OtlTags::language('zh-MO', 'DFLT ZHH ZHS ZHT ')); + $this->assertSame('ZHH ', OtlTags::language('zh-Hant-MO', 'DFLT ZHH ZHS ZHT ')); + $this->assertSame('DFLT', OtlTags::language('zh-MO', 'DFLT ZHS ZHT ')); } } diff --git a/tests/data/fontcache/NotoSansTC-MacaoLangSys-Synthetic.json b/tests/data/fontcache/NotoSansTC-MacaoLangSys-Synthetic.json new file mode 100644 index 000000000..58c35d481 --- /dev/null +++ b/tests/data/fontcache/NotoSansTC-MacaoLangSys-Synthetic.json @@ -0,0 +1,111 @@ +{ + "_": "Generated by composer fontcache:update. See tests/Mpdf/Fonts/ParserGoldenMaster.php.", + "fullName": "NotoSansTC-MacaoLangSys-Synthetic", + "mtx": { + "GSUBScriptLang": { + "hani": "DFLT ZHH ZHS ZHT ZHTM " + }, + "GSUBFeatures": { + "hani": { + "ZHH ": { + "locl": [ + 0 + ] + }, + "ZHS ": { + "locl": [ + 1 + ] + }, + "ZHT ": { + "locl": [ + 2 + ] + }, + "ZHTM": { + "locl": [ + 3 + ] + } + } + }, + "GSUBLookups": [ + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 152 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 166 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 180 + ], + "MarkFilteringSet": "" + }, + { + "Type": 1, + "Flag": 0, + "SubtableCount": 1, + "Subtables": [ + 194 + ], + "MarkFilteringSet": "" + } + ], + "GPOSScriptLang": [], + "GPOSFeatures": [], + "GPOSLookups": [], + "MarkGlyphSets": [], + "rtlPUAstr": "", + "haskernGPOS": false, + "hassmallcapsGSUB": false + }, + "cache": { + "GDEFdata.json": { + "GlyphClassBases": " 04E00| 04E09| 04E8C| 056DB| 09AA8", + "GlyphClassMarks": "", + "GlyphClassLigatures": "", + "GlyphClassComponents": "", + "MarkGlyphSets": [], + "MarkAttachmentType": [] + }, + "GSUB.dat": "206 bytes, sha256 15a34b758887cf89b199b406d9176b260da726c485910751dbf71edbb7515e8a", + "GSUBdata.json": [ + [ + { + "39592": 0 + } + ], + [ + { + "39592": 0 + } + ], + [ + { + "39592": 0 + } + ], + [ + { + "39592": 0 + } + ] + ] + } +} diff --git a/tests/data/otldump/NotoSansTC-MacaoLangSys-Synthetic.txt b/tests/data/otldump/NotoSansTC-MacaoLangSys-Synthetic.txt new file mode 100644 index 000000000..828995639 --- /dev/null +++ b/tests/data/otldump/NotoSansTC-MacaoLangSys-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
ZHTM: 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, ZHTM
+ +
GPOS table not defined
diff --git a/tests/data/shaping/NotoSansTC-MacaoLangSys-Synthetic.txt b/tests/data/shaping/NotoSansTC-MacaoLangSys-Synthetic.txt new file mode 100644 index 000000000..7c53de204 --- /dev/null +++ b/tests/data/shaping/NotoSansTC-MacaoLangSys-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 56DB 9AA8 => 0020 4E00 4E09 4E8C 56DB 9AA8 group=SCCCCC +0x80 0020 4E00 4E09 4E8C 56DB 9AA8 => 0020 4E00 4E09 4E8C 56DB 9AA8 group=SCCCCC +0xFF +kern -liga 0020 4E00 4E09 4E8C 56DB 9AA8 => 0020 4E00 4E09 4E8C 56DB 9AA8 group=SCCCCC diff --git a/tests/data/subset/NotoSansTC-MacaoLangSys-Synthetic.json b/tests/data/subset/NotoSansTC-MacaoLangSys-Synthetic.json new file mode 100644 index 000000000..95c105180 --- /dev/null +++ b/tests/data/subset/NotoSansTC-MacaoLangSys-Synthetic.json @@ -0,0 +1,123 @@ +{ + "_": "Generated by composer subset:update. See tests/Mpdf/Fonts/SubsetGoldenMaster.php.", + "useOTL=0x00": { + "characters": 7, + "makeSubset": { + "bytes": 1828, + "sha256": "1a08d48939adc69ce292ff650076355c5052f120c731933af2ee7639286467ef", + "tables": { + "OS/2": "96 bytes, checksum 0x553CF586, sha256 4feb4a7666c967ad3bedc80ca0480d43b41a41ffe07c31028a6ef52218d38859", + "cmap": "108 bytes, checksum 0x3758A5CD, sha256 27e5bdb67154aea061c1a0b3e62f0b894a919ea72a9df76f6a575a3398b6c21f", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "616 bytes, checksum 0xF2BD808C, sha256 eb64d774f26f4e9a473759bbcdc30a0a8b61b20877fdfe44a02c0bd1355b96c6", + "head": "54 bytes, checksum 0x26853F95, sha256 1acb9bbfa64aefe9fb625673eb5b941acd732a30a7a9cdbe48b0867d476876aa", + "hhea": "36 bytes, checksum 0x087602F8, sha256 d4af68535beeb99ce192587063523bb1862795cbb69021e6a6f6837509a41164", + "hmtx": "28 bytes, checksum 0x185001B1, sha256 dcdc38d773361123ed46e2ed00be835f86453b8de0b415d657f8894a0356a4ae", + "loca": "16 bytes, checksum 0x013C0204, sha256 16b586cebec1f59e81f7212f6264e445b9d03d7549e66ab0595a3c8f8b8b5a01", + "maxp": "32 bytes, checksum 0x00100051, sha256 a90cbb1baa544bddcc9065d5a64a0fb38a90285012ee709966a26b270ec7f551", + "name": "612 bytes, checksum 0x31BF4960, sha256 23c77f4598d07365d682aa25460b20f76e80c4c511e019e04b5390477f8ebb4b", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUni": 65535, + "defaultWidth": 1000, + "codeToGlyph": "7 entries, sha256 33437c8ffd013e5cb18588ae09402766e4327e02b063f3258c7c8414254c0e19" + }, + "makeSubsetSIP": { + "bytes": 1804, + "sha256": "7e85dff50a5defaf99bfafaa39871f02e383726483048542d2d524839c9ad202", + "tables": { + "OS/2": "96 bytes, checksum 0x5910D1E5, sha256 9b6aabb1544d4c39e4e3ce746b7822eaef6c698888d89808bf3fe66f6a62bf7f", + "cmap": "84 bytes, checksum 0x00380090, sha256 b744253609e2a88d21149d2993bd74c60e813a46a40dbdd5fa6a10f27bd1defe", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "616 bytes, checksum 0xF2BD808C, sha256 eb64d774f26f4e9a473759bbcdc30a0a8b61b20877fdfe44a02c0bd1355b96c6", + "head": "54 bytes, checksum 0x26853F95, sha256 d3c8bad2f1b5344ce391c0bdbbaf20112ab08c2549862b0f290b099c920716cf", + "hhea": "36 bytes, checksum 0x087602F8, sha256 d4af68535beeb99ce192587063523bb1862795cbb69021e6a6f6837509a41164", + "hmtx": "28 bytes, checksum 0x185001B1, sha256 dcdc38d773361123ed46e2ed00be835f86453b8de0b415d657f8894a0356a4ae", + "loca": "16 bytes, checksum 0x013C0204, sha256 16b586cebec1f59e81f7212f6264e445b9d03d7549e66ab0595a3c8f8b8b5a01", + "maxp": "32 bytes, checksum 0x00100051, sha256 a90cbb1baa544bddcc9065d5a64a0fb38a90285012ee709966a26b270ec7f551", + "name": "612 bytes, checksum 0x31B84960, sha256 54f060d3e1c9cbea3eba51254e89baf8171d80dc6c5b4ffddcdbb053e7fc612b", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUniChar": 65535, + "defaultWidth": 1000 + }, + "repackageTTF": { + "bytes": 1800, + "sha256": "6691f7d9e584dad1f8b60832ec511ea8bdaf26b6e87fab8060151f58b9374998", + "tables": { + "OS/2": "96 bytes, checksum 0x553CF586, sha256 4feb4a7666c967ad3bedc80ca0480d43b41a41ffe07c31028a6ef52218d38859", + "cmap": "92 bytes, checksum 0xC9881364, sha256 733b6a16d046f7383c0562d37943c507574d58ad29e5f1215189dde306cb0f29", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "610 bytes, checksum 0x310A423E, sha256 07f8c133a7cd0ae670db6b0b91d1c28df3f22310dbe6371f44400eda2f635330", + "head": "54 bytes, checksum 0x26853F95, sha256 bfd07cb84f0abdc2d9b0567935b1128c4e0496677e5451dceb5c1bf7c35208c6", + "hhea": "36 bytes, checksum 0x087602F4, sha256 3b8df10855749dc2b94ca33deacd41aa368f250bd21bfe9561090e4dece3ed54", + "hmtx": "20 bytes, checksum 0x09490118, sha256 25146f57e5c57b537146373684d9d1a2321c5a9161766675cf932e2c89f231e0", + "loca": "16 bytes, checksum 0x013501FB, sha256 aae769be1e60c3529a0063171e29d79ca147197e2ed59c40dcbc5bbb949381ed", + "maxp": "32 bytes, checksum 0x00100051, sha256 a90cbb1baa544bddcc9065d5a64a0fb38a90285012ee709966a26b270ec7f551", + "name": "612 bytes, checksum 0x31BF4960, sha256 23c77f4598d07365d682aa25460b20f76e80c4c511e019e04b5390477f8ebb4b", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUni": 0 + } + }, + "useOTL=0xFF": { + "characters": 7, + "makeSubset": { + "bytes": 1828, + "sha256": "1a08d48939adc69ce292ff650076355c5052f120c731933af2ee7639286467ef", + "tables": { + "OS/2": "96 bytes, checksum 0x553CF586, sha256 4feb4a7666c967ad3bedc80ca0480d43b41a41ffe07c31028a6ef52218d38859", + "cmap": "108 bytes, checksum 0x3758A5CD, sha256 27e5bdb67154aea061c1a0b3e62f0b894a919ea72a9df76f6a575a3398b6c21f", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "616 bytes, checksum 0xF2BD808C, sha256 eb64d774f26f4e9a473759bbcdc30a0a8b61b20877fdfe44a02c0bd1355b96c6", + "head": "54 bytes, checksum 0x26853F95, sha256 1acb9bbfa64aefe9fb625673eb5b941acd732a30a7a9cdbe48b0867d476876aa", + "hhea": "36 bytes, checksum 0x087602F8, sha256 d4af68535beeb99ce192587063523bb1862795cbb69021e6a6f6837509a41164", + "hmtx": "28 bytes, checksum 0x185001B1, sha256 dcdc38d773361123ed46e2ed00be835f86453b8de0b415d657f8894a0356a4ae", + "loca": "16 bytes, checksum 0x013C0204, sha256 16b586cebec1f59e81f7212f6264e445b9d03d7549e66ab0595a3c8f8b8b5a01", + "maxp": "32 bytes, checksum 0x00100051, sha256 a90cbb1baa544bddcc9065d5a64a0fb38a90285012ee709966a26b270ec7f551", + "name": "612 bytes, checksum 0x31BF4960, sha256 23c77f4598d07365d682aa25460b20f76e80c4c511e019e04b5390477f8ebb4b", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUni": 65535, + "defaultWidth": 1000, + "codeToGlyph": "7 entries, sha256 33437c8ffd013e5cb18588ae09402766e4327e02b063f3258c7c8414254c0e19" + }, + "makeSubsetSIP": { + "bytes": 1804, + "sha256": "7e85dff50a5defaf99bfafaa39871f02e383726483048542d2d524839c9ad202", + "tables": { + "OS/2": "96 bytes, checksum 0x5910D1E5, sha256 9b6aabb1544d4c39e4e3ce746b7822eaef6c698888d89808bf3fe66f6a62bf7f", + "cmap": "84 bytes, checksum 0x00380090, sha256 b744253609e2a88d21149d2993bd74c60e813a46a40dbdd5fa6a10f27bd1defe", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "616 bytes, checksum 0xF2BD808C, sha256 eb64d774f26f4e9a473759bbcdc30a0a8b61b20877fdfe44a02c0bd1355b96c6", + "head": "54 bytes, checksum 0x26853F95, sha256 d3c8bad2f1b5344ce391c0bdbbaf20112ab08c2549862b0f290b099c920716cf", + "hhea": "36 bytes, checksum 0x087602F8, sha256 d4af68535beeb99ce192587063523bb1862795cbb69021e6a6f6837509a41164", + "hmtx": "28 bytes, checksum 0x185001B1, sha256 dcdc38d773361123ed46e2ed00be835f86453b8de0b415d657f8894a0356a4ae", + "loca": "16 bytes, checksum 0x013C0204, sha256 16b586cebec1f59e81f7212f6264e445b9d03d7549e66ab0595a3c8f8b8b5a01", + "maxp": "32 bytes, checksum 0x00100051, sha256 a90cbb1baa544bddcc9065d5a64a0fb38a90285012ee709966a26b270ec7f551", + "name": "612 bytes, checksum 0x31B84960, sha256 54f060d3e1c9cbea3eba51254e89baf8171d80dc6c5b4ffddcdbb053e7fc612b", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUniChar": 65535, + "defaultWidth": 1000 + }, + "repackageTTF": { + "bytes": 1808, + "sha256": "2112a40561ecf5ba8871bae9f44ff1d365a448e36d2990f6abb05c613cf393c5", + "tables": { + "OS/2": "96 bytes, checksum 0x553CF586, sha256 4feb4a7666c967ad3bedc80ca0480d43b41a41ffe07c31028a6ef52218d38859", + "cmap": "100 bytes, checksum 0xC9881391, sha256 1d4376603e4499f42e96a03200d95c4a71efad8458f72b5f4a3879e07784b1bc", + "gasp": "8 bytes, checksum 0x00000010, sha256 4ca731f86ad506ac0e320283dc9461926346de3c4d91d539ea7f6620d3826940", + "glyf": "610 bytes, checksum 0x310A423E, sha256 07f8c133a7cd0ae670db6b0b91d1c28df3f22310dbe6371f44400eda2f635330", + "head": "54 bytes, checksum 0x26853F95, sha256 92de6dfe6f3b4c977d8d4cd3346333f92231c0ef7f08acdd9a2e12a3530ea8e7", + "hhea": "36 bytes, checksum 0x087602F4, sha256 3b8df10855749dc2b94ca33deacd41aa368f250bd21bfe9561090e4dece3ed54", + "hmtx": "20 bytes, checksum 0x09490118, sha256 25146f57e5c57b537146373684d9d1a2321c5a9161766675cf932e2c89f231e0", + "loca": "16 bytes, checksum 0x013501FB, sha256 aae769be1e60c3529a0063171e29d79ca147197e2ed59c40dcbc5bbb949381ed", + "maxp": "32 bytes, checksum 0x00100051, sha256 a90cbb1baa544bddcc9065d5a64a0fb38a90285012ee709966a26b270ec7f551", + "name": "612 bytes, checksum 0x31BF4960, sha256 23c77f4598d07365d682aa25460b20f76e80c4c511e019e04b5390477f8ebb4b", + "post": "32 bytes, checksum 0xFF860032, sha256 22dd499f08932214bde96943f3882f3619324478217095c049b68a57c6c10693" + }, + "maxUni": 0 + } + } +} diff --git a/tests/data/ttf/NotoSansTC-MacaoLangSys-Synthetic.ttf b/tests/data/ttf/NotoSansTC-MacaoLangSys-Synthetic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..d84eaa7ccc76b65925bf98db361ce7aeb2154482 GIT binary patch literal 2064 zcmb7FTWph69RHtl%Gb-bc4b{VV1sWf>k5pqw(H6^C~RZU$u5jBTo#>_bz3LfT4BzD zhLCW{Vj%GWq618fi3*7c;0vM;F8H7(3YeIfC?x0ukoY1AJRrUJJ73F9<-xD{-~Z=- zJC_n8q5?WVg0^hh(6*80lb%TLhSt8Rx36X3i(NlJUJv<7cke21-Q!@zsEx(cy5!b$Hzg*scTWqfi*8`8}++ zL0%f2m`WJiSqbFNA=}1dLt*A;53yF(Q#cV$#1Xv*cElrfGCVQt`Ko;fk?u5nBJtRs zsmu}bB0tQelI)E$<)KCWQgvoV%n4byLR@O`VV!O! zeUk|O)#Mg)S|rVsLqgN&X$u)Njm-4yrxU-hcW538c$@dbv^h)JAbD9Ew0Vz`!N#&d zvH_Ymv~<&YQcs={@yv{@gsIX;TKN)w14JbSl||@guEJ(&pnmF>4gdkL0m~IO(oqQ1 zj5;|wRJ*9l*mZRKuP4uBGSELGXMX#PU-;`n;_UL_!E8QwJo#483R+FQG)fnl2c1PQ zxkH$oKwKl`lZEI-Yq^!Na>lG#6cK7V&Vs2!Y!EoY-%4Ljf5?IfaVJ^A%h+r>m>w10 zza(?2z+lAe*?rFKMf^h|>QUznb!F6N%&uztoeaZJ1Jj=0|ai z-_>F-q=1ZXb)p?-0RBZjv(VS@s-Ot?tTwx^QTV*|0lVFyD9+`=S>bZ|`-Q(V73pyE zKT^dv+nL3^rBzor->{~wBJbx-CX2F2U8LkYO`LP1)2dgUWs1{f=692K__dV#L_7av z{Op0ja+AGSDeZafy}tCt5}V+br3HdJYOAn&-I;S@R+OPW3ssPd{IrhR<(^|oPCuXs zwMse<6)Ji~sdBlhymqgzzTVH!LY1%BrYKqy-0ukTfWyr7fxu$!fUXGZik;o!g;1)s z?I`zbPVWq{d!a(RqqC8l^<{M~L!L2R$weyV3>8#X>KzWFv)tIzbn4SSUH1nkg51`0 zFtN<$P8@7tmA|+0g>(D&v#--PZr=*0#}+R!7z8VD7BFTov3J+5ecwAhU8^s&>Q;=5 zR3A4jP%UdMYH9veAD_R#+-P1@%+A;C(+-}V2C#EXXM}`1p5qzr_~{(aakj7Ic!7I! zJ;&>)g_Y*`dGspoT{eya{kP<%AzFre;deiY2XMn8{s(#t4dvnFrj6r!i|d2 z>+l+aTtzjo#o!ZzS1-IK5ktiZZ3XUxJOqeQhh`at$6nZFrcuNghjuqQsXe|6Ng3>rqZ_64OyJcUf}{ zs0VLM6J`^pU5FwlF#?(ZWaIeZRgcqO4}aft`gz`*R%-K-I}k;@DbzQFQ=xsCgQ)Vq zK1K{LrA2!W^K%Xyl6*}WWCAoI76hp zx)RHESqe^*@pfxekPiQR Date: Thu, 17 Sep 2026 09:32:33 +1000 Subject: [PATCH 6/6] Hold the Chinese regions once, and read a Traditional tag's region in one place (#201) Co-Authored-By: Claude Opus 5 (1M context) --- src/Shaper/OtlTags.php | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Shaper/OtlTags.php b/src/Shaper/OtlTags.php index aaa4f48ea..07c8c57e4 100644 --- a/src/Shaper/OtlTags.php +++ b/src/Shaper/OtlTags.php @@ -30,6 +30,15 @@ class OtlTags 'mym2' => 'mymr', ]; + /** + * The Chinese regions with language systems of their own, in the order HarfBuzz looks for them. + */ + private static $chineseRegions = [ + 'hk' => ['ZHH '], + 'mo' => ['ZHTM', 'ZHH '], + 'tw' => ['ZHT '], + ]; + /** * The script tag a run is laid out with, out of what the table offers. * @@ -88,12 +97,11 @@ public static function language($ietf, $available) $subtags = $ietf ? explode('-', strtolower($ietf)) : []; + $candidates = []; if (isset($subtags[0]) && $subtags[0] == 'zh') { $candidates = self::chinese($subtags); } elseif (isset($subtags[0]) && isset(Ucdn::$ot_languages[$subtags[0]])) { $candidates = [Ucdn::$ot_languages[$subtags[0]]]; - } else { - $candidates = []; } foreach ($candidates as $langsys) { @@ -122,25 +130,18 @@ public static function language($ietf, $available) */ private static function chinese(array $subtags) { - $regions = [ - 'hk' => ['ZHH '], - 'mo' => ['ZHTM', 'ZHH '], - 'tw' => ['ZHT '], - ]; - $script = isset($subtags[1]) ? $subtags[1] : ''; - if ($script == 'hant' && isset($subtags[2]) && ($subtags[2] == 'hk' || $subtags[2] == 'mo')) { - return $regions[$subtags[2]]; - } if ($script == 'hans') { return ['ZHS ']; } if ($script == 'hant') { - return ['ZHT ']; + $region = isset($subtags[2]) ? $subtags[2] : ''; + + return $region == 'hk' || $region == 'mo' ? self::$chineseRegions[$region] : ['ZHT ']; } - foreach ($regions as $region => $langsys) { + foreach (self::$chineseRegions as $region => $langsys) { if (in_array($region, $subtags, true)) { return $langsys; }