Skip to main content

Pure-Rust PDF extraction that distills documents into clean, LLM-ready HTML — for LLMs and RAG, built on lopdf

Project description

distillPDF

Turn any PDF into clean, LLM-ready HTML — structure-aware, pure-Rust, MIT-licensed.

PyPI Python versions License: MIT CI Built with Rust

distillpdf reads a PDF and reconstructs its structure — reading order, headings, paragraphs, lists, tables, and figures — then emits compact, semantic HTML (or plain text) ready to feed to an LLM or a RAG pipeline. No styling noise, no layout junk: just the content a model needs.

It's built on lopdf and shipped to Python via PyO3 + maturin as a small, self-contained wheel — a lightweight, permissively licensed alternative to AGPL/heavyweight extractors (PyMuPDF, pdfminer, Unstructured), with no system dependencies and no Python runtime deps.

🧪 Early release (0.0.3) — testers wanted. The API is small and may still change. If you have PDFs that come out wrong, please open an issue with the file (or a description) — real-world documents are exactly what this needs to get better.

Install

pip install distillpdf

Prebuilt wheels; no compiler or system libraries required. Installing also puts a distillpdf command on your PATH.

Command line

Convert a PDF to clean HTML in one command:

distillpdf paper.pdf                  # HTML to stdout
distillpdf paper.pdf -o paper.html    # ...or to a file
distillpdf *.pdf -o out/              # batch: out/<name>.html per input

distillpdf paper.pdf --markdown       # Markdown instead of HTML
distillpdf paper.pdf -o paper.md      # Markdown (inferred from the .md extension)
distillpdf *.pdf --markdown -o out/   # batch Markdown; figures to out/img/
distillpdf paper.pdf --embed-images   # Markdown with inline data: URIs (self-contained)

distillpdf paper.pdf --mode page      # page-first HTML (default is section-first)
distillpdf paper.pdf --no-images      # placeholders, no base64 bytes
distillpdf paper.pdf --no-toc         # omit the table-of-contents nav
distillpdf paper.pdf --text           # plain text instead of HTML
distillpdf paper.pdf --toc            # print the table of contents
distillpdf paper.pdf --section abstract

(Also available as python -m distillpdf.)

Quickstart

import distillpdf

doc = distillpdf.open("paper.pdf")        # or distillpdf.from_bytes(data)

html     = doc.to_html()                  # clean, semantic HTML for an LLM
md       = doc.to_markdown()              # ...or Markdown (built from the same HTML)

# write straight to a file (like pandas .to_csv) — returns the path written:
doc.to_html("out.html")                   # ...to a specific path
doc.to_html(outputfile=True)              # ...or <source>.html next to the PDF
doc.to_markdown("out.md")                 # .md + an img/ folder of extracted figures
doc.to_markdown(outputfile=True)          # <source>.md (handy for bulk runs)

# rendering options work the same on both:
doc.to_html(mode="page", toc=False)
text     = doc.extract_text()             # plain text, in reading order
toc      = doc.toc()                      # [(level, title, page, anchor_id), ...]
abstract = doc.section("abstract")        # targeted section extraction

Markdown

to_markdown() is a transform of the very HTML to_html() produces — so every processor improvement (clipping, heading detection, tables, front-matter) flows into Markdown automatically, with no second renderer to keep in sync.

doc.to_markdown()                         # string; images are caption-only placeholders
doc.to_markdown("paper.md")               # writes paper.md + paper's img/fig_NN_slug.ext
doc.to_markdown("paper.md", embed_images=True)   # self-contained: inline data: URIs
doc.to_markdown(images=False)             # drop images entirely (placeholders)

When writing to a file, figures are extracted next to it as img/fig_NN_<slug>.ext (vector figures as self-contained .svg) and referenced relatively. Returning a string has no folder to write into, so images fall back to placeholders unless embed_images=True.

Output modes

By default, logical sections are first-order: every heading becomes its own nested <section id="sec-…">, so you can pull a whole section as one block (great for RAG / LLM chunking), and page numbers are dropped.

distillpdf.open("paper.pdf").to_html()
# <section id="sec-abstract"><h2>Abstract</h2><p>…</p></section>

distillpdf.open("paper.pdf").section("methods")   # → the <section id="sec-methods"> block

Pass mode="page" for the page-faithful structure instead — each page wrapped in <section data-page="N" id="page-N">, with page numbers in the TOC:

distillpdf.open("paper.pdf").to_html(mode="page")

Want compact, text-only output? Drop the inline image bytes — each embedded image becomes a lightweight <image N> placeholder (captions and figure anchors are kept):

distillpdf.open("paper.pdf").to_html(images=False)
# <figure id="fig-1"><image 1><figcaption>…</figcaption></figure>

Pass toc=False to skip the auto table-of-contents <nav> (heading anchors are still emitted, so #section links and doc.section(...) keep working):

distillpdf.open("paper.pdf").to_html(toc=False)

Rendering options

open() only loads the PDF; the rendering options live on to_html() and to_markdown() (and mode on toc()/section()), since that's where the content is actually extracted:

Option Default Effect
path= None a file (or directory) to write to; None returns the string. Returns the path written
outputfile= False True writes <source>.html / <source>.md next to the PDF (no path needed) — for bulk runs
mode= "section" "page" wraps each page in <section data-page="N"> and numbers TOC entries; the default groups content into nested <section id="sec-…"> and drops page info
images= True False swaps inline base64 images for <image N> placeholders (captions + #fig-N anchors kept)
embed_images= False to_markdown() only: True inlines images as data: URIs instead of an img/ folder
toc= True False omits the <nav> table of contents (section/heading anchors still emitted)

Raw pieces

Need the structured data instead of HTML?

doc.extract_tables()   # cell grids (handles multi-level / colspan headers)
doc.extract_images()   # embedded images, with raw bytes
doc.extract_links()    # hyperlinks with targets
doc.extract_fonts()    # font inventory
doc.page_count()       # number of pages

Why distillPDF

  • Structure, not just text. Two-column reading order, multi-level table headers mapped onto a single grid (colspan), vector figures transcoded to inline SVG (including rotated axis labels), an auto-generated table of contents, and named section extraction (doc.section("methods")).
  • LLM-ready output. Lean, class-free HTML — semantic markup a model can read directly, with anchor ids so toc() entries link straight into the document.
  • Small & permissive. Pure Rust on lopdf, MIT-licensed, no system dependencies, no Python runtime dependencies. Drops into any pipeline without license headaches.
  • Fast. Native Rust extraction with a release build tuned for speed (LTO, single codegen unit).

Scope

In scope: text, table, image, and font extraction, plus an HTML/markdown output layer for RAG and LLM ingestion.

Out of scope (for now): page rendering, PDF generation, OCR.

Comparison

distillPDF PyMuPDF pdfminer.six Unstructured
License MIT AGPL / commercial MIT Apache (heavy deps)
Structure-aware HTML partial
System deps none none none many
Implementation Rust C Python Python

Contributing & feedback

This is a young project and feedback is the fastest way to improve it. The most useful things you can do:

  1. Try it on your PDFs and tell me where the output is wrong — open an issue.
  2. Star the repo if it's useful, so others can find it.
  3. PRs welcome — see the development notes below.

Development

The test suite lives in tests/ (pytest) and runs on CI. It needs only distillpdf installed. CI runs entirely on data we own — a self-contained demo PDF (tests/demo/, end-to-end structure check) and a synthetic table corpus (tests/corpus_tables/). The third-party PDF corpora (tests/corpus*/) are gitignored, so their tests self-skip on a fresh clone and run only when the corpora are present locally for deeper coverage.

Build from source with maturin:

git clone https://github.com/kkollsga/distillpdf
cd distillpdf
maturin develop --release    # build + install into the current venv
bash tests/run.sh            # build distillpdf + run pytest
pytest tests/ -q             # or just run the tests against an installed build

License

MIT — see LICENSE. Use it anywhere, including commercial and closed-source projects.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

distillpdf-0.0.14-cp38-abi3-win_amd64.whl (1.5 MB view details)

Uploaded CPython 3.8+Windows x86-64

distillpdf-0.0.14-cp38-abi3-manylinux_2_34_x86_64.whl (1.5 MB view details)

Uploaded CPython 3.8+manylinux: glibc 2.34+ x86-64

distillpdf-0.0.14-cp38-abi3-macosx_11_0_arm64.whl (1.4 MB view details)

Uploaded CPython 3.8+macOS 11.0+ ARM64

File details

Details for the file distillpdf-0.0.14-cp38-abi3-win_amd64.whl.

File metadata

  • Download URL: distillpdf-0.0.14-cp38-abi3-win_amd64.whl
  • Upload date:
  • Size: 1.5 MB
  • Tags: CPython 3.8+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for distillpdf-0.0.14-cp38-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 2a5ee43fb1aaed1fb7ef74ecfcb7305ec14065c6707fa93048e00c0d584b6f40
MD5 55f6b4091f8715a461f5e8683592e551
BLAKE2b-256 e1dcd6e0105f9d515afb5224b2d6c9dd93c6c048e7c9688f7afb2522fa4c2269

See more details on using hashes here.

Provenance

The following attestation bundles were made for distillpdf-0.0.14-cp38-abi3-win_amd64.whl:

Publisher: publish.yml on kkollsga/distillpdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file distillpdf-0.0.14-cp38-abi3-manylinux_2_34_x86_64.whl.

File metadata

File hashes

Hashes for distillpdf-0.0.14-cp38-abi3-manylinux_2_34_x86_64.whl
Algorithm Hash digest
SHA256 90b7542bed530d68884a5c98bfc0e898ffb4c06229e4618f3723b75eca51da88
MD5 18ec6ac6a8553d5931cb6c2830c3dab4
BLAKE2b-256 860c15788d9c9636e947cc59a17ad85760549fa7585066302f85536183f4b394

See more details on using hashes here.

Provenance

The following attestation bundles were made for distillpdf-0.0.14-cp38-abi3-manylinux_2_34_x86_64.whl:

Publisher: publish.yml on kkollsga/distillpdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file distillpdf-0.0.14-cp38-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for distillpdf-0.0.14-cp38-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 b456208b922e1b1e32c87692896739ea5483e78451918c6eae22b6b15c8a4e31
MD5 d6c2bffa12f964977cbd84cca9835336
BLAKE2b-256 912a22226f76d3bd8b9663fc981142c0ed91a0148772e5be13f9dcbb43c064d5

See more details on using hashes here.

Provenance

The following attestation bundles were made for distillpdf-0.0.14-cp38-abi3-macosx_11_0_arm64.whl:

Publisher: publish.yml on kkollsga/distillpdf

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page