โ PDF: all libraries ยท Reader board
pypdf vs pdfminer.six: which one reads more real-world PDF files?
pypdf 7.0.0 and pdfminer.six 20260107 on the same 1,077 odd real-world files from Mozilla pdf.js test PDFs (bug reports, fuzzers, broken exporters). Measured, not opinion; re-tested by itself on every new PyPI release. Updated 06 November 2026.
pypdf got text from more files: 664 vs 649 of 1,077. pypdf broke (error, hang or crash) on 6 files that pdfminer.six read; pdfminer.six broke on 7 files that pypdf read.
| Library | Text from | ๐ด Fails where the other reads | ๐ก Empty where the other reads | โฑ Hangs or crashes | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|
| pypdf 7.0.0 | 664 | 6 | 0 | 0 | 4 | 145.8 |
| pdfminer.six 20260107 | 649 | 7 | 14 | 0 | 4 | 179.5 |
Files only pypdf reads (21)
| Brotli-Prototype-FileA.pdf | pdfminer.six: PDFNotImplementedError: Unsupported filter: /'BrotliDecode' |
| bug1978317.pdf | pdfminer.six: no text |
| bug920426.pdf | pdfminer.six: no text |
| crypt_filters.pdf | pdfminer.six: PDFEncryptionError: Unsupported crypt filter: param={'Filter': /'Standard', 'V': 4, 'R': 4, 'Length': 128, 'P': -4, 'O': b'Vo\xa8s\xee3\xc7\x97\xcd;\x90O\xda\xd |
| franz_2.pdf | pdfminer.six: TypeError: unsupported operand type(s) for +: 'int' and 'PDFObjRef' |
| issue11549_reduced.pdf | pdfminer.six: PSSyntaxError: Invalid dictionary construct: [/'BaseFont', /'AASGAA+Arial,Unicode', /b'MS', /'DescendantFonts', [<PDFObjRef:56>], /'Encoding', /'Identity-H', /' |
| issue1293r.pdf | pdfminer.six: no text |
| issue18099_reduced.pdf | pdfminer.six: no text |
| issue18117.pdf | pdfminer.six: no text |
| issue22011.pdf | pdfminer.six: PSSyntaxError: Invalid dictionary construct: [/'Type', /'Page', /'Parent', <PDFObjRef:2>, /'MediaBox', [0, 0, 595.276, 841.89], /'Resources', {'Font': {'F1': <P |
| issue2931.pdf | pdfminer.six: no text |
| issue3323.pdf | pdfminer.six: no text |
| issue4402_reduced.pdf | pdfminer.six: no text |
| issue6108.pdf | pdfminer.six: Error: Odd-length string |
| issue7665.pdf | pdfminer.six: TypeError: 'PDFObjRef' object is not subscriptable |
| issue7901.pdf | pdfminer.six: no text |
| issue9418.pdf | pdfminer.six: no text |
| issue9534_reduced.pdf | pdfminer.six: no text |
| issue9915_reduced.pdf | pdfminer.six: no text |
| perf_text_cmap_mixed.pdf | pdfminer.six: no text |
| prefilled_f1040.pdf | pdfminer.six: no text |
Files only pdfminer.six reads (6)
| Pages-tree-refs.pdf | pypdf: PdfReadError: Detected cyclic page references. |
| bomb_giant.pdf | pypdf: DependencyError: jbig2dec binary is not available. |
| issue10175.pdf | pypdf: DeprecationError: The filter name /A85 is deprecated and has been removed in pypdf 7.0.0. Use /ASCII85Decode instead. |
| issue15893_reduced.pdf | pypdf: FileNotDecryptedError: File has not been decrypted |
| issue19800.pdf | pypdf: PdfReadError: startxref not found |
| issue6069.pdf | pypdf: PdfStreamError: Stream has ended unexpectedly |
Don't want to run it yourself? I also run PDF & Document to Markdown as a pay-per-use API on Apify (it uses pdfplumber and pypdfium2 for PDFs, markitdown for Office files). Disclosure: that tool is mine; the rankings above come only from the measurements.
What this does not tell you
This compares not breaking: whether each library gets any text out of each file. It does not rank text quality (reading order, tables, headings), and speed is one GitHub-hosted runner, default settings, no OCR switched on. If you need structure for RAG, also look at the output on your own files.
How this is measured
- Each library runs a short recipe from its own docs (board/targets.py):
open the file, get all text. Installed with the extras the docs recommend for reading
(
pypdf[full]: AES decryption, Brotli), nothing else. One worker process per CPU; per file 60 s / 120 s / 180 s / 300 s / 600 s at most (the any-document table allows longer: some tools load models), killed above the memory limit. - Fails where another reads: the library raised an error, hung or crashed on a file from which another library in the same table got text. Files that every library fails on (encrypted, truncated, fuzzed) are not counted against anyone.
- Empty where another reads: no error, but no text. This is often the recipe, not the library: for example
python-docx's paragraphs+tables recipe skips headers, text boxes and nested tables. Check the file list. - Any document โ text: a file type the tool's docs don't list, or that the tool itself rejects as unsupported, counts as "not supported", not as a failure (lists in board/libs.json). Returning a file's raw RTF source counts as empty, for every tool. Each tool runs with default settings: no OCR switched on by hand.
- OCR: each image comes with the human-written ground-truth text published with its dataset. Words found is the share of its words that appear anywhere in the engine's output (reading order doesn't matter); an image that fails or comes back empty counts as 0. The ground truth of receipts and document pages covers only their annotated text, so a character error rate would punish engines for reading more than was annotated; it is in the raw results but not ranked. English models, CPU, default settings.
- Speech: each clip comes with its dataset's reference transcript. Both texts go through Whisper's English text normalizer (case, punctuation, "twenty" = "20"); word error rate is all substituted, missing and extra words over all reference words, the usual corpus-level WER. A clip that fails or comes back empty counts all its words as errors. The three Whisper implementations use the same base.en model; each other engine its documented default model. English, CPU, default decoding settings.
- Language detection: each text's language is known from Tatoeba. An answer counts as correct if the library's code matches after mapping variants to ISO 639-1 (zh-cn โ zh, nb โ no, iw โ he). Close languages count as different: Croatian detected as Bosnian or Serbian, or Norwegian as Danish, is wrong. One call per text with default settings and all languages enabled.
- PDF โ Markdown: pages from AI2's olmOCR-bench (ODC-BY), scored with its own checks (vendored, Apache-2.0): a sentence must be present, a page header or footer absent, two passages in the right order, a table cell next to the right neighbour. Checks passed is all passed checks over all checks; a page that fails or comes back empty passes none. Math pages are left out (their checks need a browser to render formulas). Default settings: whatever OCR a tool does on its own, it does.
- Text quality (order, tables, layout) is not ranked in the other tables; they are about not breaking. The raw results have every output's size and hash.
- Same files for every version: the test collections are pinned to one commit. GitHub-hosted runner, so times are comparable within one table, roughly.