Choose an Indic run's shaper on the script chosen for it, as HarfBuzz does (#199) - #214
Conversation
… does (#199) Otl::applyOTL() sent a run whose script has a shaper of its own to that shaper under any GSUB script, DFLT and latn included, and skipped a run the font offered no script for entirely. HarfBuzz decides the other way round in hb_ot_shaper_categorize(): under DFLT, or latn picked as the last fallback, the default shaper lays the run out with the font's features alone, and with no script chosen the script's shaper still runs. shaperForScriptTag() now makes that choice once the tags are known, per shaper as HarfBuzz 14.3.1 has it: - Indic, Sinhala and the South East Asian scripts (Indic and USE in HarfBuzz), and N'Ko and Mandaic: the default shaper under DFLT or latn - Myanmar: also under mymr, which replaces the mym2-only check - Syriac: the default shaper under DFLT only - Arabic, Khmer, Thai and Lao: their own shaper whatever was chosen A run with no script reaches its shaper as long as the document opened the script to OpenType layout (useOTL 0x80, which covers every script with a shaper). It is shaped with no lookups: loadGsubData() reads no GSUB table for it, which the font need not have, and the GSUB rule walks read the font's features for a script and language through gsubFeatures(), which has none to give. Measured with Noto Sans Bengali subsets whose GSUB has one 'locl' lookup, KA + I and KA + VIRAMA + SSA + I: DFLT and deva 09BF 0997 / 0997 09CD 09BF 09B7 -> 0997 09BF / 0997 09CD 09B7 09BF guru only 0995 09BF / 0995 09CD 09B7 09BF -> 09BF 0995 / 0995 09CD 09BF 09B7 which is what hb-shape draws. 288 lines of the shaping golden master move over 31 fonts, all of them now what hb-shape draws on the same font and text, 18 of them before as well: those 18 are an Arabic run gaining a kashida point on its tatweel in fonts with no Arabic script, and draw the same. The rest are the Devanagari, Bengali and Gurmukhi runs, reordered in the 8 fonts with no script for them and no longer in the fonts laying them out under DFLT or latn, and the Khmer run reordered in the 8 fonts with no Khmer script. The South East Asian shaper is now reached only under a Tai Tham tag, and Lanna Alif offers latn, so SeaTest and the Tai Tham snapshot, which pin a cluster reaching its reorderer through a 'ccmp' ligature, move to NotoSansTaiTham-LanaScript-Synthetic. The snapshot's broken cluster loses the dotted circle Lanna Alif drew; the new font has none. Test fonts: NotoSansBengali-GuruScript-Synthetic (Noto Sans Bengali 3.011, OFL 1.1) and NotoSansTaiTham-LanaScript-Synthetic (Noto Sans Tai Tham 2.002, OFL 1.1, script retagged lana, its Sakot ligatures moved from liga to ccmp), built in fontTools 4.59.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… GSUB and GPOS features one way (#199) loadGsubData() took a flag only to return between its two halves: the derived tables every shaper reads, and the table, coverage and lookup list only a run with features needs. They are now two methods, and applyGSUB() calls the second where there are features. The GPOS feature read beside the GSUB one goes through the same accessor, and shaperForScriptTag() takes the shaper it narrows as an argument rather than reading it back off the object. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… DFLT script (#199) #175's synthetic font reached gravitypdf after this branch was written. Its GSUB offers DFLT and no Bengali or Gurmukhi script, so those two runs now take the default shaper and are left in logical order. hb-shape 14.3.1 does the same: it returns the run as two grapheme clusters, 0 0 2 2, where the Indic shaper would have made one syllable of it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b38b6fd to
4c81cc3
Compare
26c367c to
ae5339f
Compare
|
Rebased: #190 was squash-merged into |
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
6a0a9fe
into
fix/191-chinese-region-langsys
|
This PR merged into a base branch that had already been squash-merged, so its change never reached |
Closes #199
What changes
Otl::applyOTL()picked a run's shaper from its Unicode script alone. It then sent the run to that shaper under any GSUB script,DFLTandlatnincluded. When the font offered no script for the run, it skipped the run entirely: no reordering, no GSUB, no GPOS. HarfBuzz does the reverse:hb_ot_shaper_categorize()(src/hb-ot-shaper.hh, 14.3.1) decides on the GSUB script it chose. UnderDFLT, orlatnpicked as the last fallback, the run gets the default shaper. With no script (HB_TAG_NONE), the script's own shaper still runs.shaperForScriptTag()makes that choice once the tags are known, shaper by shaper, and replaces the Myanmar "only undermym2" check.continue, as long as its script has a shaper and the document opened it with useOTL0x80. Every script with a shaper sits behind that bit, so a document that closed the script still gets nothing (testARunInAScriptTheDocumentHasNotOpenedToOpenTypeLayoutIsNotShaped).applyGSUB()goes on when there are no features but there is a shaper.loadGsubDerivedData()still setsGSUBfont. That key has no cached derived data, so the reorderers get emptyrphf/pref/blwf/pstf/rtlSUB/finals.loadGsubLookups(), which reads the GSUB table, coverage and lookup list, is only called where there are features; a font with no GSUB table has none (NotoSans-GPOS3-Syntheticin the shaping master is one).applyOTL()itself, read features throughfeatures(), which returns none for an empty tag (the GPOS read beside it uses it too).removeJoinControls(true), the word-boundary passes andapplyGPOS()(gated on its own features) need no change.Per shaper, as HarfBuzz 14.3.1 has it
DFLTlatnDFLTDFLT,latn,mymrmym2, skipped with noneIn each case mPDF now does what the HarfBuzz column says, using its own shaper where HarfBuzz has USE. Scripts mPDF has no shaper for are unchanged. mPDF never picks an
*3tag, so HarfBuzz's USE route forindy3tags never comes up.Original specification versus v2. HarfBuzz sets
is_old_specfrom the chosen tag not ending in2(hb-ot-shaper-indic.cc:324), so a run with no script is laid out to the original specification. With no lookups, mPDF's two reorderings draw the same thing. I shaped all 14,762 Bengali sequences of up to five of KA, SSA, RA, YA, VIRAMA, ZWJ, ZWNJ, I and NUKTA (starting with KA or RA) through theguru-only font with and without forcing the original specification, and nothing differed. So the dispatch keepsOtlTags' flag rather than adding a line no test could catch.Measured
KA + I and KA + VIRAMA + SSA + I, drawn text via
TextRecordingMpdf:hb-shape14.3.1DFLT,deva(NotoSansBengali-DevaScript-Synthetic)09BF 0997/0997 09CD 09BF 09B70997 09BF/0997 09CD 09B7 09BFuni0997 uni09BF/uni0997 uni09CD uni09B7 uni09BFguruonly (NotoSansBengali-GuruScript-Synthetic, new)0995 09BF/0995 09CD 09B7 09BF09BF 0995/0995 09CD 09BF 09B7uni09BF uni0995/uni0995 uni09CD uni09BF uni09B7ShaperChoiceTestpins both rows, the useOTL gate, and a Tai Tham run under Lanna Alif'slatn(default shaper, no dotted circle;hb-shapedrawsgid144= U+F001).Fixture diff
Golden masters were regenerated cold: I deleted
tests/Mpdf/tmp/mpdf,tmp/mpdfandtmp/ttfontdata, then ranfontcache,otldump,shapingandsubset:update all. Only the shaping master moved, plus the four fixtures for each of the two new fonts. A script read every moved line and shaped the same text in the same font withhb-shape. It split runs by script asanalyseCharacters()does, used--direction=ltr --cluster-level=2so glyph order maps back to codepoints (.notdeftaken from its cluster, other glyphs from mPDF'sglyphIDtoUni), added--features=kern,-ligafor that setting, and left scripts that0x80keeps closed unshaped.hb-shapeafterhb-shapebeforehb-shape0995 09CD 09B7 09BF→0995 09CD 09BF 09B7DFLT/latn, 1 more (DevaScript) with itslocl0995 09CD 09BF 09B7→0995 09CD 09B7 09BF1780 17D2 1781 17C1→17C1 1780 17D2 1781NotoSansBengali-DevaScript-Synthetic(DFLT)… 09BF 09B7 …→… 09B7 09BF …gpos={"1":{"kashida":8}}The 18 lines that matched before are the Arabic run. It now reaches the Arabic shaper, which has no forms to substitute, and it marks the tatweel as a kashida point, as it already did in every font that offers any script for Arabic. Glyphs are unchanged.
NotoSansGurmukhiUI-GPOS71-Subsetis one of the "reordered" fonts: its Devanagari and Bengali runs come back reordered, which is what the issue expects.Snapshots
tai-tham-sakotis the only snapshot that moves; the other 78 are unchanged. The South East Asian shaper is now reached only under a Tai Tham script tag. Lanna Alif offerslatnand Noto Sans Tai Tham offersDFLTandlatn, so neither reaches it, and with Lanna Alif the snapshot andSeaTestwould no longer reach the #104 path they pin (a cluster reaching the reorderer through accmpligature). Both move toNotoSansTaiTham-LanaScript-Synthetic: a subset of Noto Sans Tai Tham 2.002 (OFL 1.1) with its script retaggedlana, its Sakot ligatures moved fromligatoccmp, the rule swapping the ligature for a second form after a High Ka taken out (see #212), and a GDEF added because mPDF refuses useOTL without one.hb-shapedraws every sample in the snapshot and everySeaTestexpectation the same way.Ebranches of Three unguarded reads warn under E_ALL when loading Noto Sans Arabic, Bengali and Devanagari, and when shaping Tai Tham #104's category copy inGSUBsubstitute()removed,SeaTestfails again withUndefined array key "sea_category".Mutation check
DFLT/latnkeep the script's shaper (and Myanmar only undermym2)ShaperChoiceTestdefault-script Indic and Tai Tham tests, and 24 shaping mastersShaperChoiceTestno-script tests, and 8 shaping masters0x80gate droppedtestARunInAScriptTheDocumentHasNotOpenedToOpenTypeLayoutIsNotShapedloadGsubLookups()called with no featuresNotoSans-GPOS3-Synthetic(no GSUB table) errorsfeatures()indexes the features directlyVerification
vendor/bin/phpunit: 2103 tests, 5993 assertions, 1 skipped (base 2084 / 5955 / 1).--group=snapshot: 79 tests, 109 assertions (one assertion left the Tai Tham test with the dotted circle).composer cs: clean.phpstan analyse --memory-limit=2G: 33, none in files touched.git merge-tree --write-tree --name-only fix/199-indic-shaper-choice origin/fix/193-glyph-class-membership(Test a glyph's membership of a GDEF class by the glyph, not by its hex turning up in the class (#193) #205, headd9451ed): clean, no conflicts. The source diff is confined toapplyOTL()'s dispatch,applyGSUB()and the GSUB loading it calls (split in two), two new private methods, and the one-line feature read in each GSUB rule walk. Re-run after the/simplifycommit: still clean.gravitypdf. This PR's base, Select ZHH, ZHS and ZHT for a Chinese region in the lang attribute (#191) #202 (fix/191-chinese-region-langsys), is still open atb38b6fd, which this branch sits on, so it was neither rebased nor retargeted.Test fonts are built in fontTools 4.59.2:
NotoSansBengali-GuruScript-Synthetic(Noto Sans Bengali 3.011, OFL 1.1, as #200'sDevaScriptfont with a singleguruscript whoseloclgives KA the glyph of KHA) andNotoSansTaiTham-LanaScript-Syntheticabove.Found on the way and not fixed here: #212,
_applyGSUBrulesSingly()applies every feature it is given once per feature it is given.Upstream
mpdf/mpdfdevelopment(744f75a) has the same dispatch inline insrc/Otl.php::233-268selects the shaper from the script block.:311-321skips the subchunk when all four tags are empty. Mirror: keep the skip only where the shaper is empty or!($useOTL & 0x80), after replacing:323-326(themym2check) with the per-shaper choice above, made once$GSUBscriptTagis known.:364gates all of GSUB on($useOTL & 0xFF) && $GSUBscriptTag && $GSUBlangsys && $GSUBFeatures. Mirror:($useOTL & 0xFF) && ($GSUBFeatures || $this->shaper), with theGSLuCoverage/GSUBLookupsload at:391-398done only when there are features.:1288,:1336,:1391and:1448read$this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys]unguarded. Mirror: anissetfalling back to[].🤖 Generated with Claude Code