Skip to content

Finish #81: OtlDump forks TTFontFile, the GSUB parse bypasses Table\*, and FontSubsetter writes the same ten tables twice #160

Description

@jakejackson1

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

if (($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) {   // TTFontFile.php:2996
if (($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) {   // Otl.php:4697
if (($flag & 0x0008) == 0x0008) {                            // OtlDump.php:1376

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.
  • 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 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

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions