PDF-to-Markdown Test Corpus
Last updated: 21 September 2026
18 small PDFs, each built to break a PDF-to-Markdown converter in one specific way, plus the measured output of one converter against all of them.
The corpus is archived on Zenodo and has a DOI, so it can be cited. It is also mirrored on GitHub and on Hugging Face, but the archived copy is the one to cite.
Inspect the dataset card and PDF files, or use the repository’s files and versions to reproduce a check. For a versioned citation, use the Zenodo DOI below.
What this is
If you are writing a converter, or choosing one, the hard part is not the happy path. It is knowing what happens when a document has two columns, or a heading that is only bold, or a scanned page in the middle. This corpus makes that testable in about a minute, and gives you somewhere to point when you find a bug.
Every PDF in it was written byte by byte from scratch — no PDF library, no reportlab — so the exact content stream behind any result can be read. When a converter does something surprising, it matters that the input is not a black box.
Why I built it
The failure modes documented in the guides on this site were not found by reading specifications. They were found by building documents that isolate one behaviour each, converting them, and reading the output. That process needed a fixed set of inputs — something you can re-run over time and compare against, rather than a folder of ad-hoc test files that nobody can reproduce.
Having built it, publishing it costs nothing and saves the next person from rebuilding the same set. The corpus is released into the public domain for that reason.
What is in it
18 PDFs, 18 measured results, and the two scripts that build and check the corpus:
| File | What it isolates |
|---|---|
01-single-column.pdf | The case everything is built for — a baseline to compare against |
02-two-column-shared-baseline.pdf | Two columns on identical baselines |
03-table.pdf | Table cells sharing a baseline |
04-no-text-layer.pdf | A page with no text operators at all |
05-two-column-offset.pdf | Two columns that overlap for only part of the page |
06-three-column.pdf | Three columns sharing baselines |
07-bold-headings-body-size.pdf | Headings marked out only by weight, not size |
08-running-header-2-pages.pdf | The same header and footer repeated on every page |
09-hyphenated-breaks.pdf | Words split across lines with a trailing hyphen |
10-toc-wide-gap.pdf | A wide horizontal gap inside one visual line |
11-scan-styled.pdf | Bars standing in for a scanned page |
12-mixed-worst-case.pdf | Title, two-column paragraph and table on one page |
13-heading-thresholds.pdf | One line at each of six font sizes around the usual thresholds |
14a-row-major.pdf | Identical geometry to 14b, text written row by row |
14b-column-major.pdf | Identical geometry to 14a, text written column by column |
15-mixed-first-page-text.pdf | Page 1 has text, page 2 is a scan |
16-mixed-middle-page-text.pdf | Scan, text, scan |
17-compressed-stream.pdf | A FlateDecode-compressed content stream |
Two rules apply to all of them. Each file isolates one behaviour, so a surprising result points at a single cause. And where two files are compared they differ in one variable only — 14a and 14b are the clearest case: same page size, same font, same coordinates, the same four left-column and four right-column strings, the same nine text items in total. The only difference is the order those text operators appear in the content stream.
The scripts
make_pdfs.py— regenerates the 18 PDFs. Byte-identical when run twice. Standard library only.verify.py— checks the structural invariants a reader depends on: thatstartxrefpoints at the xref table, that every xref entry points at a real object header, that declared/Lengthmatches the actual stream length, that every page's/Contentsresolves. Then it pulls the text back out with a regular expression. If it passes, the files are well-formed enough for a conforming parser.results.json— the verbatim output of one converter against every file, with the status string it reported.
The pair worth looking at first
14a-row-major.pdf and 14b-column-major.pdf are visually indistinguishable. Open them side by side and you cannot tell which is which.
Left column, line one.␠Right column, line one.Columns joined with a space.
Left column, line one.Right column, line one.Columns joined with no separating space.
␠ marks the existing space for visibility. Both recorded outputs merge two columns; this is a failure illustration, not a corrected result. Inspect the files and results.json.
Yet the measured output differs. Both files begin with a heading naming which one they are, and then the same four lines of body text. On the row-major file, the first body line came out as:
Left column, line one. Right column, line one.
and on the column-major file:
Left column, line one.Right column, line one.
No space at all between the columns. Both are wrong, but the second is far more dangerous, because line one.Right column reads as a typo rather than as two columns that have been fused together. Nobody proofreading the output catches that.
The cause is not the geometry. It is that PDF.js emits the spacing between two text runs as its own synthetic text item when the runs are written consecutively, and omits it otherwise. Same page, same coordinates, different outcome — a property of the file's internal byte order that no amount of looking at the page will reveal. This is documented in more detail in the two-column guide.
How to use it
git clone https://github.com/GordonGuoJin/pdf-to-markdown-test-corpus
cd pdf-to-markdown-test-corpus
python3 verify.py
Then open your converter, drop in the files from tests/, and compare what you get against results.json.
Your converter's results will differ from the measured ones. That is the point — the value is in having a fixed set of inputs you can compare over time.
How to cite it
The archived copy has the DOI 10.5281/zenodo.22884428, which resolves at doi.org/10.5281/zenodo.22884428. The record itself is at zenodo.org/records/22884428.
The citation string, as Zenodo issues it:
BayKolm (2026). PDF-to-Markdown Test Corpus [Data set]. Zenodo. https://doi.org/10.5281/zenodo.22884428
If you are referring to the corpus as a whole rather than to this specific release, the concept DOI is 10.5281/zenodo.22884427 — that one always resolves to the most recent version. The record DOI above is pinned to the version published on 22 September 2026.
Licence
The PDFs and scripts are released under CC0 1.0 — public domain. Use them for anything, including commercially, with no attribution required. They are synthetic documents built for testing and contain no third-party content.
Related guides
- Two-column PDF to Markdown — the failure that produces plausible-looking output with no warning attached, and the one the
14a/14bpair exists to expose. - Scanned PDF to Markdown — why a scan cannot be converted, and what happens when only some pages are scans.
- Why headings disappear when you convert a PDF to Markdown — what happens to document structure, with the measured thresholds.
- How to convert a PDF to Markdown, and what actually breaks — the overview.
- PDF to Markdown regression test — the fixed corpus and recorded output you re-run after any change, including the failures a visual check misses.