Repository navigation
Description
With useOTL on, a font counts as having characters it doesn't have. TTFontFile gives every glyph its cmap doesn't map a made-up codepoint, starting at U+E000, so the glyph has a slot in the widths table. _charDefined() can't tell those slots from real characters, so text holding that codepoint can be drawn with an unrelated glyph.
Steps to reproduce (gravitypdf-v6 09b67ea, the head of #847)
$mpdf = new \Mpdf\Mpdf(['default_font' => 'freeserif', 'useSubstitutions' => true]);
$mpdf->SetCompression(false);
$mpdf->WriteHTML('AB');FreeSerif's cmap has no Private Use characters at all. With backupSubsFont set to dejavusans:
FreeSerif's useOTL |
_charDefined(U+E000) |
Written | Result |
|---|---|---|---|
0xFF, the default |
true | <418042> |
one of FreeSerif's glyphs, stored in slot 0x80 |
0 |
false | <4100> then <42> |
glyph 0, the missing glyph |
I read this from the page stream and haven't rasterised it, so I can't say which glyph it is.
How far it reaches
I parsed the 71 .ttf and .otf files in ttfonts/ at useOTL 0xFF, and compared five of them with and without it:
| Font | Private Use characters in the cmap | Slots filled with OTL |
|---|---|---|
| Aegyptus | 71 | 6,181 |
| UnBatang | 2,048 | 3,542 |
| Jomolhari | 1,537 | 2,794 |
| FreeSerif | 0 | 2,450 |
| FreeSans | 0 | 1,649 |
Past the Private Use Area. When U+E000–U+F8FF is full, the codepoints continue from U+2CEB0 (TTFontFile.php:1181, Fonts/FontSubsetter.php:671). The comment there calls U+2CEB0–U+2F7FF unassigned, but CJK Extensions F and I are assigned there now. A font that gets that far would claim real ideographs, and FontSubstitution::sipFontHas() would believe it. None of the 71 fonts does; Aegyptus comes closest, with 6,181 of the 6,400 slots filled. So this part is a risk for other fonts rather than something I've seen.
The answer also depends on where the widths come from
- A shipped ranges file is written without OTL, so it holds only real characters.
- A font's loaded widths, and its generated
<family>.ranges.jsonin the font cache, come from the metrics, so they include the made-up codepoints.
So the scan can answer differently for one font before and after it loads.
What a fix needs
- Separate the two questions. The made-up codepoints need width slots, because shaped glyphs look their widths up there, so they can't just be removed. The check for whether a font has an input character needs to skip them. That means recording which codepoints were assigned, and using that in the substitution scan and in
UTF8toSubset()and the other code that draws text. - Generated ranges files: once they hold real characters only,
rangesSource()can stop recordinguseOTL, and a cache format bump will replace the existing files. - U+2CEB0: decide whether the overflow stays there, and correct the comment. Moving it changes which glyphs get which slot, so it needs checking against the snapshots.
- Tests:
- U+E000 in FreeSerif at
useOTL0xFFis substituted, or drawn as the missing glyph, and not as one of its other glyphs. - Ligatures and alternates reached through GSUB still draw with the right widths.
- A generated test font with more than 6,400 unmapped glyphs, so no third-party licence is needed, doesn't claim Extension F.
- A captioned snapshot, rasterised and checked, that fails before the change.
- U+E000 in FreeSerif at
Related
- Move a Plane 2 character into Sun-ExtB only when Sun-ExtB has it (#837) #838 and Move a Plane 2 character into Sun-ExtB only when Sun-ExtB has it (v6) #847 added
sipFontHas(), which trusts the widths this way. They didn't cause the problem. - A font's Plane 3 and higher characters can't be drawn, because the font parser drops them #846 proposes reading more of the cmap, which adds more real characters above these codepoints.
Activity