Move Otl's three non-shaping tenants out: OtlData, OtlTags and the debugOTL trace (#160) - #198
Merged
Merged
Conversation
This was referenced Sep 16, 2026
jakejackson1
force-pushed
the
refactor/160-otldump-gsub-array
branch
2 times, most recently
from
September 16, 2026 23:42
052c539 to
e8a2bce
Compare
…OtlTags (#160) _getOTLscriptTag and _getOTLLangTag were lookup tables living in the shaper as private methods. They move to Shaper\OtlTags as script() and language(), with the same results: the Indic switch becomes a table of v2 tag to original tag, and the useOTL gate a method returning the group's bit. script() drops the $mode argument, which the old method never read. Checked against the old private methods before they were deleted: 107,016,000 script cases (every Ucdn script block, with its own tag and five others, four shapers, eight useOTL masks and 3,000 random offered-script sets) and 517,720 language cases (every Ucdn language, with region and script subtags, against every OpenType language tag), none different. Two behaviours the move keeps and does not fix: a v2 Indic tag falls through to the original tags of the scripts listed after it (#192), and the language-and-region lookup never matches because Ucdn keys the regions 'zh-hk' (#191). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…160) _dumpproc was debug reporting living in the shaper. It is reachable, though nothing in src/, tests/ or utils/ reaches it: Otl::$debugOTL is a public var, off by default, and $mpdf->otl is public, so a caller may still switch the trace on. It is not dead code, so it moves rather than goes. It becomes OtlDump::shapingStep(), returning the HTML its 28 call sites echo. Static, so that Otl does not construct a dump or depend on one: the class is loaded only once the trace is on, and the shaper hands over the run it is tracing. The five outputs the new tests expect were captured from _dumpproc before it was deleted, and the whole trace of a two-Beh run through the joining fixture is byte-identical before and after. Because the trace ends the run with exit, that last test runs it in a process of its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er (#160) splitOTLdata, sliceOTLdata, prependOTLchar, removeChar, replaceSpace and trimOTLdata were buffer surgery on OTLdata that Mpdf, Bidi and two tags called through Otl. They move to Shaper\OtlData as split, slice, prependChar, removeChar, nbspToSpace and trim, static and operating on the same arrays, and every caller in src/ goes there directly. They stay functions over arrays rather than becoming a value object. Runs are copied by assignment all through Mpdf - into $savedPreOTLdata, $objattr['OTLdata'], textbuffer[18] - and each copy is relied on to be independent. The textbuffer and OTLdata are also what Mpdf::getStateSnapshot() captures for a look-ahead, and StateSnapshot copies arrays by value but shares any object inside them, so a run trimmed during the look-ahead would stay trimmed after restoreStateSnapshot(). removeChar and nbspToSpace read Mpdf::$mb_enc through Otl's $mpdf; they now take the encoding. Bidi::prepare() needed its Otl only for removeChar, so it takes the encoding instead, and Mpdf no longer has to make sure an Otl exists before calling it. The six stay on Otl as deprecated one-line delegates: Otl is reachable as the public Mpdf::$otl, and upstream has all of them public but prependOTLchar. Checked against the old methods before they were replaced: 200,000 random runs and operations - empty, null and false runs, missing parts, out-of-order GPOSinfo, four-byte characters, both encodings - with identical results, by-reference changes, warnings and exceptions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hunk builder (#160) Cleanups from review of the three moves: - OtlTags::script() walks its table once from the text's tag, rather than slicing it from an array_search over its keys. Checked against the committed version over 53,508,000 cases, none different. - BidiTest::sort() builds its chunk through chunk(), and gets back the docblock the new prepare() test had been put under. - Drop a comment that restated nbspToSpace's name, a sentence from shapingStep's docblock that did not explain anything, and a claim in OtlDataTest that its runs are grouped as getBasicOTLdata() groups them, which leaves out marks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jakejackson1
force-pushed
the
refactor/160-otl-tenants
branch
from
September 17, 2026 01:46
37f1e92 to
a398165
Compare
jakejackson1
changed the base branch from
refactor/160-otldump-gsub-array
to
gravitypdf
September 17, 2026 01:47
Member
Author
|
Rebased: #190 was squash-merged into |
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.
Part of #160: the sixth Order checkbox, "
Otl's three non-shaping tenants (5)". This moves all three out ofOtl. Once it merges, the checkbox can be ticked.It's one PR with one commit per tenant, and each commit can be reviewed on its own. They share no code, and all three are pure refactors: no fixture moves.
Base.
refactor/160-otldump-gsub-array(#190). #177 and #178 were squash-merged intogravitypdf, and the stack below this PR was rebased onto it. This branch was then rebased with--ontofrom the old #190 head (d820375) to the new one (e5db3e5). #186, #188 and #190 are still open, so the base stays #190.1. Script and language tags →
Shaper\OtlTags_getOTLscriptTagand_getOTLLangTagwere private lookup tables. They're nowOtlTags::script()andOtlTags::language(), andOtl::selectScriptAndLanguage()is their only caller.switchis now a v2 tag → original tag table.useOTLgate is now a method that returns the group's bit.script()drops$mode, which the old method never read.Exact results. Before deleting the old private methods, I compared them with the new ones through reflection:
useOTLmasks × 3,000+ offered-script sets.Two existing defects are kept exactly and filed separately, not fixed here:
bng2in a font with onlydeva→deva).OtlTagsTestpins this, so a fix will change that test.Ucdn::$ot_languageskeys regions aszh-hkand the code looks upzhhk.ZHH,ZHSandZHTare unreachable.2. Debug trace →
OtlDump::shapingStep()_dumpprocisn't dead code.Otl::$debugOTLis a public var, off by default, and$mpdf->otlis public, so a caller can switch the trace on. Nothing insrc/,tests/orutils/does. The trace ends the run withexit.It's now a static on
OtlDumpthat returns the HTML, placed beside the otherreport*methods. The 28 call sitesecho OtlDump::shapingStep($this->OTLdata, …).Otlnever builds a dump.OtlDump(andTTFontFileunder it) only loads once the trace is on.Exact output. Five outputs were captured from
_dumpprocbefore it was deleted, and the four unit tests expect four of them. The whole trace of a two-Beh run is byte-identical before and after. Because of theexit, the end-to-end test (ShapingTraceTest::testTheShaperTracesTheRunFromBeginningToEnd) runs the shaper in a child process (tests/Mpdf/Fixtures/shaping-trace.php).3. OTLdata buffer operations →
Shaper\OtlDataOtlmethodsplitOTLdataOtlData::splitsliceOTLdataOtlData::sliceprependOTLcharOtlData::prependCharremoveCharOtlData::removeChar(…, $encoding)replaceSpaceOtlData::nbspToSpace(…, $encoding)trimOTLdataOtlData::trimAll callers in
src/now callOtlDatadirectly:Mpdf(28 calls),Bidi(9),Tag\BlockTagandTag\Br.Why static functions over the same arrays, not a value object. I checked
StateSnapshotfirst, and it settles the question:Mpdf($savedPreOTLdata,$objattr['OTLdata'],textbuffer[..][18]), and each copy is expected to be independent.textbufferandOTLdataare exactly whatMpdf::getStateSnapshot()captures for the table-of-contents look-ahead.StateSnapshotcopies arrays by value but shares any object inside them.restoreStateSnapshot().Mpdf. That's a different and riskier change than this checkbox.Encoding.
removeCharandreplaceSpacereadMpdf::$mb_encthroughOtl's$mpdf, so the static versions take the encoding as an argument.Bidi::prepare()took anOtlonly forremoveChar. It now takes the encoding instead:Bidiis fork-only, andMpdfis its only caller.Mpdfno longer creates anOtljust before calling it.ServiceFactoryalways sets$otl, so that guard never fired.BC. All six stay on
Otlas one-line@deprecateddelegates, the way #167 keptunicode_hex().Otlis reachable through the publicMpdf::$otl, and upstreammpdf/mpdfexposessplitOTLdata,sliceOTLdata,removeChar,replaceSpaceandtrimOTLdataas public.prependOTLcharis fork-only (#145) but was public here too.Exact results. Before the old methods were replaced, 200,000 random runs and operations were compared between old and new. The inputs covered empty,
nullandfalseruns, missing parts, out-of-orderGPOSinfo, four-byte characters, both encodings, and cut points at 0, at the end and past the end. Return values, by-reference changes, warnings and exceptions were all identical.Tests
tests/Mpdf/Shaper/OtlDataTest.php(25 tests, all on hand-built OTLdata arrays): cut at 0, at the end and between cut and restart; restart''and0;GPOSinfoordering; removal at either end, and positions counted in characters with a four-byte emoji ahead; a no-break space the run no longer holds as U+00A0; U+3000 trimming; one-sided trims; all spaces;nullandchar_data => null;slice/prependCharof[]. A group cut down to nothing is compared as a string, becausesubstr()past the end returnsfalseon PHP 5.6.tests/Mpdf/Shaper/OtlTagsTest.php(32 tests): eachuseOTLgroup including both CJK ends and Hangul; v2 vs original Indic tags, with and without a shaper; the An Indic run the font has no tag for is laid out with another Indic script's original tag #192 fall-through;DFLT→dflt→latn; language with region, script, script+region, upper case, unknown language, missingDFLT, empty tag, empty offer.tests/Mpdf/ShapingTraceTest.php(5 tests), described above.OtlTest: the six deprecated delegates return whatOtlDatareturns.BidiTest:Bidi::prepare()removes RLE/PDF from the text and its run, and sets their levels.Mutation check
47 mutants: 24 in
OtlData, 16 inOtlTags, 5 inshapingStep, 1 inOtl's trace wiring and 1 in a delegate. Each was run against the full suite, snapshots included.Caught by a unit test only:
split: restart0no longer read as none; first part cut at the restart; noksortof either part.slice:<→<=at the end; noksort.prependChar: no defaults for[];GPOSinfonot shifted.removeChar:GPOSinfoof the removed character kept;>→>=.nbspToSpace:uniset without checking it's 160.trim: U+3000 ignored at either end;>=→>for either end.OtlTags: every group bit and range end; original tags tried without a shaper;DFLT/dfltorder; language not lower-cased; theDFLT-only fallback made unconditional.shapingStep: all 5 mutants, plus theOtlwiring and the delegate mutants.Also caught by snapshots and golden masters:
split>=→>at the restartAutoFontSnapshotTestremoveCharcounting bytes / ignoring its encodingDictionaryLineBreakingSnapshotTest,AutoFontSnapshotTest,BidiTestnbspToSpacecounting bytesRtlSnapshotTest,IndexOfFiftyEntriesSnapshotTest,LongTableOfContentsSnapshotTesttrimleft/right flags swappedRtlSnapshotTest,AutoFontSnapshotTest,ZeroFontSizeSnapshotTesttrimwithout itsis_arrayguardIssue905Test,JustifySnapshotTest,RtlSnapshotTestuseOTLgate removedlatnfallback removedTaiThamSakotSnapshotTest,SeaTest, Carlito golden masterDFLTlanguage fallbackAutoFontSnapshotTestSurvivors, all equivalent or unreachable:
slicegroup read with a suppressed warning instead ofisset: the same''for[].language()without its empty-offer early return:strpos('', …)gives the same''.language(): the only branch that reads the region can't match (A Chinese region never selects its OpenType language system: ZHH, ZHS and ZHT are unreachable #191)./simplify follow-up
The last commit applies what four review agents found:
OtlTags::script()now walks the tag table once instead of callingarray_searchand thenarray_slice. Over 53,508,000 cases it gives the same result as the committed version.BidiTest::sort()now reuseschunk(), and its docblock is back in the right place.Suggestions not taken:
Otlwrapper around the 28 trace call sites. It's optional, and the call sites were already this shape.Tag::$otl.Tag\Columnsstill passes it on, and removing it changes the constructor signature.Verification (after the rebase onto
e5db3e5, re-run after the /simplify commit)WriteHtmltests pass when run on their own too (76 tests).tests/Mpdf/tmp/mpdf,tmp/mpdfandtmp/ttfontdatadeleted, thenfontcache:update,otldump:update,shaping:updateandsubset:updatefor all fonts.git status -- tests/datastays empty.composer csis clean.Gif/*andWriter/FontWriter.php. None are in touched files.Refs #160, #191, #192.
🤖 Generated with Claude Code