DEV Community

Cover image for I benchmarked 3 PDF-to-Markdown converters on 5 public PDFs, including the one I built. Here is where it loses
Giacomo
Giacomo

Posted on Originally published at cleanmd.dev

I benchmarked 3 PDF-to-Markdown converters on 5 public PDFs, including the one I built. Here is where it loses

Every PDF-to-Markdown tool claims it "preserves the structure". I build one of them (CleanMD), and I started it because a DeepSeek paper I wanted in my notes came out of every tool I tried as soup. So I had an obvious bias and an obvious question: how would I know if that claim were false about mine?

I ended up writing a small benchmark. Five public PDFs from five genres, three tools with default settings, and a ground truth that comes from the documents themselves, never from any tool's output. Here is what I found, losses first.

The setup

Documents (all public, none picked after seeing results):

The five public PDFs used in the benchmark (RFC 9110, BERT, Think Python, a Supreme Court opinion, NIST CSF 2.0) with page counts and why each one is hard to convert

Document Why it is hard
RFC 9110 HTTP Semantics (194 pp.) 291 numbered sections, ABNF grammar, ASCII diagrams, running footer
BERT (arXiv 1810.04805, 16 pp.) two-column paper with result tables and captions right next to them
Think Python 2e (292 pp.) technical book: table of contents, 500+ code listings
Loper Bright v. Raimondo (SCOTUS, 114 pp.) legal opinion: parts I/II and sub-parts A/B are single centered letters at body size
NIST CSF 2.0 (32 pp.) standard with appendices and tables with multi-line cells

Tools: MarkItDown 0.1.7 (Microsoft), pymupdf4llm 1.28.2 (Artifex), and CleanMD 0.93.0 (mine, the same engine that runs in the browser, executed in Node). Pandoc is not in the table because it cannot read PDF at all (PDF is an output format for it): return code 21 on all five files.

Ground truth: RFC sections from the table of contents of the official rfc9110.txt; Think Python's 126 entries from the book's own ToC; BERT's 29 numbered headings from the lines set in the Medium weight; NIST's 8 entries from its ToC. For the court opinion there is no ToC, so the 31 part markers were defined geometrically (a single token, centered, at body size), and that definition is close to the heuristic my own engine uses, which I flag on the page. Weigh that document accordingly.

Metrics: sections recognised as headings (and at a depth consistent with the numbering), fenced code blocks, tables and "prose cells" (a table cell holding a sentence = a fake or contaminated table), plus a few document-specific checks like "is the collected ABNF one block?".

Where CleanMD loses

The 'Where CleanMD loses' section of the benchmark page: RFC section recall on the first run, fence count on Think Python, and prose cells on NIST, with the competitor ahead in each row

  • RFC 9110 section recall, first run: pymupdf4llm 290/291, CleanMD 280/291. Eleven bold, body-size headings (8.8.2 Last-Modified, 13.1.3 If-Modified-Since, 15.5.10 409 Conflict…) came out of CleanMD as plain paragraphs. The cause turned out to be embarrassing and specific: pdf.js splits a line into two font ids when a glyph (the hyphen, here) comes from a second subset of the same bold font, and my detector only looked at lines with one uniform font. Fixed the same day; the re-run is 291/291. I am keeping the first number in the article and on the page, because a benchmark that only shows the after is marketing.
  • Fence count on Think Python: pymupdf4llm 657, CleanMD 569. With a caveat: 328 of pymupdf4llm's fences are single-line fences wrapping inline code words. The operator listing that opens §5.2 (x != y … x >= y) is a fence in CleanMD and not there.
  • Prose cells on NIST: MarkItDown 0, CleanMD 23. Most of CleanMD's are the Tier table in Appendix B, whose cells are paragraphs by design (it now comes out as one table per page instead of one block per Tier, so the metric counts every wrapped line); MarkItDown's tables are cleaner on that document. It also emits zero headings on it, so I wouldn't take that trade, but the row is there.

Where it wins

  • Code survives. RFC 9110: 158 fenced blocks vs 0 and 0. The ABNF grammar, the message diagrams and the examples come out of the other two as prose; the collected ABNF appendix is one block only in CleanMD.
  • Running footers are gone. "Fielding, et al. Standards Track Page N" left in the text: 0 vs 187 (MarkItDown) vs 194 (pymupdf4llm).
  • Hierarchy has depth. Think Python: 122 of 126 sections at the right depth for CleanMD; pymupdf4llm finds 113 but emits all of them as H4, so the outline is flat. On the court opinion, 28 of 31 part markers become headings (I → H2, A → H3, 1 → H4); pymupdf4llm gets 18 and promotes the running page header to an H3 on 80 pages.
  • No fake tables. MarkItDown turns BERT into 134 pseudo-tables (the title becomes a 9-column table) and Think Python's code listings into 683 tables.

The numbers

CleanMD MarkItDown pymupdf4llm
RFC 9110: sections / 291 (right depth) 291 (291) · first run 280 0 290 (275)
RFC 9110: fences / ABNF one block 158 / yes 0 / no 0 / no
RFC 9110: footer lines in text 0 187 194
BERT: sections / 29 26 0 26
BERT: tables (prose cells) 11 (0) 134 (0) 9 (0)
Think Python: sections / 126 (right depth) 122 (122) 0 113 (0)
Think Python: fences (single-line) 569 (52) 0 657 (328)
SCOTUS: part markers as headings / 31 28 0 18
SCOTUS: running headers promoted 0 0 80
NIST: sections / 8 8 0 8
Seconds, RFC 9110 (same laptop) 1.3 5.5 10.9

What this does not tell you

Five documents are five documents. Nothing here is scanned (all five have a text layer), nothing measures reading order inside dense three-column layouts (where all three tools struggle), nothing measures math or prose quality. It shows failure modes, not a universal ranking.

Reproduce it

The page has the full tables with the best value per row highlighted and a kit you can download: run.sh (downloads the PDFs, installs the two open-source tools in a venv, builds the ground truth, scores everything in about two minutes), the ground truth, the results, and the raw Markdown CleanMD produced, so every sentence above can be checked against the actual output.

https://cleanmd.dev/benchmarks/pdf-to-markdown

If you know a public PDF that breaks all three, I want it.

Top comments (0)