Dumb PDF2MD
dumb-pdf2md is a lightweight, typed Python library that converts text-based PDF documents into structured, LLM-friendly Markdown. It reconstructs document structure from the PDF text layer, geometry, and typography without layout models, OCR models, GPU resources, or external services.
Installation
With pip:
python -m pip install dumb-pdf2md
With uv:
uv add dumb-pdf2md
Requires Python 3.10 or later.
Benchmarks
READoc selectable-text holdout (12 real-world PDFs, 161 pages):
| Metric | dumb-pdf2md | MarkItDown 0.1.6 | Docling 2.115.0 | PyMuPDF4LLM 1.28.0 | pdftext 0.7.1 | pdfplumber 0.11.10 |
|---|---|---|---|---|---|---|
| Text token F1 | 0.852 | 0.756 | 0.899 | 0.863 | 0.893 | 0.691 |
| Reading-order bigram F1 | 0.734 | 0.627 | 0.831 | 0.793 | 0.818 | 0.562 |
| Table-content F1 (6-PDF subset) | 0.452 | 0.326 | 0.890 | 0.685 | N/A | N/A |
| Conversion time | 188.7 ms/page | 53.5 ms/page | 917.1 ms/page | 143.7 ms/page | 24.2 ms/page | 53.6 ms/page |
| Peak RSS | 248.5 MiB | 115.1 MiB | 4.44 GiB | 667.7 MiB | 122.6 MiB | 202.6 MiB |
| Uses OCR model | No | No | Yes | No | No | No |
| Uses layout model | No | No | Yes | Yes | No | No |
| Installed size with dependencies | 112.5 MiB | 168.8 MiB | 5.25 GiB | 233.2 MiB | 70.0 MiB | 50.2 MiB |
| License | Apache-2.0 | MIT | MIT | AGPL-3.0 + PolyForm NC / commercial | Apache-2.0 | MIT |
pdftext and pdfplumber are the two base extraction libraries used by
dumb-pdf2md, not end-to-end Markdown converters. Their columns score native
text output without dumb-pdf2md's structure analysis or Markdown rendering,
so table-content F1 is not applicable to those baseline modes. pdfplumber
does provide table-finding APIs; dumb-pdf2md combines their candidates with
pdftext geometry, custom aligned-table detection, cell normalization,
deduplication, and Markdown rendering.
Model rows describe the exact benchmark configuration. Docling ran on CPU with
EasyOCR and its layout/table models enabled, without forcing full-page OCR on
pages that already contain text. PyMuPDF4LLM uses its installed
pymupdf-layout dependency. MarkItDown's optional OCR plugin and cloud
integrations were not enabled. The PyMuPDF4LLM license row includes the license
of its layout dependency.
Quality metrics are higher-is-better; time and memory are lower-is-better. Performance uses three isolated process runs per tool, excludes import time, and reports median conversion time and maximum peak RSS.
Installed size is the site-packages footprint in a clean Python 3.13
environment after installing the package and all transitive dependencies. It
excludes Python itself, package caches, and models downloaded at runtime.
Performance host: Intel Xeon Platinum 8488C, 16 vCPUs (8 cores/16 threads), 64 GB RAM, Ubuntu 24.04.4 LTS (x86_64), and CPython 3.13.3. Results are directly comparable only on the same environment.
0.1.2 release benchmark
On a fixed 17-PDF service corpus, five isolated measured runs on the same host showed the following change from 0.1.1 to 0.1.2:
| Metric | 0.1.1 | 0.1.2 | Change |
|---|---|---|---|
| Median aggregate conversion time | 24.668951 s | 21.363090 s | 13.40% faster |
| Maximum peak RSS | 190,406,656 bytes | 190,771,200 bytes | +0.19% |
Sixteen of 17 document output hashes were unchanged. The only difference was
an approved correctness fix in evidence.pdf: numbered references on page 8
are no longer misclassified as a 63-row table, while the real table on page 5
is preserved. A 16-worker, 24-conversion stress run was deterministic and
reported no native extraction failures. See the
reproduction details.
READoc was not used for parser tuning. See benchmark reproduction, the base-engine results, the MarkItDown results, the Docling results, the performance results, and the evaluation methodology.
Quality-resource map
quadrantChart
title Overall extraction quality versus runtime memory
x-axis Higher peak RSS --> Lower peak RSS
y-axis Lower composite F1 --> Higher composite F1
quadrant-1 Best quality-resource balance
quadrant-2 High quality, high resource use
quadrant-3 High resource use, weaker structure
quadrant-4 Lightweight, weaker structure
"MarkItDown": [0.96, 0.570]
"pdfplumber": [0.82, 0.418]
"★ Dumb PDF2MD": [0.77, 0.679]
"PyMuPDF4LLM": [0.51, 0.781]
"Docling": [0.02, 0.873]
Licenses:
- ★ Dumb PDF2MD: Apache-2.0
- MarkItDown: MIT
- pdfplumber: MIT
- PyMuPDF4LLM: AGPL-3.0 + PolyForm Noncommercial / commercial
- Docling: MIT
The horizontal coordinates reverse and normalize log10(peak RSS), so lower
memory use moves a tool to the right while Docling's 4.44 GiB result does not
compress the other tools at the origin. They show relative resource tiers, not
raw memory values; the exact measurements are in the table above. Composite F1
is the equal-weight mean of text-token, reading-order-bigram, and table-content
F1; the table component uses the six-PDF subset with ground-truth tables. For
this visualization only, pdfplumber receives zero for the table component
because its benchmark mode does not produce Markdown tables; the source table
keeps this result as N/A because no table-quality measurement was made.
Positioning
dumb-pdf2md fills the gap between raw text extraction and model-based document understanding:
- It adds headings, lists, multi-column reading order, table reconstruction, deduplication, and Markdown rendering on top of lightweight PDF engines.
- It has higher text, reading-order, and table F1 than MarkItDown on this holdout, while remaining fully local and model-free.
- It uses substantially less memory and disk space than Docling, and less memory than PyMuPDF4LLM.
- Its Apache-2.0 runtime has no model-weight license, GPU requirement, model download, or external service dependency.
The tradeoff is deliberate: Docling is more accurate on complex structures and supports scanned PDFs, while raw extractors are faster when structure is not needed. dumb-pdf2md targets selectable-text PDFs where useful Markdown structure matters but OCR/model deployment cost does not make sense.
Features
- Accepts file paths, bytes-like objects, and readable binary streams.
- Runs locally on CPU without downloading layout or OCR models.
- Preserves headings, paragraphs, lists, footers, and reading order.
- Converts ruled and structurally aligned tables to Markdown tables.
- Handles common multi-column layouts and CJK text.
- Produces clean Markdown suitable for LLM context, indexing, and RAG pipelines.
- Returns page counts and non-fatal diagnostics alongside Markdown.
- Provides immutable result models and a documented exception hierarchy.
- Supports concurrent callers without taking ownership of caller-provided streams.
dumb-pdf2md is designed for PDFs that contain extractable text. Scanned or image-only PDFs require OCR before conversion. The library does not currently extract images, preserve inline text styling, or expose page-level bounding boxes and metadata.
Usage
Use parse when you need conversion metadata:
from dumb_pdf2md import parse
result = parse("report.pdf")
print(result.markdown)
print(f"Pages: {result.page_count}")
for diagnostic in result.diagnostics:
print(diagnostic.code, diagnostic.page, diagnostic.message)
Use to_markdown when you only need the generated Markdown:
from dumb_pdf2md import to_markdown
markdown = to_markdown("report.pdf")
Both functions also accept PDF data and readable binary streams:
from io import BytesIO
from dumb_pdf2md import parse
with open("report.pdf", "rb") as file:
pdf_data = file.read()
from_bytes = parse(pdf_data)
stream = BytesIO(pdf_data)
from_stream = parse(stream)
assert not stream.closed
Caller-owned streams remain open after conversion.
Parser Options
Document heuristics are enabled by default and can be controlled with
ParserOptions:
from dumb_pdf2md import ParserOptions, parse
result = parse(
"report.pdf",
options=ParserOptions(
promote_bold_headings=False,
detect_columns=False,
),
)
promote_bold_headingspromotes qualifying short bold lines to headings.detect_columnsdetects common column layouts and restores reading order.
Error Handling
All public conversion errors inherit from PdfParserError:
from dumb_pdf2md import PdfInputError, PdfParserError, parse
try:
result = parse("report.pdf")
except PdfInputError as error:
print(f"Invalid PDF input: {error}")
except PdfParserError as error:
print(f"PDF conversion failed: {error}")
PdfInputError reports invalid, empty, missing, or unreadable input.
PdfConversionError reports extraction failures or an empty conversion
result. Wrapped failures retain the original exception in __cause__.
Development
Run the complete release checkpoint:
scripts/release-check.sh
Or run individual checks:
uv run ruff check .
uv run mypy
uv run pytest
uv build
The test suite covers the public API, parsing algorithms, resource cleanup, concurrent use, distribution artifacts, and a reproducible golden PDF corpus.
Documentation
- API reference
- Architecture
- Contributor guide
- Benchmark reproduction
- Release verification
- Changelog
- Third-party notices
License
Licensed under the Apache License 2.0.
Metadata
Release files for dumb-pdf2md 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dumb_pdf2md-0.1.2.tar.gz | 297.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dumb_pdf2md-0.1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 326.0 kB
Release files / dumb_pdf2md-0.1.2.tar.gz
| Download URL | dumb_pdf2md-0.1.2.tar.gz |
|---|---|
| Size | 297.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
646a03eecfb36d0e7ceeb123c18e718d0b48a9092dd76b021cea1f604059ef94
|
|
BLAKE2b-256 checksum How to use checksums |
4a277e10d1142beb938f72850d0dc8f16a7a485bb53942937a8408a68453c23e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.2
|
Release files / dumb_pdf2md-0.1.2-py3-none-any.whl
| Download URL | dumb_pdf2md-0.1.2-py3-none-any.whl |
|---|---|
| Size | 28.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6d1f48fc6ddc8086e7be31a4a690c3bbf97ee89ca6927dfca0e530ec2c04ae0b
|
|
BLAKE2b-256 checksum How to use checksums |
e8b32155cd990ca9cf3968fe2a931dbc5f8b55550efefd628631f26e170cec61
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.7.2
|