Skip to content

Measure the exact boxes of non-embedded CJK fonts on their faces (#624) - #659

Merged
yhay81 merged 7 commits into
mainfrom
claude/624-cjk-exact-boxes
Nov 4, 2026
Merged

yhay81 merged 7 commits into
mainfrom
claude/624-cjk-exact-boxes

Conversation

@yhay81

@yhay81 yhay81 commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Closes #624

words, layout, chars, and search with exact_bboxes=True now measure a font that a document does not embed on the fallback face that draws it, as frozen pylopdf does. fallback-exact-boxes retires from evidence/phase1-parity.

Acceptance

  • On tests/assets/text/nonembedded-cjk.pdf, words(exact_bboxes=True) matches frozen pylopdf with its CJK extras, and evidence/phase1-parity/causes.toml no longer lists fallback-exact-boxes. run.py measured the merged tree: every output of the 7 pages is equal, and the corpus keeps only the one close raster and the heading of patent-us223898.pdf. PR Read a multi-glyph cluster as its text in other readers (#637) #650 moved nothing in the report. tests/test_parity.py passes with PYROPDF_PARITY_PYLOPDF set.
  • Editing doc.fallback_fonts changes the exact boxes that the next call returns, and a call with the same faces reuses the memoized result (test_editing_the_fallback_fonts_changes_the_exact_boxes_of_the_next_call, which counts hits and computations). Other text keeps its memo key (test_the_memo_of_other_text_keeps_its_key_when_the_fallback_fonts_change).
  • Without an installed face, exact boxes are as they are now, and a document whose fonts are all embedded reads no fallback face (exact_boxes_measure_a_font_that_a_document_does_not_embed_on_its_face, only_exact_boxes_of_a_font_that_is_not_embedded_read_a_face).
  • The API reference of exact_bboxes says that the boxes depend on the fallback faces (Page.words, Page.layout, Document.fallback_fonts).

python tools/check.py --locked passes. No engine API that fuzz/ uses changed: page_text keeps its signature, so Annotation.text and Document.text, which have no exact_bboxes, are untouched too.

Two things that differ from what the issue expected

The faces supply only the ink, in a second interpretation. Interpreting the page through the faces once, as frozen pylopdf does, is not enough on hayro 0.9. hayro 0.8 stretched a substitute's outline to the PDF's width, and hayro 0.9 puts that stretch in the glyph's transform, where nothing tells it apart. Advances taken through it are scaled: with /W [0 65535 500], 計画地 split into three words in exact mode. So the records still come from an interpretation without faces, which also keeps a face's line metrics out of the font's, and the faces give only ink. A watch on the font resolver runs the second interpretation only when a face would draw a font of the page, so other pages cost what they did and read no face.

I checked this against the oracle beyond the Acceptance, on the fixture and on the same pages with explicit half and full /W: words, layout with and without chars and styles, and search with boxes and quads, all with exact_bboxes=True, have no difference over 0.01 point. That script is not committed.

Page 1 was not equal because of CIDFontType0. Frozen pylopdf fills its faces at its first rendering, and text extraction before that gives what it gives without them. The dump rendered each page after reading it, so only page 1 read without faces, and the README's explanation was wrong. dump.py --fallback-fonts now renders first, and the README says so. pyropdf gives the same boxes in any order, so a frozen pylopdf call before any rendering still differs from it, by design.

Beyond the issue's Paths

docs/en/api/ is generated from the docstrings, and docs/en/rendering.md and docs/ja/rendering.md said that text extraction does not use the faces, which is no longer true.

Follow-ups, both filed

For 0.2.2's changelog

🤖 Generated with Claude Code

yhay81 and others added 5 commits November 4, 2026 12:01
`words(exact_bboxes=True)`, `layout`, `chars`, and `search` now measure a
font that a document does not embed on the fallback face that draws it, as
frozen pylopdf does. The faces join the memo key of the layout with exact
boxes, and other text stays the same under any faces.

Records still come from an interpretation without faces, and only their
ink from a second one through them: hayro 0.9 keeps a face's stretch to the
PDF's widths in the glyph's transform, which would change advances and so
split or join words. A watch on the font resolver runs that second
interpretation only when a face would draw a font of the page, so a
document with embedded fonts reads no face.

Frozen pylopdf fills its faces at its first rendering, so its first page
read without them. The parity dump now renders first, which replaces the
earlier explanation of that page by its descendant font type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…boxes

# Conflicts:
#	crates/pyropdf-engine/src/extract/text.rs
Every output of the 7 pages of nonembedded-cjk.pdf now matches frozen
pylopdf, so `fallback-exact-boxes` retires. PR #650 moved nothing in the
report: the corpus keeps its one close raster and the heading of
patent-us223898.pdf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yhay81
yhay81 merged commit b3a76eb into main Nov 4, 2026
14 checks passed
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.

Measure the exact boxes of non-embedded CJK fonts on their fallback faces

1 participant