Repository navigation
A PDF read back as a document rather than as text - #2
Merged
Merged
Conversation
A PDF page has no headings, no paragraphs and no lists. It has instructions for drawing glyphs at places, and everything a reader sees above that is an inference. This turns those inferences into the richdoc model, which gets Markdown, reST and HTML out of a PDF through code the fleet already has. main was created by GitHub (ghnew), so no unreviewed commit of ours is in the history. WHAT A HEADING IS Text set larger than the body, where the body size is the one the document draws the most CHARACTERS at, over every page. That much is PyMuPDF4LLM's IdentifyHeaders rule. But larger is not enough, and taking it as enough was a defect this reported on four real documents out of six: a 9-page report set in 10, 12 and 14 point came back with 131 headings, 130 of which were paragraphs. The report has two body sizes and "most popular" can only pick one. So a heading size also has to be RARE. Measured over six real documents, as the share of characters at each size above that document's body size: heading sizes 0.01% .. 5.76% second body sizes 32.6% .. 39.4% An order of magnitude apart; the threshold sits between them at 15%. THE ORDER OF THE WHOLE THING A numbered heading leaves a gap after its number, the table below it leaves that same gap empty on every row, so asking the page for its tables returns ONE two-column table that is the heading and the grid together -- and the grid's real columns are gone. Page 2 of a 6-page paper read as | 2. | Vue d'ensemble technique | with the whole four-column comparison crushed into the second column. Nothing in the alignment says that line is a heading; its SIZE does, and this package is the only one that knows which sizes those are. So the tables are looked for among the lines that are NOT headings. NO ERROR Nothing here can fail. The document has already been parsed to get this far, and a page that cannot be READ is an empty page -- an answer, not a failure. An error that is always nil teaches every caller to write a branch that never runs. 100% of statements with no uncovered blocks, 21 mutations caught. Several of the tests needed fixtures with REAL proportions: a document of one heading and one short line makes that heading a fifth of the text, and the share rule correctly calls a fifth of a document body text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things the real documents showed once the markdown came out. A WRAPPED HEADING IS ONE HEADING. A two-page note's title broke across two lines at a hyphen and came back as two h1s, the second reading "ager". The guard is the same SIZE and not the same level: levels are clamped to as many as the format has, so thirty, twenty-eight and twenty-six point can share one -- and joining on the level made one heading out of three. The distance is the other guard, so a second heading further down stays its own. NO SPACE AFTER A HYPHEN. "cadre man-" and "ager" joined with a space is "man- ager", which is not what the page says; removing the line break says "man-ager", which is. And the hyphen STAYS: dropping it reads "manager" here and "wellknown" for a word broken at its own hyphen, and the two cannot be told apart without a dictionary. Keeping it loses nothing a reader cannot undo. The same join is used in all three places a line continues another: a paragraph, a heading and a list item. Measured: the 213-page reference work goes from 695 headings to 618, and the note's title from two to one. 100% of statements, no uncovered blocks, 27 mutations caught. Co-Authored-By: Claude Opus 5 <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.
A PDF page has no headings, no paragraphs and no lists. It has instructions for
drawing glyphs at places, and everything a reader sees above that is an
inference. This turns those inferences into the
richdoc model — which gets Markdown,
reST and HTML out of a PDF through code the fleet already has.
mainwas created by GitHub (ghnew), so no unreviewed commit of ours is inthe history.
What a heading is
Text set larger than the body, where the body size is the one the document
draws the most characters at, over every page. That much is PyMuPDF4LLM's
IdentifyHeadersrule.⛔ But larger is not enough, and taking it as enough was a defect this
reported on four real documents out of six. A 9-page report set in 10, 12 and
14 point came back with 131 headings, 130 of which were paragraphs — the
report has two body sizes, and "most popular" can only pick one.
So a heading size also has to be rare. Measured over six real documents, as
the share of characters at each size above that document's body size:
An order of magnitude apart; the threshold sits between them at 15%, and the
code says why that is a fact about documents rather than a tuning knob.
⛔ The order of the whole thing
A numbered heading leaves a gap after its number; the table below it leaves
that same gap empty on every row; so asking the page for its tables returns
one two-column table that is the heading and the grid together — and the
grid's real columns are gone. Page 2 of a 6-page paper read as:
Nothing in the alignment says that line is a heading; its size does, and
this package is the only one that knows which sizes those are. So the tables
are looked for among the lines that are not headings, and the same page now
reads:
No error
Nothing here can fail. The document has already been parsed to get this far,
and a page that cannot be read is an empty page — an answer, not a failure.
An error that is always nil teaches every caller to write a branch that never
runs.
How it is checked
100% of statements with no uncovered blocks, 21 mutations caught. The one
survivor is the tie-break between two equally popular sizes, which buys
determinism and not a different reading; the code says so where it is.
Several tests needed fixtures with real proportions — a document of one
heading and one short line makes that heading a fifth of the text, and the
share rule correctly calls a fifth of a document body text. Two tests failed on
that, and the fixtures were what was wrong. One test passed against the
unfixed code because its heading sat six points from its number, too narrow
to be a boundary at all; it now asserts the defect before asserting the fix.
Measured on real documents, the control last:
Parity
This is the capability behind BentoPDF's
pdf-to-markdownandprepare-pdf-for-ai; thepdfopsverbs come next.🤖 Generated with Claude Code