Lightweight, model-free PDF-to-Markdown conversion
Project description
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.
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dumb_pdf2md-0.1.1.tar.gz.
File metadata
- Download URL: dumb_pdf2md-0.1.1.tar.gz
- Upload date:
- Size: 288.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c62df682df94dab15e5d53ce0cac4b9cd50f920cb8464358627538fc360289c6
|
|
| MD5 |
6ebf9408a7433f4cc1f087ce36a23d67
|
|
| BLAKE2b-256 |
050aef077ffcbc47913445eb14ee8e712ff66f6207bc469bfeeaf4e7fa52e9dc
|
File details
Details for the file dumb_pdf2md-0.1.1-py3-none-any.whl.
File metadata
- Download URL: dumb_pdf2md-0.1.1-py3-none-any.whl
- Upload date:
- Size: 26.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.7.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c4004eef4fcccfd2b9bab7a75023f3bc87fc162f80ce848f74ae7882ec7f019
|
|
| MD5 |
5b080f26674d1b769d4ae69249a0bdf9
|
|
| BLAKE2b-256 |
e121f3adc5e50b6930caf82362d649a90861b5d08fbfb7f7d65f333e28857b03
|