Skip to content

fix(pdf): stop embedded fonts rendering as tofu and wrong glyphs - #660

Merged
andiwand merged 4 commits into
mainfrom
fix/pdf-font-tofu
Aug 7, 2026
Merged

fix(pdf): stop embedded fonts rendering as tofu and wrong glyphs#660
andiwand merged 4 commits into
mainfrom
fix/pdf-font-tofu

Conversation

@andiwand

@andiwand andiwand commented Aug 7, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

odr-private/pdf/VZA_Ankauf_Kunde.pdf translated to a page of tofu. Three
independent defects, all in the font embedding path.

1. CFF font names the sanitizer rejects

A CFF Name INDEX entry holding a space or a PostScript delimiter makes
the browser's OpenType Sanitizer discard the whole table — the @font-face
never loads and every glyph in it renders as tofu. PDF producers emit such
names routinely; this file carries *Microsoft Sans Serif-3111, which cost
4 of its 7 embedded fonts.

Fixed by patching the offending bytes in place. Same length, so the absolute
offsets the Top DICT carries stay valid and the CFF is otherwise still passed
through byte for byte.

2. A left-behind PUA cmap

reencode_to_pua rewrites the font's cmap in place and the SfntFont is
shared by every page and every output of a document. write_font_face never
restored it, so the next output's glyph_for_code missed and fell back to
using the character code as a glyph id. In this file that turned every number,
date and amount into a run of accented letters — 20.984,99 came out as
ÄÃÄ.ÃÄÅ,ÃÃ. Both callers now go through one helper that restores the cmap.

3. Tables that only put the font at risk

SfntFont::write copied every table through verbatim, including OpenType
Layout, device metrics and signatures. One malformed table sinks the whole
font at the sanitizer, and none of them earn their place — the caller
positions every glyph itself, and the CSS already sets font-kerning:none /
font-variant-ligatures:none, so layout never applies. Now an allowlist:
outlines, metrics, hinting and color strikes; everything else dropped.

Verification

A sweep over the whole generated corpus — every @font-face in every output
HTML, run through ots-sanitize:

before after
distinct font programs 591 591
rejected (would render tofu) 5 0
accepted with a dropped table 2 0

Unit tests cover the name patch (CFF still parses against its own absolute
offsets afterwards) and the table allowlist. Full HtmlOutputTests green.

andiwand and others added 3 commits August 8, 2026 00:57
A `CFF ` Name INDEX entry holding a space or a PostScript delimiter makes
the browser's OpenType Sanitizer discard the whole table, so the wrapped
font never loads and the page renders tofu. PDF producers emit such names
routinely (`*Microsoft Sans Serif-3111`).

Patch the offending bytes in place. The replacement is the same length, so
the absolute offsets the Top DICT carries stay valid and the CFF is still
passed through byte for byte otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NpJEXjYDdL43zpDg3SvHTK
`reencode_to_pua` rewrites the font's cmap in place, and the `SfntFont` is
shared by every page and every output of a document. `write_font_face` left
the PUA cmap behind, so the next output's `glyph_for_code` found nothing in
it and fell back to using the character code as a glyph id — whole runs
drawn with the wrong glyphs.

Route both callers through one helper that restores the cmap, as the
usability probe already did.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NpJEXjYDdL43zpDg3SvHTK
Every table was copied through verbatim, including the OpenType Layout,
device-metric and signature tables. One malformed table — routine in a PDF
subset — makes the OpenType Sanitizer reject the whole font, and none of
them earn their place: the caller positions every glyph itself, so layout
never applies.

Keep outlines, metrics, hinting and color strikes; drop the rest.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NpJEXjYDdL43zpDg3SvHTK
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@andiwand
andiwand enabled auto-merge (squash) August 7, 2026 22:59
@andiwand
andiwand merged commit 3a0d565 into main Aug 7, 2026
34 checks passed
@andiwand
andiwand deleted the fix/pdf-font-tofu branch August 7, 2026 23:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant