You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Follow-up to #81, which split the parser from the subsetter, gave every GSUB and GPOS subtable
structure its own method, took Bidi, Shaper\Arabic and Shaper\LineBreaking out of Otl, and put Table\Coverage, Table\ClassDef and Table\SequenceRule in. Measured on gravitypdf at 0a4bc24,
PHP 8.5.4; tests/Mpdf/Fonts, tests/Mpdf/OtlTest.php and tests/Mpdf/TTFontFileTest.php are green
at 287 tests, 609 assertions, 2 skipped.
What that left is four golden masters — parser, dump, subset, shaping — pinning every font in tests/data/ttf, and Otl and OtlDump entirely off raw unpack(). Both GPOS paths now read their
rules through SequenceRule. The work below is what the same treatment has not reached, ordered by
what it costs to leave alone.
1. OtlDump forks TTFontFile rather than extending it
OtlDump extends TTFontFile and then redeclares 55 of its 60 properties, src/OtlDump.php:20-184.
Only ignoreStrings, maxUni, mode, glyphdata and glyphIDtoUn are its own. Every other one — tables, charWidths, glyphToChar, GSUBLookups, GSLuCoverage, ascent, bbox, the lot — names
a property the parent already declares.
The methods are forks too, each overriding a parent implementation it was copied from:
method
OtlDump
TTFontFile
extractInfo
:272, 412 lines
:546, 559 lines
_getGDEFtables
:689, 201 lines
:1114, 151 lines
_getGSUBarray
:955, 381 lines
:2306, 548 lines
_getGSUBignoreString
:1350, 56 lines
:2973, 63 lines
_makeGSUBcontextInputMatch
:1463, 22 lines
:3092, 22 lines
_makeGSUBinputMatch
:1495, 16 lines
:3124, 16 lines
_makeGSUBbacktrackMatch
:1524, 14 lines
:3153, 14 lines
_makeGSUBlookaheadMatch
:1548, 14 lines
:3177, 14 lines
extractInfo shares 304 stripped lines with the version it overrides. The four match builders share
every line but one each, and the one is whether the glyph is wrapped: the dump writes $str .= "" . $inputGlyphs[$i - 1] . "", the parser writes $str .= "(" . $inputGlyphs[$i - 1] . ")",
because one is read by a person and the other is a regex capture group. That is an argument, not a
method.
The seam this wants already exists on the GPOS side. wantsLookups, reportTableRead, reportTableMissing, reportScriptList, useGPOSlookups, multipleSubstitutes and alternateSubstitutes are three- to eight-line hooks in TTFontFile that OtlDump overrides properly,
and #144 and 400a51e carried the GSUB reporting through the same shape. The GSUB reading never
went through it.
Two copies have already drifted apart. The IgnoreMarks branch reads
Without the guard the branch overwrites the mark attachment class the branch above it set, so where a
lookup flag names an attachment class and sets IgnoreMarks the dump reports an ignore set the shaper
does not apply. Two of the three agree, so the dump is the odd one out. Latent for now: of 377 GSUB
lookups across the 29 fonts of tests/data/ttf that offer OTL tables, 19 name a mark attachment class
— in Carlito, Noto Sans, Noto Sans Coptic and both Takri subsets — and none of the 19 also sets
IgnoreMarks. Nothing in the corpus reaches the line, which is why it drifted.
2. The GSUB parse in TTFontFile bypasses Table\* entirely
Zero uses of SequenceRule::, against 16 in Otl and 15 in OtlDump. readGSUBsubtables
(src/TTFontFile.php:1443, 239 lines) and readGSUBrules (:1682, 344 lines) hand-roll the sequence,
chained-sequence and class-rule structures SequenceRule was extracted to own. It also keeps its own _getCoverage($convert2hex, $mode) (:3277) and _getClasses (:3310), which wrap Coverage::glyphs() and ClassDef::pairs() behind two projection flags, where Otl projects per
caller — charsOf, classSets, coverageSets, getCoverageUni.
This is what makes (1) hard to do directly: the two _getGSUBarray bodies are not comparable while one
reads rules through SequenceRule and the other reads them by hand.
3. Small table readers still exist in two copies each
The three classes in src/Fonts/Table/ were the first of these. Still pairwise duplicated:
read
first copy
second copy
_getValueRecord
Otl.php:4511
OtlDump.php:2958
_getAnchorTable
Otl.php:4561
OtlDump.php:3007
_getMarkRecord
Otl.php:4582
OtlDump.php:3029
_getClassDefinitionTable
Otl.php:4467
TTFontFile.php:1275
count_bits
Otl.php:4491
OtlDump.php:2816
marksOutsideFilteringSet
Otl.php:4603
TTFontFile.php:2926
marksOutsideAttachmentClass
Otl.php:4633
TTFontFile.php:2956
_getCoverage
Otl.php:5013
TTFontFile.php:3277
_getClasses
Otl.php:5130
TTFontFile.php:3310
Table\ValueRecord, Table\Anchor and Table\MarkArray are the obvious next three. The Flag and
MarkFilteringSet logic wants one class of its own: it exists in three dialects today — Otl::getGCOMignoreSet/buildGCOMignoreList/_checkGCOMignore, TTFontFile::_checkGSUBignore with markGlyphSet, and OtlDump::_getGSUBignoreString — and that is the code #142 and #154 are both open
against, currently fixable one copy at a time. Section 1's diverged guard is in it as well.
The last two are a pair of a different kind, and belong to sections 2 and 3 at once. The read is
already shared — both copies call Coverage::glyphs() and ClassDef::pairs() — so what is
duplicated is the projection and the caching around it, and the two deliberately disagree. Otl
keeps every result in LuDataCache (25 references, against 0 in TTFontFile) and projects through
its own private copy of unicode_hex(), the duplicate section 6 is about; TTFontFile projects
through the global one and hides three projections behind two flags, where (true, 1) gives hex
strings, (false, 1) raw glyph ids, and (false, 2) a [unicode => index] map that only :1397
and :3594 ask for. TTFontFile::_getClasses already says as much in its own docblock: it keeps
class 0 where Otl drops it, and it drops glyphs no character reaches where Otl tests for them at
match time.
So this one is not an extraction like the three above it — both projections are wanted. What wants
naming is the projection itself, per caller, the way Otl already names charsOf, classSets, coverageSets and getCoverageUni. TTFontFile::_getCoverage and _getClasses are public, so the
flags cannot be retired without deciding what becomes of the signature.
4. FontSubsetter writes the same ten tables twice
makeSubset (src/Fonts/FontSubsetter.php:192, 447 lines) and makeSubsetSIP (:639, 488 lines)
each build name, OS/2, post, hhea, maxp, cmap, hmtx, glyf, loca and head — the same
ten, in two parallel bodies, differing in the cmap format and which glyphs are collected. repackageTTF (:1127, 147 lines) is a third partial copy. The shared prologue was already pulled out
into open() (:119), under a docblock that says the three builders "opened the file and reset the
same state in the same twenty lines each"; the bodies were left.
It is also the last font class doing its own binary work — 9 unpack() calls plus raw substr(),
against 0 in Otl and OtlDump and 3 in TTFontFile — and it reads the parser's internals through
ten separate members: seek_table 13 times, tables 7, get_table_pos 5, then maxStrLenRead, getCMAP4, getHMTX, selectFont, readTableDirectory, open and filename.
This is where the open bugs are. #150, #151, #156 and #152 are all the same cmap and usage arithmetic
repeated across the copies, and #158 is patching one of them.
5. Otl is 5,447 lines, and three of its tenants are not shaping
By responsibility:
methods
lines
shapers and dispatch
24
1,773
GPOS apply, per format
16
1,028
GSUB apply, per format
13
748
rule loops and context matching
10
687
low-level table reads
17
429
OTLdata text utilities
6
207
script and language tag tables
2
197
ignore and mark filtering
5
167
debug dump
1
44
constructor
1
27
The three in bold do not belong to a shaper. splitOTLdata, sliceOTLdata, prependOTLchar, removeChar, replaceSpace and trimOTLdata (:4770-4977) are buffer surgery called from Mpdf,
and want to be a value object. _getOTLscriptTag (:5207, 135 lines — the file's fifth largest
method) and _getOTLLangTag (:5342) are lookup tables. _dumpproc (:5404) is debug reporting that
belongs with OtlDump.
GSUBsubstitute (:2701) is the largest method in the file at 311 lines.
The four rule loops — _applyGSUBrules, _applyGSUBrulesSingly, _applyGSUBrulesMyanmar, _applyGSUBrulesIndic — are 46% to 78% similar pairwise, and the GSUB and GPOS context-apply pairs sit
between 46% and 65%. Real duplication, and the riskiest here to fold together; last.
6. Dead members, and a function outside its class
OtlDump::$glyphIDtoUn (src/OtlDump.php:172) is declared and read nowhere in src/, tests/ or utils/. A truncated twin of $glyphIDtoUni, which the same block also redeclares.
unicode_hex() (src/TTFontFile.php:32) is a global function sitting outside the class in a
namespaced file, called 11 times from TTFontFile and 4 from OtlDump, while Otl carries its own
private copy at :4988.
TTFontFileAnalysis::extractCoreInfo (src/Fonts/TTFontFileAnalysis.php:23) re-copies the parent's
open, version-check and TTC-detect prologue instead of sharing it.
Order
The 55 redundant properties in OtlDump, and the dead members and stray function of section 6.
Mechanical, and Strict will say if a property was load-bearing.
TTFontFile's GSUB subtable and rule reads onto SequenceRule, Coverage and ClassDef (2).
Table\ValueRecord, Table\Anchor, Table\MarkArray, and one class for the Flag and
MarkFilteringSet ignore logic (3), which settles the diverged guard in section 1.
Fold OtlDump onto the reporting hooks: the four match builders first, then _getGSUBignoreString, _getGDEFtables, extractInfo, _getGSUBarray (1).
The four rule loops, if the golden masters make it safe enough to be worth it (5).
Each step is covered by the golden masters, which is what #81 built them for: any of these that changes
what the parser hands the shaper, what the dump reports, what the subsetter emits or what the shaper
makes of a run of text will show up as a fixture diff.
Follow-up to #81, which split the parser from the subsetter, gave every GSUB and GPOS subtable
structure its own method, took
Bidi,Shaper\ArabicandShaper\LineBreakingout ofOtl, and putTable\Coverage,Table\ClassDefandTable\SequenceRulein. Measured ongravitypdfat0a4bc24,PHP 8.5.4;
tests/Mpdf/Fonts,tests/Mpdf/OtlTest.phpandtests/Mpdf/TTFontFileTest.phpare greenat 287 tests, 609 assertions, 2 skipped.
What that left is four golden masters — parser, dump, subset, shaping — pinning every font in
tests/data/ttf, andOtlandOtlDumpentirely off rawunpack(). Both GPOS paths now read theirrules through
SequenceRule. The work below is what the same treatment has not reached, ordered bywhat it costs to leave alone.
1.
OtlDumpforksTTFontFilerather than extending itOtlDump extends TTFontFileand then redeclares 55 of its 60 properties,src/OtlDump.php:20-184.Only
ignoreStrings,maxUni,mode,glyphdataandglyphIDtoUnare its own. Every other one —tables,charWidths,glyphToChar,GSUBLookups,GSLuCoverage,ascent,bbox, the lot — namesa property the parent already declares.
The methods are forks too, each overriding a parent implementation it was copied from:
OtlDumpTTFontFileextractInfo:272, 412 lines:546, 559 lines_getGDEFtables:689, 201 lines:1114, 151 lines_getGSUBarray:955, 381 lines:2306, 548 lines_getGSUBignoreString:1350, 56 lines:2973, 63 lines_makeGSUBcontextInputMatch:1463, 22 lines:3092, 22 lines_makeGSUBinputMatch:1495, 16 lines:3124, 16 lines_makeGSUBbacktrackMatch:1524, 14 lines:3153, 14 lines_makeGSUBlookaheadMatch:1548, 14 lines:3177, 14 linesextractInfoshares 304 stripped lines with the version it overrides. The four match builders shareevery line but one each, and the one is whether the glyph is wrapped: the dump writes
$str .= "" . $inputGlyphs[$i - 1] . "", the parser writes$str .= "(" . $inputGlyphs[$i - 1] . ")",because one is read by a person and the other is a regex capture group. That is an argument, not a
method.
The seam this wants already exists on the GPOS side.
wantsLookups,reportTableRead,reportTableMissing,reportScriptList,useGPOSlookups,multipleSubstitutesandalternateSubstitutesare three- to eight-line hooks inTTFontFilethatOtlDumpoverrides properly,and #144 and
400a51ecarried the GSUB reporting through the same shape. The GSUB reading neverwent through it.
Two copies have already drifted apart. The IgnoreMarks branch reads
Without the guard the branch overwrites the mark attachment class the branch above it set, so where a
lookup flag names an attachment class and sets IgnoreMarks the dump reports an ignore set the shaper
does not apply. Two of the three agree, so the dump is the odd one out. Latent for now: of 377 GSUB
lookups across the 29 fonts of
tests/data/ttfthat offer OTL tables, 19 name a mark attachment class— in Carlito, Noto Sans, Noto Sans Coptic and both Takri subsets — and none of the 19 also sets
IgnoreMarks. Nothing in the corpus reaches the line, which is why it drifted.
2. The GSUB parse in
TTFontFilebypassesTable\*entirelyZero uses of
SequenceRule::, against 16 inOtland 15 inOtlDump.readGSUBsubtables(
src/TTFontFile.php:1443, 239 lines) andreadGSUBrules(:1682, 344 lines) hand-roll the sequence,chained-sequence and class-rule structures
SequenceRulewas extracted to own. It also keeps its own_getCoverage($convert2hex, $mode)(:3277) and_getClasses(:3310), which wrapCoverage::glyphs()andClassDef::pairs()behind two projection flags, whereOtlprojects percaller —
charsOf,classSets,coverageSets,getCoverageUni.This is what makes (1) hard to do directly: the two
_getGSUBarraybodies are not comparable while onereads rules through
SequenceRuleand the other reads them by hand.3. Small table readers still exist in two copies each
The three classes in
src/Fonts/Table/were the first of these. Still pairwise duplicated:_getValueRecordOtl.php:4511OtlDump.php:2958_getAnchorTableOtl.php:4561OtlDump.php:3007_getMarkRecordOtl.php:4582OtlDump.php:3029_getClassDefinitionTableOtl.php:4467TTFontFile.php:1275count_bitsOtl.php:4491OtlDump.php:2816marksOutsideFilteringSetOtl.php:4603TTFontFile.php:2926marksOutsideAttachmentClassOtl.php:4633TTFontFile.php:2956_getCoverageOtl.php:5013TTFontFile.php:3277_getClassesOtl.php:5130TTFontFile.php:3310Table\ValueRecord,Table\AnchorandTable\MarkArrayare the obvious next three. The Flag andMarkFilteringSet logic wants one class of its own: it exists in three dialects today —
Otl::getGCOMignoreSet/buildGCOMignoreList/_checkGCOMignore,TTFontFile::_checkGSUBignorewithmarkGlyphSet, andOtlDump::_getGSUBignoreString— and that is the code #142 and #154 are both openagainst, currently fixable one copy at a time. Section 1's diverged guard is in it as well.
The last two are a pair of a different kind, and belong to sections 2 and 3 at once. The read is
already shared — both copies call
Coverage::glyphs()andClassDef::pairs()— so what isduplicated is the projection and the caching around it, and the two deliberately disagree.
Otlkeeps every result in
LuDataCache(25 references, against 0 inTTFontFile) and projects throughits own private copy of
unicode_hex(), the duplicate section 6 is about;TTFontFileprojectsthrough the global one and hides three projections behind two flags, where
(true, 1)gives hexstrings,
(false, 1)raw glyph ids, and(false, 2)a[unicode => index]map that only:1397and
:3594ask for.TTFontFile::_getClassesalready says as much in its own docblock: it keepsclass 0 where
Otldrops it, and it drops glyphs no character reaches whereOtltests for them atmatch time.
So this one is not an extraction like the three above it — both projections are wanted. What wants
naming is the projection itself, per caller, the way
Otlalready namescharsOf,classSets,coverageSetsandgetCoverageUni.TTFontFile::_getCoverageand_getClassesarepublic, so theflags cannot be retired without deciding what becomes of the signature.
4.
FontSubsetterwrites the same ten tables twicemakeSubset(src/Fonts/FontSubsetter.php:192, 447 lines) andmakeSubsetSIP(:639, 488 lines)each build
name,OS/2,post,hhea,maxp,cmap,hmtx,glyf,locaandhead— the sameten, in two parallel bodies, differing in the cmap format and which glyphs are collected.
repackageTTF(:1127, 147 lines) is a third partial copy. The shared prologue was already pulled outinto
open()(:119), under a docblock that says the three builders "opened the file and reset thesame state in the same twenty lines each"; the bodies were left.
It is also the last font class doing its own binary work — 9
unpack()calls plus rawsubstr(),against 0 in
OtlandOtlDumpand 3 inTTFontFile— and it reads the parser's internals throughten separate members:
seek_table13 times,tables7,get_table_pos5, thenmaxStrLenRead,getCMAP4,getHMTX,selectFont,readTableDirectory,openandfilename.This is where the open bugs are. #150, #151, #156 and #152 are all the same cmap and usage arithmetic
repeated across the copies, and #158 is patching one of them.
5.
Otlis 5,447 lines, and three of its tenants are not shapingBy responsibility:
The three in bold do not belong to a shaper.
splitOTLdata,sliceOTLdata,prependOTLchar,removeChar,replaceSpaceandtrimOTLdata(:4770-4977) are buffer surgery called fromMpdf,and want to be a value object.
_getOTLscriptTag(:5207, 135 lines — the file's fifth largestmethod) and
_getOTLLangTag(:5342) are lookup tables._dumpproc(:5404) is debug reporting thatbelongs with
OtlDump.GSUBsubstitute(:2701) is the largest method in the file at 311 lines.The four rule loops —
_applyGSUBrules,_applyGSUBrulesSingly,_applyGSUBrulesMyanmar,_applyGSUBrulesIndic— are 46% to 78% similar pairwise, and the GSUB and GPOS context-apply pairs sitbetween 46% and 65%. Real duplication, and the riskiest here to fold together; last.
6. Dead members, and a function outside its class
OtlDump::$glyphIDtoUn(src/OtlDump.php:172) is declared and read nowhere insrc/,tests/orutils/. A truncated twin of$glyphIDtoUni, which the same block also redeclares.Otl::$current_fh(src/Otl.php:119) is assigned''in the constructor (:146) and never read.Same category as the pre-OTL Arabic state Six public Mpdf properties for Arabic shaping are read and written nowhere #88 removed.
unicode_hex()(src/TTFontFile.php:32) is a global function sitting outside the class in anamespaced file, called 11 times from
TTFontFileand 4 fromOtlDump, whileOtlcarries its ownprivate copy at
:4988.TTFontFileAnalysis::extractCoreInfo(src/Fonts/TTFontFileAnalysis.php:23) re-copies the parent'sopen, version-check and TTC-detect prologue instead of sharing it.
Order
OtlDump, and the dead members and stray function of section 6.Mechanical, and
Strictwill say if a property was load-bearing.TTFontFile's GSUB subtable and rule reads ontoSequenceRule,CoverageandClassDef(2).Table\ValueRecord,Table\Anchor,Table\MarkArray, and one class for the Flag andMarkFilteringSet ignore logic (3), which settles the diverged guard in section 1.
OtlDumponto the reporting hooks: the four match builders first, then_getGSUBignoreString,_getGDEFtables,extractInfo,_getGSUBarray(1).makeSubsetandmakeSubsetSIP, and the parser's internals behind aninterface rather than ten reach-ins (4). Independent of the rest, and closes repackageTTF() can build a format 4 cmap subtable larger than the uint16 its length field is written as #150, Count a subset cmap's glyphIdArray in bytes, the way the length field is measured #151, percentSubset is read against a usage figure no document moves: FontWriter counts the seeded ASCII range, not what was drawn #152
and makeSubset() and makeSubsetSIP() write a cmap glyphIdArray that idRangeOffset puts out of reach #156.
Otl's three non-shaping tenants (5).Each step is covered by the golden masters, which is what #81 built them for: any of these that changes
what the parser hands the shaper, what the dump reports, what the subsetter emits or what the shaper
makes of a run of text will show up as a fixture diff.