fix(pdf): place a run by its own font, and land a search hit on its word - #694
Merged
Merged
Conversation
The ios app reported a search hit covering the wrong word. Four faults in the html pdf renderer, all in how a run is placed relative to the one before it. A recovered word break was written into the run text *and* spanned by the `margin-left` placing that run, so a page whose fonts are not embedded drifted a space to the right per word — far enough by the end of a line that the selection layer's highlight sat over a neighbouring word. Both fallback paths now take the run's core text; single-layer gives the break its own span, as its collapse path already did. A substitute `@font-face` named only the family, so `local(Helvetica)` resolved the regular cut and the browser faked the bold at the regular's advance widths. It is now asked for by the name of the cut it wants, and declares the weight and style so nothing is synthesised on top. A visual line block carries its first run's placement and the rest flow inline off it, so a run in another font or size was anchored against metrics that were not its own — subscripts flattened onto the baseline, and a bullet floated the text beside it a line away from the highlight that selects it. The block now breaks on the same flow key the single-layer path already used. A gap spacer's lone space cannot be justified to its box the way a word is, so a selection or a search hit left a sliver of white at every word break. Its advance is padded and clipped back, for a gap that still reads as a word space — a wider one is a column of white, not a space. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QELPoDuuBiFJ6B3jVFDok5
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cd60b068dc
ℹ️ 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".
`family_stack` quotes a name that needs it ('Times New Roman', 'Courier
New', 'Zapf Dingbats'), and `local_font_sources` quotes again — so a serif
or monospace substitute asked for a face named with literal apostrophes,
matched nothing, and rendered through the plain stack without the
ascent/descent overrides its placement assumes.
Advances the reference-output pins for the whole branch.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QELPoDuuBiFJ6B3jVFDok5
andiwand
enabled auto-merge (squash)
August 16, 2026 19:59
andiwand
disabled auto-merge
August 16, 2026 20:03
andiwand
added a commit
that referenced
this pull request
Aug 16, 2026
They landed under the `v6.7.0` heading: the branch was cut before the release run stamped it, so the merge put them where `## Unreleased` had been. v6.7.0 shipped without that fix — it is in the next release. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
The ios app reported a search hit covering the wrong word, on
OpenDocumentReader/Samples/sample-paper-en.pdf. Four faults in the html pdfrenderer, all in how a run is placed relative to the one before it. Everything
here is dual-layer unless noted; the tint path in the 6.7.0 notes was engaging
all along — the highlight was simply sitting over the wrong glyphs.
A recovered word break was counted twice
The extractor prepends a space to a run when it infers a word break; that space
"backs no code and no advance". The glyph and collapse paths skip it, but both
fallback paths rendered it literally while the run's
margin-leftalreadyspanned the whole gap. On a page whose fonts are not embedded the text drifted a
space right per word — the repro's second line ran 630px against the selection
layer's 562px, so
<mark>on "quarter" painted over "cond q".Both paths now take the run's core text. Single-layer gives the break its own
span, which is what its collapse path already did.
A bold substitute rendered at regular widths
The generated face was
src:local("Helvetica")with no weight descriptor.local()matches a face's full or PostScript name, never a family plus theweight the
@font-facedeclares, so it resolved the regular cut and the browserfaked the bold — synthetic bold does not widen advances. The
.srbox came fromthe pdf's bold metrics, so a hit on "Invoice" measured 101.1px over 92.9px of
glyphs.
The cut is now asked for by name (
local("Helvetica-Bold"),local("Helvetica Bold"), then the plain name so a system without it still resolves), and theface declares the weight and style so nothing is synthesised on top.
A line block anchored every run on its first run
A visual line block carries its first run's placement and the rest flow inline
off it. A run in another font or size was therefore anchored against metrics
that were not its own. On the Infineon datasheet a bullet at 8pt put the 10pt
text beside it 7px off its own baseline, a full line away from the highlight
that selects it — and every subscript on the page was flattened onto the
baseline for the same reason.
The block now breaks on a flow key of font, size, ascent and substitute — the
same thing the single-layer path already did.
toptopA gap spacer painted a sliver
.sgis an inline-block of the pdf's gap width, but its lone space renders atroughly half that under
size-adjust:50%, and a highlight paints the glyph, notthe box — so a selection or a multi-word hit left white at every word break.
.srescapes this because justify stretches the word to fill.Its advance is now padded and clipped back by the box, but only for a gap that
still reads as a word space (
<= 1em). Above that it is a column of white, nota space: the datasheet's footer puts
www.irf.comand the page number1inone line separated by a 481px spacer, and padding that painted a bar across the
whole page.
Verification
Tdpositions — "quarter," at 265.8pt renders at 713.7px, exact.V_DSS,R_DS(on),I_D @ T_C = 25°Cinstead of flattened, and
HEXFET®superscripts.phd-agreement.pdfidentical but for one form placeholder moving to its ownbaseline. Html grows 0.43% (odr-public) / 0.16% (odr-private).
A quoted family was quoted again
family_stackalready quotes a name that needs it —'Times New Roman','Courier New','Zapf Dingbats'— andlocal_font_sourcesquoted it a secondtime, so a serif or monospace substitute asked for a face named with literal
apostrophes. Nothing matched, and the text rendered through the plain family
stack without the ascent/descent overrides its placement assumes — the very
misplacement the rest of this PR fixes. Found by Codex on the first commit;
fixed in
3bd49b4b. Probed on macOS/Chrome:local("Times New Roman Bold")now resolves to the bold cut (311.1px against 299.9px regular).
Notes
test/data.cmake:OpenDocument.test.output@ea67115(17 files) andOpenDocument.test-private.output@97f3bb9(581 files), both committed onmainof their repos. Only pdf output changed — no other format, and no fileadded or removed.
still chains
margin-left, so a long substituted run can drift. Fixing thatneeds per-run widths in the visual layer.
.sr,text-justify:inter-characterspreads the fallbackacross the pdf width, so a partial-word hit stays approximate. Whole-word
hits are exact.