Skip to content

fix(flash): measure word gaps from each char code's font width - #553

Merged
rejojer merged 3 commits into
mainfrom
fix/flash-code-widths
Oct 8, 2026
Merged

rejojer merged 3 commits into
mainfrom
fix/flash-code-widths

Conversation

@rejojer

@rejojer rejojer commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Flash split words such as "infe rence" and "Va riational" in page text and in titles, so a chapter heading like "Monte Carlo inference" no longer matched its bookmark.

Cause

The gap before a character was measured from the previous glyph's ink box whenever the ink reached past the glyph's advance, as the tail of a bold-italic "f" does. The ligature rule then held the pen at that ink edge, so the next letter looked like the start of a new word.

Fix

Measure from the pen position, as the extractor Flash was ported from does: the font's width for the character code the content stream draws. The existing code walk already pairs each character with that code, so it now also reads the width from the font dictionary (/Widths, else /MissingWidth; Identity-H /W, else /DW). PDFium's GetGlyphWidth looks widths up by unicode and can land on another glyph's width, for example a summation sign drawn with a letter code.

Only text whose pen moves along +x uses the code width: upright, with a positive font size, read left to right.

  • Rotated or mirrored text keeps the old measurement, since its pen does not move along +x.
  • Right-to-left characters keep it too. PDFium returns them in logical order, so the code walk pairs each one with its mirror's code.
  • Type 3 fonts keep it as well: their exact widths split headings ("Typ es") that the box end keeps whole.
  • So do characters the walk can't pair, and a character that a later walk re-pairs with a font that has no widths.

When PDFium drops a glyph that overlaps the one before it, such as the second "f" of an italic "ff" drawn one glyph per show op, Flash re-emits it. The re-emitted glyph now starts at the previous glyph's pen end rather than its ink edge. When no later glyph gives its advance, it advances by its own code width. Before, a 10-K read "the ef fects of".

Results

Measured on 16 documents (3,579 pages) against the original extractor's text items:

  • 4,744 word gaps fixed. 7 got worse, all in math or references, where the gap sits exactly on the 0.1 em threshold.
  • Outlines change in 2 documents, both for the better: titles get their whole words back, and a formula no longer becomes a heading.
  • CPU +4% on a 1,098-page book.

Rotated text in 34 other documents and 5,380 generated lines mixing Hebrew with Latin, punctuation and digits read exactly as before.

Tests

  • test_ink_past_the_advance_does_not_split_the_word: a narrow f whose ink overlaps the next letter. It reads "infe rence" before the fix and "inference" after.
  • test_dropped_glyph_after_ink_past_the_advance_does_not_split_the_word: an "f" painted where the word's second "f" lands makes PDFium drop that glyph. Without the fix the word reads "ef fects".
  • test_dropped_glyph_with_no_next_survivor_advances_by_its_code_width: a dropped glyph with no later glyph starts at the previous glyph's pen end and advances by its own code width.
  • test_rotated_text_ignores_code_widths: "O'Brien rock'n'roll" rotated 270°, which code widths would read as "O'B rien rock'n 'r oll".
  • test_rtl_chars_ignore_code_widths: a Latin line with two Hebrew letters keeps its order and gaps.
  • test_composite_font_widths_read_both_w_forms: both /W forms, /DW, and a bounded range.
  • build_pdf in tests/conftest.py also accepts a page's raw content stream.
  • Full suite: 844 passed. The flash tests also pass on Linux and on pypdfium2 4.30.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review ✅ Completed 2026-10-07T09:22:41.632069Z c155a32 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Flash split words such as "infe rence" and "Va riational" that the
reader it was ported from keeps whole. The gap before a char was
measured from the previous glyph's ink box whenever the ink reached
past the glyph's advance, as the tail of a bold-italic 'f' does. The
ligature band then held the pen at that ink edge, so the next letter
looked like a new word.

The original measures from the pen position instead: the font's width
for the char code the content stream draws. The code walk already pairs
each char with that code, so it now also reads the width from the font
dict (/Widths, else /MissingWidth; Identity-H /W, else /DW).
PDFium's GetGlyphWidth looks the width up by unicode and can land on
another glyph's width, e.g. a summation sign drawn with a letter code.
Type 3 fonts keep the box end, because their exact widths split
headings ("Typ es") that the box end keeps whole. Chars the walk can't
pair keep the old end as well.

On 16 documents (3,579 pages), checked against the original's text
items: 4,744 word gaps fixed, 7 worse, all in math or references where
the gap sits exactly on the 0.1 em threshold. A textbook outline gets
its titles back ("Monte Carlo inference"), and a paper's formula no
longer becomes a heading. CPU +4% on a 1,098-page book.
@rejojer
rejojer force-pushed the fix/flash-code-widths branch from c155a32 to b382695 Compare October 7, 2026 09:26
Rotated and mirrored text and right-to-left characters go back to the
box-end measurement: a rotated pen does not move along +x, and PDFium
returns RTL text in logical order, so the code walk pairs each char with
its mirror's code. The latest walk now decides a char's width, so a char
re-paired with a font that has no widths drops a stale one.

A glyph PDFium drops after an overlapping one is re-emitted from the
previous glyph's pen end instead of its ink edge, and advances by its own
code width when no later glyph gives the advance. A 10-K drawn one glyph
per show op read "the ef fects of".
Non-embedded Helvetica is drawn with the system's substitute, so whether
PDFium drops the second of two overlapping one-glyph "f" objects differs
between macOS and Linux. An "f" painted first at the same spot makes the
drop certain on both. The advance of a dropped glyph with no later glyph
is checked on _synthesize_dropped_glyphs directly.
@rejojer
rejojer merged commit 1a060df into main Oct 8, 2026
10 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.

1 participant