Skip to content

fix(pdf): flow matrix runs into one selection block - #721

Merged
andiwand merged 3 commits into
mainfrom
fix/pdf-matrix-selection-flow
Aug 20, 2026
Merged

fix(pdf): flow matrix runs into one selection block#721
andiwand merged 3 commits into
mainfrom
fix/pdf-matrix-selection-flow

Conversation

@andiwand

@andiwand andiwand commented Aug 20, 2026

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Stacked on #720 — review that one first; this PR's diff is against it.

What was wrong

Marking text on a page laid out glyph by glyph highlighted a trail of narrow
boxes beside the words instead of the words:

Two things caused it.

1. Every matrix run got its own block. The selection layer compares runs by
ox/baseline, which live in page space, so once a general transform is in
play two runs of the same line are not comparable and each opened a block of
its own. Blocks are absolutely positioned, so the glyphs still landed right —
but the selection became one box per glyph.

2. Those blocks carry no width. width_pt > 0 && !is_matrix skipped the
w class on the matrix path, so each box shrink-wrapped its content. The
selection layer renders in sf at size-adjust:50% (deliberately narrow, then
justified out to the PDF advance), so with nothing to justify to, every box came
out roughly half the width of the glyphs it covers.

The fix

A run is measured in the frame its line block lays out in: page space for an
axis-aligned block, the block's own space for a matrix one — where the CSS
matrix, not the page, is what relates the runs to each other. Origins resolve
along the block's axes (local_origin), and the unit of the frame is the run's
font size, so starts_new_line, the gap test and the widths all carry over
unchanged. A run joins the open block when its linear part matches
(same_linear); otherwise it opens a new one.

break_space from #720 then becomes simply "did the frame carry over", which is
what that flag was reaching for.

A second bug the flow exposed

With the words merged, every line still came up short by about a space per word.
A whitespace-only run emits no .sr — its core is empty — and the spacer span
beside it was sized by the gap before it alone, so its own advance was dropped.
One Tj per line hid this completely (the whole line is one .sr and its width
is the whole advance); word by word it accumulated. The spacer now takes it.

Measured on the affected page, selection extent vs glyph extent per line:

before after
heading, one word 44.3–103.5 vs 44.3–109.6 44.3–109.6 vs 44.3–109.6
body line, ten words 44.3–716.6 vs 44.3–731.8 44.3–731.8 vs 44.3–731.8
body line, eight words 44.3–616.0 vs 44.3–634.1 44.3–634.0 vs 44.3–634.1

Scope of the output change

31 documents, all pdf. Every changed line is a .sr / .sg / .t … i
selection element or a .wN width rule — a scripted classification of all
21799 distinct changed line shapes finds zero in the visual layer: no glyph
span, no <path>, no @font-face, no non-i block. Display is untouched.

Selection blocks on the affected page drop from ~1900 to 33.

Review follow-ups

  • A whitespace-only run's advance is now owed to whatever comes next — as the
    spacer's width, or as a margin-left on the run — so a line opening with a
    space, or carrying two in a row, keeps its geometry.
  • A run a rise lifts off the block's baseline (a superscript) gets a block of
    its own again rather than taking the block's baseline, and without the
    separator a real line break would bring.

Tests

  • PdfFile.matrix_runs_flow_into_one_selection_block: glyphs shown one Tj at a
    time under an anisotropic Tm land in a single selection block, both words
    and the space between them carrying a PDF-derived width. It fails on fix(pdf): keep a matrix-placed word from spacing out its glyphs #720
    (5 blocks, no merged words, no widths).
  • Reference output regenerated; pins advanced.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 572f986e67

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/odr/internal/html/pdf_file.cpp Outdated
Comment thread src/odr/internal/html/pdf_file.cpp
@andiwand
andiwand force-pushed the fix/pdf-matrix-run-spacing branch from 21c2771 to 8ab31a4 Compare August 20, 2026 18:45
Base automatically changed from fix/pdf-matrix-run-spacing to main August 20, 2026 18:50
@andiwand
andiwand force-pushed the fix/pdf-matrix-selection-flow branch from 572f986 to 0e953f5 Compare August 20, 2026 19:15
The selection layer measured every run in page space, so two runs a general
transform placed could not be compared and each got a line block of its own —
one per glyph on a page laid out glyph by glyph. Those blocks carry no
PDF-derived width either, so each shrink-wrapped the half-size fallback font
and the marking sat beside the glyphs rather than over them.

Runs one CSS matrix can place now flow inside a single block, measured in that
block's own frame (origins resolved along its axes, the unit its font size), so
the existing gap and line tests apply there unchanged and the widths follow.

The advance of a whitespace-only run went missing along the way: it emits no
`.sr` to carry it, and the spacer span was sized by the gap before it alone.
One `Tj` per line hid that; word by word it cost a space per word.

Only the selection layer and the width-class table move — no glyph, path or
`@font-face` line differs in the reference output.
@andiwand
andiwand force-pushed the fix/pdf-matrix-selection-flow branch from e7b7577 to 34e70bb Compare August 20, 2026 19:23
…he block

Two holes in the selection flow.

A whitespace-only run emits no `.sr`, and the spacer that should carry its
advance was skipped wherever a space was already there — a line opening with
one, or two in a row. The advance is now owed to whatever comes next, reaching
it as the spacer's width or as a `margin-left` on the run itself.

A block anchors its runs to one baseline, so a run a rise lifts off it — a
superscript — took the block's baseline instead of its own once the frame let
it flow. It gets a block of its own again, without the separator a real line
break would bring.
…dropped

`read_operator_name` stopped at four of the nine delimiters of 7.2.2. `(` was
not among them, so `Tm(text)Tj` let the name run on and swallow the string, and
the show was dropped as an unknown operator. The whole set now ends a bareword;
a stray closing one, which opens no token for any reader above to consume, is
eaten so the caller's loop still makes progress.

`font_is_usable` swallowed the re-encode failure with a bare `catch (...)`,
though failing there swaps in a substitute and the page shows it. It now warns
through the `Logger`. No corpus font takes that path, so no reference output
moves.
@andiwand
andiwand force-pushed the fix/pdf-matrix-selection-flow branch from 67962af to a47cede Compare August 20, 2026 19:35
@andiwand
andiwand merged commit 556d803 into main Aug 20, 2026
27 checks passed
@andiwand
andiwand deleted the fix/pdf-matrix-selection-flow branch August 20, 2026 19:40
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