PDF Conversion
PDF is the format all2md works hardest at, because a PDF does not describe a document — it describes ink on a page. There are no headings, no paragraphs, no tables and no reading order in the file; there are positioned glyphs, and sometimes vector lines. Everything structural that comes out the other side was inferred.
This page explains what those inferences are, which knobs change them, and what
they are worth. Numbers quoted here are measured against a pinned corpus of 66
born-digital PubMed Central articles with publisher JATS XML as ground truth
(benchmarks/pmc); where a number comes from the scanned-page corpus instead,
it says so.
For the exhaustive option reference see Configuration Options. For searching the option space automatically on one of your own documents, see PDF Parsing Optimizations.
Two paths through a PDF
Every page goes down one of two routes, and they behave very differently:
The text layer. Born-digital PDFs — anything produced by a word processor, LaTeX, or a publisher’s typesetting pipeline — carry the characters as data. all2md reads them with their font, size, weight and bounding box, which is what makes heading detection, table extraction and column ordering possible at all.
OCR. Scanned pages carry only a picture. Text has to be recovered by an OCR engine, which returns lower-confidence characters and coarser geometry. Some structure survives (see OCR), but less of it.
The two are decided per page, not per document, so a report with three scanned appendix pages takes both routes in one conversion.
# Install the PDF extra (PyMuPDF)
pip install all2md[pdf]
# Convert, keeping the default inference
all2md report.pdf --out report.md
Layout analysis
By default all2md infers structure from geometry alone: font sizes, gaps and
alignment. Installing the optional pdf_layout extra adds a machine-learning
layout model that labels regions semantically — title, section-header,
list-item, table, caption, footnote, picture, formula.
pip install all2md[pdf_layout]
Note
pdf_layout is deliberately excluded from the all extra. Its
dependency, pymupdf-layout, is distributed under the Polyform
Noncommercial license, so it cannot be pulled in by a blanket install. You
have to ask for it. Everything on this page works without it; the table and
heading numbers are simply better with it.
--pdf-layout-analysis-mode (auto / enabled / disabled, default
auto) controls whether the model is consulted. auto uses it when it is
installed and silently skips it when it is not.
--pdf-layout-feature-set picks which bundled classifier reads the page:
Value |
Features |
When it helps |
|---|---|---|
|
image + text geometry |
The default. Balanced across document kinds. |
|
text geometry only |
Dense multi-column text. ~29% faster for skipping image inference. |
|
image only |
Pages where geometry is uninformative. |
The difference is not cosmetic. On a two-column reference page the
image-feature models read the dense left column as a table and deleted nine
reference entries; rf labelled all 41 entries correctly and predicted no
table at all. Across 20 articles rf led on every axis measured. The
default is deliberately unchanged anyway, because that is one corpus of one
document kind, containing no scanned pages — where image features plausibly
earn their place. If your documents look like journal articles, try rf.
Tables
--pdf-table-detection-mode (default both) chooses the strategy:
pymupdf— PyMuPDF’s own detectorruling— ruled lines on both axesboth— try eachnone— never emit a table
Journal tables are typically booktabs-style: horizontal rules only, or none at
all. Line-based strategies find nothing on those, so with layout analysis on, a
region the model predicts to be a table also gets a text-alignment fallback
(--pdf-table-fallback-extraction-mode, default grid).
That fallback is held to one test the line strategies are not: its columns
must not cut through words. It has no ruling lines corroborating it and the
layout model over-fires, so without that guard a mis-predicted region rendered a
page of ordinary prose as a seven-column table of half-words
(study was condu | cted to explore). Every table metric improved while
whole-article text recall fell from 92.6% to 83.8% — a good example of why one
instrument is never enough. Grid shape, reading-order preservation and region
corroboration were each tried as guards and each failed to separate real tables
from gridded prose; whole-word integrity, measured against the page’s own word
segmentation, separates them cleanly.
Borderless tables are recovered from the geometry of their own words. When neither ruling lines nor the grid fallback commit, a table-predicted region is swept for word gutters — vertical channels no word crosses, persistent down the region. Columns that never touch are what makes a table a table, so the gutters recover exactly the booktabs-style grid the line strategies cannot see. The same sweep runs a second time in the rotated frame, which is how a sideways table on a landscape page comes back as a table rather than a column of fragments. Two-column regions — the narrowest grid the sweep can claim — are admitted under measured guards: a numbered-bibliography test (the one two-column grid every reference list resembles) and a drawing-density gate (chart regions carry hundreds of vector paths where real tables carry a handful).
Together these took the corpus from 4 tables emitted against 121 expected to 173 against 121 — the surplus is real tables the ground truth does not place, not junk — with the residual deficit at 6, each miss with its own recorded cause.
Figures
A figure on a PDF page is an image (or a cluster of images, or bare vector
drawings) with a caption set near it. The parser binds the two into a
Figure container — the image plus its caption as one block — rather than
emitting an image here and an orphaned paragraph there. Grouped panels bind to
one shared caption, and a vector-drawn figure with no raster image at all still
gets its caption attached. This holds under the default attachment mode
(alt_text): the caption survives as the figure’s structure even when the
image bytes themselves are not extracted.
Reading order and columns
Column detection is on by default (--pdf-no-detect-columns turns it off),
with --pdf-column-detection-mode (auto / force_single /
force_multi / disabled) when you know better than the heuristic, and
--pdf-column-gap-threshold (default 20 points) setting how wide a gutter has
to be to count.
Where a page’s gutter is too narrow to split, blocks are ordered by vertical position — and two columns that start level are a hazard, because whichever column’s top edge is a fraction of a point higher would otherwise decide the order of the whole page. Blocks whose tops fall within a twentieth of the page’s average line height are now treated as starting on the same row and ordered left-to-right. The tolerance is size-relative rather than a fixed point count, since what reads as “level” scales with the type size.
Headings, lists and prose
A few inferences are worth knowing about because they explain output that would otherwise look arbitrary:
A heading that wraps onto a second printed line is one heading. A PDF has no notion of a wrapped heading — it has two lines of type. A second line continues the first when they share a level, nothing was emitted between them, the gap is within a ratio of the line height, and the first line filled the measure the two lines share — a line only wraps because it ran out of room, which is what separates a wrap from a subsection heading printed directly under its section heading. A line opening with its own numbering starts a new heading however tightly it is set.
A heading must contain a letter or digit. Large math delimiters are set in a
symbol font well above body size, so they cleared every size and length gate. One
chemistry paper emitted 179 headings for its 9 sections, 122 of them a single
Private Use Area glyph. The gate is str.isalnum, deliberately not an ASCII
test, so headings in CJK, Cyrillic, Arabic, Devanagari, Greek and Hangul still
qualify — an ASCII test would have deleted every one of them while fixing this.
List markers are read after glyph normalisation, not before. The parser
rewrites four bullet glyphs (U+F0B7, U+00B7, U+2022, U+25CF) to
-, and three of the four are not markers in their printed form, so detection
has to run afterwards. Detection descends into inline wrappers, because a bullet
set in a symbol font arrives wrapped in Emphasis and a line can have no
top-level text node at all.
A number is held to a stricter rule than a bullet. A marker and the space after it are routinely separate spans, so bullet detection is allowed to read across that boundary. Numbers are not, and the restriction is load-bearing: nothing in a PDF distinguishes the 44th bibliography entry from the 44th item of a list, and reading across turned reference lists into ordered lists renumbered from 1 — so reference 44 came out as item 1 and no citation in the body could be matched to its reference.
Words broken across a line are rejoined. A capitalised continuation keeps its
hyphen (Anglo-Saxon) and a digit continuation is never merged.
The honest limit: items the PDF prints with no marker of any kind cannot be recovered by any marker rule, and they are the bulk of what list detection still misses.
OCR
OCR is off unless you ask for it or auto mode decides a page needs it.
# Tesseract (default engine) — needs the Tesseract binary on your system
pip install all2md[pdf,ocr]
# EasyOCR — no system binary, but pulls in PyTorch
pip install all2md[pdf,ocr-easyocr]
all2md scan.pdf --pdf-ocr-enabled --pdf-ocr-mode force --pdf-ocr-languages eng+fra
When ``auto`` fires. Three conditions, and the third is the one that changed:
Option |
Default |
Meaning |
|---|---|---|
|
50 |
Minimum characters for a page to count as text-bearing. |
|
16 |
Whole-document floor. If no page triggered OCR and the result still has fewer meaningful characters than this, the parse is retried once with OCR forced. |
|
0.8 |
Share of the page its largest single image must cover to read the page as a scan. |
That last threshold is measured on the largest image, not on every image’s area
added together, and the boundary is calibrated rather than picked: across 101
scanned pages the largest image covers exactly 100% of every one, while across
851 born-digital pages it never passes 64%. The old behaviour — summed area at
0.5 — fired on 20 pages across 11 of 66 born-digital articles that had perfectly
good publisher text, and because --pdf-ocr-preserve-existing-text defaults to
False, that text was discarded and re-read from a picture.
Warning
If you set --pdf-ocr-image-area-threshold explicitly, note its meaning
changed along with its default. It is now read against the largest single
image rather than the summed area of all of them.
Remaining OCR knobs: --pdf-ocr-engine, --pdf-ocr-dpi (default 300),
--pdf-ocr-languages, --pdf-ocr-auto-detect-language, --pdf-ocr-gpu
(EasyOCR only), --pdf-ocr-tesseract-config for raw Tesseract flags, and
--pdf-ocr-preserve-existing-text to merge rather than replace.
What OCR gives you structurally. Tesseract reports block, paragraph and line
numbers with per-word boxes, and all2md keeps that segmentation instead of
flattening the page to a string — so an OCR’d page emits real blocks with real
bounding boxes, and each one reaches the AST with its own
SourceLocation.metadata['bbox']. One scanned page went from a single box
covering the whole page to 54 distinct ones, which is what lets a citation into a
scanned document resolve to a region rather than to “somewhere on this page”.
Engines that cannot report layout — EasyOCR today — fall back to flat text rather than losing the page, and reading order for OCR’d blocks comes from the engine rather than being rebuilt from geometry, because OCR boxes are tight to their glyphs and defeat the column detector.
Working with pages
# A page range
all2md report.pdf --pdf-pages "1-5"
# Custom page separator (a parser option, shared with ODP and PPTX)
all2md report.pdf --pdf-page-separator-template "--- Page {page_num} ---"
# Drop running headers and footers (off by default)
all2md report.pdf --pdf-auto-trim-headers-footers
From Python
from all2md import OCROptions, PdfOptions, to_markdown
options = PdfOptions(
pages="1-20",
layout_feature_set="rf", # text-geometry classifier
table_detection_mode="both",
ocr=OCROptions(enabled=True, mode="auto", languages="eng"),
)
markdown = to_markdown("report.pdf", parser_options=options)
Knowing how much to trust a conversion
Because so much of a PDF conversion is inference, it is worth checking rather
than assuming. all2md report prints a confidence report naming what was
degraded, guessed, or dropped:
all2md report report.pdf
See also PDF Parsing Optimizations for searching the option space against one of your own documents rather than reasoning about the knobs by hand.