Measured on gravitypdf at 28aec23, over the whole google/fonts catalogue — 3,328 faces across 1,942 families, each rendered at useOTL 0 and 0xFF with warnings promoted to exceptions. 63 families shape correctly and then warn; 53 of them stopped warning between 4388dc7 and head, which is #104, #96, #97, #99 and #83 landing. Nine are left, at four reads, and all four are the same shape as #104: a key the font never set, which PHP 7 answered with a notice and null and PHP 8 answers with a warning and null. The behaviour was always "treat missing as absent"; only the log changed.
Every font named below reproduces from its upstream bytes, not from anything this pipeline produced.
1. A GSUB 1/1 delta lands on a glyph the cmap never mapped
TTFontFile.php:1816, in the LookupType 1 Format 1 branch:
$substitute[] = unicode_hex($this->glyphToChar[($glyphs[$g] + $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])][0]);
DeltaGlyphID is added to a glyph id and the sum is looked up in glyphToChar, which only holds glyphs the cmap reaches. Measured keys: Undefined array key 66822, and -7000 and -28279 where the delta wraps negative. The missing key gives null, [0] is then Trying to access array offset on null, and the null travels into unicode_hex() — so dechex(): Passing null to parameter #1 ($num) of type int is deprecated at TTFontFile.php:37 is the same bug one frame down, not a second one.
Five families, and four of them are among the ten largest in google/fonts:
| family |
upstream |
| Cactus Classical Serif |
ofl/cactusclassicalserif/CactusClassicalSerif-Regular.ttf |
| Noto Sans SignWriting |
ofl/notosanssignwriting/NotoSansSignWriting-Regular.ttf |
| Chiron Sung HK, Chiron Hei HK, Chiron GoRound TC |
variable upstream; statics from the family's fonts.google.com download |
2. A lookup flag names a mark attachment class the parser never built
$this->MarkAttachmentType[$flag >> 8], read in four places:
TTFontFile.php:2951 — strpos($this->MarkAttachmentType[($flag >> 8)], $glyph)
TTFontFile.php:3024 — $str = $this->MarkAttachmentType[$MarkAttachmentType];
Otl.php:4649 — the same strpos() call as 2951
Otl.php:4594 — the same assignment as 3024
Undefined array key 1, then the null reaches strpos(): Passing null to parameter #1 ($haystack) at 2951 and 4649, and Otl.php:4566 — explode('|', $this->buildGCOMignoreList(...)) — gets the null that 4594 returned.
Two families: Carlito (ofl/carlito/Carlito-Regular.ttf, all four faces, 33 warnings each) and NATS (ofl/nats/NATS-Regular.ttf, 6,853 warnings on one page).
3. Context class 0 is the one index without the guard
TTFontFile.php:2519, GSUB context format 2:
$inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass];
Class 0 is the implicit "every glyph not named by another class" and is not in InputClasses. Three lines further down the identical read for every index above 0 already handles exactly this, with the comment saying so:
} // if class[0] = all glyphs excluding those specified in all other classes
// set to blank '' for now
else {
$inputGlyphs[$gcl] = '';
}
Index 0 does not get that else. The null then reaches strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) at TTFontFile.php:2554.
One family: Molengo (ofl/molengo/Molengo-Regular.ttf), 32 warnings.
4. A feature with an empty lookup list indexes by null
TTFontFile.php:1437:
$lg[$ft['LookupListIndex'][0]] = $ft;
Undefined array key 0 when the feature's LookupListIndex is empty, so the key is null and Using null as an array offset is deprecated puts the row at $lg[''] — where ksort() then orders it ahead of every real lookup index.
One family: Sedan SC (ofl/sedansc/SedanSC-Regular.ttf), 20 warnings.
Reproducing
$mpdf = new \Mpdf\Mpdf([
'mode' => 'UTF-8',
'fontDir' => [$dir],
'fontdata' => ['probe' => ['R' => 'Carlito-Regular.ttf', 'useOTL' => 0xFF]],
'default_font' => 'probe',
]);
$mpdf->WriteHTML('<div>' . $text . '</div>');
with an E_ALL error handler and text that reaches the font's own lookups. The sample text here is derived per font from its GSUB and GPOS coverage rather than written by hand, so it fires the rules that are actually there.
What is not measured
Whether guarding these four leaves the nine families rendering byte-identically. That is the evidence #104's three had before they were changed, and none of these has it yet — "it is the same class of bug" is a claim about a fix, not a finding about this one.
Two other bugs came out of the same run and are filed separately: a multi-glyph substitute reaching hexdec() in the Arabic shaper, and Output() costing ~140 s per font independent of the page.
Measured on
gravitypdfat28aec23, over the wholegoogle/fontscatalogue — 3,328 faces across 1,942 families, each rendered atuseOTL0 and 0xFF with warnings promoted to exceptions. 63 families shape correctly and then warn; 53 of them stopped warning between4388dc7and head, which is #104, #96, #97, #99 and #83 landing. Nine are left, at four reads, and all four are the same shape as #104: a key the font never set, which PHP 7 answered with a notice and null and PHP 8 answers with a warning and null. The behaviour was always "treat missing as absent"; only the log changed.Every font named below reproduces from its upstream bytes, not from anything this pipeline produced.
1. A GSUB 1/1 delta lands on a glyph the cmap never mapped
TTFontFile.php:1816, in the LookupType 1 Format 1 branch:DeltaGlyphIDis added to a glyph id and the sum is looked up inglyphToChar, which only holds glyphs the cmap reaches. Measured keys:Undefined array key 66822, and-7000and-28279where the delta wraps negative. The missing key gives null,[0]is thenTrying to access array offset on null, and the null travels intounicode_hex()— sodechex(): Passing null to parameter #1 ($num) of type int is deprecatedatTTFontFile.php:37is the same bug one frame down, not a second one.Five families, and four of them are among the ten largest in
google/fonts:ofl/cactusclassicalserif/CactusClassicalSerif-Regular.ttfofl/notosanssignwriting/NotoSansSignWriting-Regular.ttffonts.google.comdownload2. A lookup flag names a mark attachment class the parser never built
$this->MarkAttachmentType[$flag >> 8], read in four places:TTFontFile.php:2951—strpos($this->MarkAttachmentType[($flag >> 8)], $glyph)TTFontFile.php:3024—$str = $this->MarkAttachmentType[$MarkAttachmentType];Otl.php:4649— the samestrpos()call as 2951Otl.php:4594— the same assignment as 3024Undefined array key 1, then the null reachesstrpos(): Passing null to parameter #1 ($haystack)at 2951 and 4649, andOtl.php:4566—explode('|', $this->buildGCOMignoreList(...))— gets the null that 4594 returned.Two families: Carlito (
ofl/carlito/Carlito-Regular.ttf, all four faces, 33 warnings each) and NATS (ofl/nats/NATS-Regular.ttf, 6,853 warnings on one page).3. Context class 0 is the one index without the guard
TTFontFile.php:2519, GSUB context format 2:Class 0 is the implicit "every glyph not named by another class" and is not in
InputClasses. Three lines further down the identical read for every index above 0 already handles exactly this, with the comment saying so:Index 0 does not get that
else. The null then reachesstrpos($inputGlyphs[$seqIndex], $lookupGlyphs[0])atTTFontFile.php:2554.One family: Molengo (
ofl/molengo/Molengo-Regular.ttf), 32 warnings.4. A feature with an empty lookup list indexes by null
TTFontFile.php:1437:Undefined array key 0when the feature'sLookupListIndexis empty, so the key is null andUsing null as an array offset is deprecatedputs the row at$lg['']— whereksort()then orders it ahead of every real lookup index.One family: Sedan SC (
ofl/sedansc/SedanSC-Regular.ttf), 20 warnings.Reproducing
with an
E_ALLerror handler and text that reaches the font's own lookups. The sample text here is derived per font from its GSUB and GPOS coverage rather than written by hand, so it fires the rules that are actually there.What is not measured
Whether guarding these four leaves the nine families rendering byte-identically. That is the evidence #104's three had before they were changed, and none of these has it yet — "it is the same class of bug" is a claim about a fix, not a finding about this one.
Two other bugs came out of the same run and are filed separately: a multi-glyph substitute reaching
hexdec()in the Arabic shaper, andOutput()costing ~140 s per font independent of the page.