Skip to content

Select ZHH, ZHS and ZHT for a Chinese region in the lang attribute (#191) - #202

Merged
jakejackson1 merged 2 commits into
fix/192-indic-script-fallbackfrom
fix/191-chinese-region-langsys
Sep 17, 2026
Merged

jakejackson1 merged 2 commits into
fix/192-indic-script-fallbackfrom
fix/191-chinese-region-langsys

Conversation

@jakejackson1

@jakejackson1 jakejackson1 commented Sep 16, 2026

Copy link
Copy Markdown
Member

Closes #191

What

OtlTags::language() falls back from the language subtag to the language and region. It joined them with nothing in between, so it looked up zhhk, but the only region keys in Ucdn::$ot_languages are written with a hyphen (zh-cn, zh-hk, zh-mo, zh-sg, zh-tw, at src/Ucdn.php:778-782). No other key is four or more letters long, so the branch could never match. Every Chinese region laid text out with the script's DFLT entry.

-		} 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];
+		$region = $lang . '-' . $country;
+
 		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];

The fix belongs in the lookup key, not in Ucdn. The hyphenated keys are the table's own convention, generated like the rest of it.

How lang is split

language() splits the tag on - and lower-cases each part:

  • the first subtag is the language
  • the second is the region only if it is two letters long, so a four-letter script subtag like Hant is skipped
  • a third subtag is always the region

So zh-HK, zh-hk and zh-Hant-HK all look up zh-hk. zh-Hant alone has no region and gets no language system. A language with its own key (e.g. sr) still wins over its region, as before.

Test font and shaping test

I checked every font in tests/data/ttf and packages/*/fonts for a language system starting ZH in GSUB or GPOS: none has one.

NotoSansTC-RegionLangSys-Synthetic.ttf makes the fix visible. It's Noto Sans TC 2.004 (OFL 1.1), subset in fontTools 4.59.2 to space, 骨, 一, 二 and 三. GPOS, BASE, STAT and the vertical metrics are dropped, and name IDs 1, 4 and 6 are renamed. It also gets a GDEF marking the four Han glyphs as bases, because mPDF refuses OpenType layout to a font without GDEF. Its GSUB is replaced by one script, hani, with no default features and one locl lookup per language system:

language system 骨 U+9AA8 is drawn as
ZHH 三 U+4E09
ZHS 一 U+4E00
ZHT 二 U+4E8C

ChineseRegionLangSysTest writes <p lang="…">骨</p> and records the glyph drawn through TextRecordingMpdf. It runs zh-HK, zh-TW and zh-CN, one per language system. I measured every row below the same way:

lang before after hb-shape --language 14.3.1
zh-HK 三 (ZHH) ZHH ✓
zh-Hant-HK 三 (ZHH) ZHH ✓
zh-TW 二 (ZHT) ZHT ✓
zh-tw 二 (ZHT) ZHT ✓
zh-MO 二 (ZHT) ZHH (see below)
zh-CN 一 (ZHS) ZHS ✓
zh-SG 一 (ZHS) ZHS ✓

OtlTagsTest has a case for every row of the issue's table, plus zh-Hant-HK, lower case, and a region the table has no entry for (zh-USDFLT).

Where HarfBuzz goes further: #201

HarfBuzz's hb_ot_tags_from_complex_language() (src/hb-ot-tag-table.hh) also maps the script subtag, and it checks the script before the region:

    if (lang_matches (&lang_str[1], limit, "zh-hans", 7))
      tags[0] = HB_TAG('Z','H','S',' ');  /* Chinese, Simplified */
    ...
    if (lang_matches (&lang_str[1], limit, "zh-hant", 7))
      tags[0] = HB_TAG('Z','H','T',' ');  /* Chinese, Traditional */

It also maps bare zhZHS, and Macao → ZHTM then ZHH. The issue asks for the region mapping, and mPDF has no equivalent for the rest, so I opened #201 instead of widening this PR. It covers zh, zh-Hant, zh-Hans, a script that disagrees with its region (zh-Hans-HK, zh-Hant-CN), and Macao. The zh-MO row above follows the issue and Ucdn's own 'zh-mo' => 'ZHT ' entry. OtlTagsTest::testChineseWithoutARegionHasNoLanguageSystem pins zh, zh-Hant and zh-Hans as they stand, with a reference to #201.

Fixtures

Regenerated from cold. I deleted tests/Mpdf/tmp/mpdf, tmp/mpdf and tmp/ttfontdata, then ran composer fontcache:update all, otldump:update all, shaping:update all and subset:update all. git status showed only the new font's five files. No existing fixture moved, which is expected because none of the golden masters sets a language. The new font's otldump lists hani with ZHH, ZHS and ZHT, each with locl. Its shaping master shapes with no language, so 骨 comes back unchanged.

Mutation check

With the key put back to $lang . $country and the caches cleared, 10 tests fail:

  • all 3 data sets of ChineseRegionLangSysTest::testTheRegionSelectsTheLanguageSystem
  • 7 of the 8 data sets of OtlTagsTest::testAChineseRegionSelectsItsLanguageSystem (all except zh-US)

Mirroring upstream

Upstream Otl::_getOTLLangTag() has the same key at src/Otl.php:6183 on development:

} elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$lang . '' . $country])) {
	$langsys = Ucdn::$ot_languages[$lang . '' . $country];

The mirror changes $lang . '' . $country to $lang . '-' . $country in both places. Upstream splits the tag the same way (preg_split('/-/', …), region from a two-letter second subtag or any third subtag), so nothing else changes. The upstream PR also needs a CHANGELOG.md entry. The test font, ChineseRegionLangSysTest, and the language rows (as calls to _getOTLLangTag through reflection) can be carried over.

Base

Stacked on #200 (fix/192-indic-script-fallback) → #198#190#188#186gravitypdf. Before pushing I checked all five: all still open, none merged, so this targets fix/192-indic-script-fallback without a rebase.

/simplify

Applied:

  • the region key is built once, into $region
  • ChineseRegionLangSysTest now runs one region per language system (zh-HK, zh-TW, zh-CN), because OtlTagsTest already covers every row of the table without building an Mpdf for each

Skipped:

  • dropping the $lang != '' guard on the region branch. It's redundant, but it was there before this change and it matches the branch above it.
  • a shared drawn-codepoints helper on TextRecordingMpdf. That would mean editing about a dozen tests outside this diff.

Verification

🤖 Generated with Claude Code

jakejackson1 and others added 2 commits September 17, 2026 11:40
)

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) <noreply@anthropic.com>
)

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) <noreply@anthropic.com>
@jakejackson1
jakejackson1 force-pushed the fix/192-indic-script-fallback branch from a17f1fa to 47eabba Compare September 17, 2026 01:47
@jakejackson1
jakejackson1 force-pushed the fix/191-chinese-region-langsys branch from b38b6fd to 4c81cc3 Compare September 17, 2026 01:47
@jakejackson1

Copy link
Copy Markdown
Member Author

Rebased: #190 was squash-merged into gravitypdf (f9b3c0e), so this stack still carried its pre-squash commits. This branch is rebased onto its rebased parent with --onto; no conflicts and the base is unchanged. Suite, snapshots and cs pass on the new head, with no fixture movement.

@jakejackson1
jakejackson1 merged commit c7ca805 into fix/192-indic-script-fallback Sep 17, 2026
27 checks passed
@jakejackson1

Copy link
Copy Markdown
Member Author

This PR merged into a base branch that had already been squash-merged, so its change never reached gravitypdf. It's carried to gravitypdf by #218.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working create-upstream-pr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant