Select ZHS or ZHT from a Chinese script subtag, ZHS for zh alone, and ZHTM then ZHH for Macao (#201) - #207
Merged
jakejackson1 merged 2 commits intoSep 17, 2026
Conversation
This was referenced Sep 16, 2026
… ZHTM then ZHH for Macao (#201) 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) <noreply@anthropic.com>
… one place (#201) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jakejackson1
force-pushed
the
fix/191-chinese-region-langsys
branch
from
September 17, 2026 01:47
b38b6fd to
4c81cc3
Compare
jakejackson1
force-pushed
the
fix/201-chinese-script-subtags
branch
from
September 17, 2026 01:47
035d522 to
b23641e
Compare
Member
Author
|
Rebased: #190 was squash-merged into |
jakejackson1
merged commit Sep 17, 2026
0b88657
into
fix/191-chinese-region-langsys
27 checks passed
Member
Author
|
This PR merged into a base branch that had already been squash-merged, so its change never reached |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #201
What
OtlTags::language()read only the language subtag, then the language and region together. No key mapped a script subtag or barezh, so:zh,zh-Hansandzh-Hantgot the script'sDFLTentryzh-Hans-HKgotZHH,zh-Hant-CNgotZHSZHTChinese now follows HarfBuzz 14.3.1.
What HarfBuzz does
I read
hb_ot_tags_from_complex_language()(src/hb-ot-tag-table.hh,case 'z') andhb_ot_tags_from_language()(src/hb-ot-tag.cc). HarfBuzz lower-cases the tag and tries these rules in order, and the first match wins:zh-hant-hkprefixZHHzh-hant-moprefixZHTM,ZHHzh-min-nanexactlyZHSzh-hansprefixZHSzh-hantprefixZHTzh-minexactlyZHSzh-and a-hksubtag anywhereZHHzh-and a-mosubtag anywhereZHTM,ZHHzh-and a-twsubtag anywhereZHTzh-yuezhin the two-letter table (sozh-CN,zh-SG,zh-US,zh-Latn,zh-hakka, …)ZHSOtlTags::chinese()implements rules 1-9 and 11. Rules 3 and 6 need no code, because they give the sameZHSas rule 11. Rule 10 is general to every language, and mPDF has no key foryue,lzhor the other Chinese languages, so it's in #206 along with the non-zhcomplex mappings.zh-yue→ZHShere, where HarfBuzz takesZHH.Several candidates, and no change to
Otl.phplanguage()still returns one tag. It now tries each candidate against the script's language systems in order, then falls back toDFLT, then'', as before. Macao is['ZHTM', 'ZHH '], and every other language is a single candidate.Otl.phpis untouched.The generic region lookup (
Ucdn::$ot_languages[$lang . '-' . $country]) is gone. Only thezh-keys ever used it, andzh-mo => 'ZHT 'is one of the mappings this changes. Those fiveUcdnentries are no longer read. I leftUcdn.phpalone to keep the change toOtlTags.php. A comment onchinese()says why they aren't read.Behaviour change: bare
zh→ZHSAny document with
lang="zh",zh-CN,zh-SGor an unrecognised region such aszh-USnow selectsZHSwhere the font offers it. Before,zhandzh-USfell toDFLT. This is what HarfBuzz does and what the issue asks for. I checked the blast radius:tests/data/ttforpackages/*/fonts(85 files) has aZH*language system, apart from the two synthetic test fonts.lang="zh…".Test font and tests
NotoSansTC-MacaoLangSys-Synthetic.ttfis #191'sNotoSansTC-RegionLangSys-Syntheticrebuilt with one more glyph and language system. It's built from the same script in fontTools 4.59.2 (Noto Sans TC 2.004, OFL 1.1). #191's font isn't touched, so its fixtures don't move. In each font, 骨 U+9AA8 is drawn as:ZHHZHSZHTZHTMtests/Mpdf/ChineseRegionLangSysTest.phpis renamed toChineseLangSysTest.php. It draws<p lang="…">骨</p>throughTextRecordingMpdf:zh-HK,zh-TW,zh-CN(A Chinese region never selects its OpenType language system: ZHH, ZHS and ZHT are unreachable #191's rows), pluszh,zh-Hans,zh-Hant,zh-Hans-HK,zh-Hant-CN, andzh-MO, which falls back toZHHzh-MO→ZHTMOtlTagsTest: #191's pinned "zh,zh-Hantandzh-Hans→DFLT" test and its region provider become one provider with 20 rows. They cover the issue's table, every HarfBuzz rule above, upper and lower case, andzh-US→ZHS(wasDFLT).testMacaoFallsBackToHongKongcoverszh-MOandzh-Hant-MOwith noZHTMoffered (→ZHH), and with neitherZHTMnorZHH(→DFLT).Comparison with
hb-shape --language14.3.1Glyph drawn for 骨. mPDF is
TextRecordingMpdfand HarfBuzz ishb-shape, both on the same font file:langzhzh-Hanszh-HantZH-HANTzh-Hans-HKzh-Hans-TWzh-Hans-MOzh-Hant-CNzh-Hant-HKzh-Hant-MOzh-MOzh-HKzh-TWzh-CNzh-SGzh-USzh-Latn-HKzh-min-nanzh-hakkazh-yueFixtures
Regenerated from cold. I deleted
tests/Mpdf/tmp/mpdf,tmp/mpdfandtmp/ttfontdata, then rancomposer fontcache:update all,otldump:update all,shaping:update allandsubset:update all.git statusshowed only the new font's five files. No existing fixture moved.otldumplistshaniwithZHH,ZHS,ZHTandZHTM, each withlocl.fontcachehas"hani": "DFLT ZHH ZHS ZHT ZHTM ".Mutation check
Each mutation was applied alone to
OtlTags.phpagainst the full suite:hk/mo/twloop above thehans/hantchecks)ChineseLangSysTestzh-Hans-HK, andOtlTagsTestzh-Hans-HK,zh-Hans-TW,zh-Hans-MOzh(chinese()endsreturn [])ChineseLangSysTestzh,zh-CN, andOtlTagsTestzh,zh-CN,zh-SG,zh-US,zh-min-nanZHHfallback ('mo' => ['ZHTM'])ChineseLangSysTest"zh-MO, where the font has no ZHTM", andOtlTagsTest::testMacaoFallsBackToHongKongMirroring upstream
Upstream's version is
Otl::_getOTLLangTag()(src/Otl.php:6150ondevelopment). It still has the$lang . '' . $countrykey that #202 fixed. The mirror of this PR replaces that function's body:$countryextraction and the region branch gozhfirst subtag gets the candidate list from a new private method with the same body asOtlTags::chinese()[Ucdn::$ot_languages[$lang]]DFLT/''fallbackThe
$lang . ''bug disappears with the region branch, so the upstream PR can carry #191 and #201 together. Both callers (Otl.php:284,:300) are unchanged. The test fonts,ChineseLangSysTestand theOtlTagsTestrows (through reflection on_getOTLLangTag) can be carried over. The upstream PR also needs aCHANGELOG.mdentry.Base
Stacked on #202 (
fix/191-chinese-region-langsys) → #200 → #198 → #190 → #188 → #186 →gravitypdf. Before pushing I checked all six: all open, none merged, andfix/191-chinese-region-langsysis still atb38b6fd. So this targets it without a rebase./simplify
Four review passes: reuse, simplification, efficiency and altitude.
Applied:
private static $chineseRegionsbeside$originalIndicTags, so it isn't rebuilt on every call.hantbranches are folded into one.$candidatesis initialised once, so the emptyelseis gone.Skipped:
$ietf ?guard.language()is called withnull(anOtlTagsTestrow), andstrtolower(null)is deprecated from PHP 8.1.zhbranch. With one entry it's premature; A variant, script or region subtag never selects the language system HarfBuzz picks outside zh, e.g. el-polyton, ro-MD, yue #206 can add it when there's a second.chinese()reads the script by position and the region anywhere on purpose: that is HarfBuzz'slang_matchesprefix test against itssubtag_matchessearch (zh-Latn-HK→ZHH).Ucdn's unreadzh-keys, which is outsideOtlTags.php. I noted it on A variant, script or region subtag never selects the language system HarfBuzz picks outside zh, e.g. el-polyton, ro-MD, yue #206.The mutation check above was re-run after these changes, with the same results. The verification numbers are also from after the changes.
Verification
vendor/bin/phpunit: 2110 tests, 5991 assertions, 1 skipped (base is 2084 / 5955 / 1)vendor/bin/phpunit --group=snapshot: OK, 79 tests, 110 assertionscomposer cs: clean🤖 Generated with Claude Code