Skip to content

fix(pdf): lay a matrix-placed run out large and scale it down - #752

Merged
andiwand merged 3 commits into
mainfrom
fix/pdf-matrix-run-font-size
Aug 27, 2026
Merged

fix(pdf): lay a matrix-placed run out large and scale it down#752
andiwand merged 3 commits into
mainfrom
fix/pdf-matrix-run-font-size

Conversation

@andiwand

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Stacked on #751#750#749.

This is the bug the user actually reported. Text comes out in giant overlapping type on a phone.

Cause

The matrix path lays a run out in its block's own space and lets the CSS matrix scale it, so the CSS font-size is the PDF's Tf size — which InDesign and friends routinely write as 1, putting the real size in Tm:

/T1_0 1 Tf
43.4349 0 0 45 50.9214 757.6895 Tm

That emitted .f2{font-size:1pt} (1.33px) with matrix(10.1768,0,0,11.3076,0,0). A browser with a minimum font size clamps 1.33px up to its floor — 8px is the Android WebView default — and then the matrix multiplies the clamped size. 1.33 → 8 is a 6× boost, times the matrix.

Reproduced exactly with --blink-settings=minimumFontSize=8,minimumLogicalFontSize=8: the render is pixel-for-pixel the reported screenshot. It also explains why only some text broke — runs on the uniform branch carry their real size (.f1{font-size:14.7pt}) and were never clamped. In the report, "INHALT" is fine and the TOC under it is destroyed; that is exactly the uniform/matrix split.

-webkit-text-size-adjust:none does not help — minimumFontSize is a separate Blink setting from the autosizer. Verified, byte-identical output.

Fix

Blow the whole local space up by a constant (48) and divide the matrix by it. font-size:1pt + matrix(10.1768,…) becomes font-size:48pt + matrix(0.212017,…), same translate, same rendering, and the font size is now far out of the clamp's reach.

Constant rather than per-run: runs in one block are compared against each other in that space, so a per-run factor would break block grouping.

It also renders better

A 1pt font is too small to carry the metrics the baseline is derived from, and the matrix multiplied that error too. Rasterising all 255 pages of the 11-file corpus and scoring against ghostscript, every file improves or holds — none regress:

file before after
Rockrider_..._Gebrauchsinformationen 4.59 2.39
Rockrider_..._Allgemein_und_Garantie 4.68 2.79
Laserentfernungsmesser_PLEM50_D5 5.88 5.30
Sunfun_Melina_BBQ_Pavillion 1.95 1.70
Fahrradtraeger_... 4.71 4.57

(mean absolute difference vs the ghostscript raster, lower is better)

So the output does change — the old placement was wrong, and the reference output should be regenerated.

Two tests pin it: a 1 Tf run with a Tm scale emits font-size:48pt and matrix(0.208333,0,0,0.416667,…), and a uniform run still states its own 12pt.

Full suite 1230 passed, 0 failed, 6 pre-existing skips.

Known residue, not in this PR

The uniform branch states the real size, so genuinely small text is still clamped — milder, since nothing multiplies it. Measured by rendering at minimumFontSize 0 / 8 / 24:

  • at 8 (the android default): the reported files are now identical across settings; FNIRSI still shifts 4% of pixels and Laser 0.2% — small text slightly enlarged, readable, no overlap
  • at 24 (Chrome's maximum user setting): Normstahl shifts 26% and does overlap

Fixing that means giving every run a transform, or scaling the whole text layer through one wrapper. Worth doing, but it is a much larger change than this one and deserves its own PR.

@andiwand
andiwand force-pushed the feat/pdf-dedupe-images branch 2 times, most recently from 0b4d9ba to 5846958 Compare August 27, 2026 21:26
Base automatically changed from feat/pdf-dedupe-images to main August 27, 2026 21:26
`const std::string &` over a braced list of `const char *` constructs a
temporary per iteration, which gcc rejects under `-Werror=range-loop-construct`.
Clang does not, so it reached main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiPhteiEmPp8iPQLBfnthA
@andiwand
andiwand force-pushed the fix/pdf-matrix-run-font-size branch from c4a9589 to 384d4d6 Compare August 27, 2026 21:37
andiwand and others added 2 commits August 27, 2026 23:39
The matrix path laid a run out in its block's own space and let the CSS matrix
scale it, so the CSS font size was the PDF's `Tf` — routinely `1`, the size
living in `Tm`. A browser with a minimum font size (8px on android) clamped
that up and the matrix multiplied the clamped size, which is how a page came
out in giant overlapping type. Reproduced with
`--blink-settings=minimumFontSize=8`.

Blowing the local space up by a constant and dividing the matrix by it renders
identically and puts the font size out of the clamp's reach. Constant rather
than per run: runs in one block are compared against each other in that space.

It also places such a run *more* accurately — a 1pt font is too small to carry
the metrics the baseline is derived from, and the matrix multiplied that error
too. Against a ghostscript raster of all 255 pages every file improves or holds:
Rockrider 4.59 -> 2.39 and 4.68 -> 2.79 mean absolute difference, Laser 5.88 ->
5.30, none worse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169dUoxqhqfi7ZU5ei8b9ov
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KiPhteiEmPp8iPQLBfnthA
@andiwand
andiwand force-pushed the fix/pdf-matrix-run-font-size branch from 384d4d6 to ee1dcb7 Compare August 27, 2026 21:44
@andiwand
andiwand merged commit c382fa4 into main Aug 27, 2026
27 checks passed
@andiwand
andiwand deleted the fix/pdf-matrix-run-font-size branch August 27, 2026 21:45
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