pdfspine
An Apache-2.0-licensed, pure-Rust reimplementation of PyMuPDF (fitz), with PyO3 Python bindings.
🦴 Part of the
spinefamily — framework-free backend engines, each the spine of a domain: zero framework lock-in, Protocol-ized seams, offline-capable. pdfspine is the PDF spine (this repo); ragspine is the RAG spine (deterministic dual-channel retrieval + agent orchestration).🤖 For AI agents / LLMs: before using this library, read
llms.txt(concise index) andpython/pdfspine/_llms/docs/(full API / recipes / gotchas); afterpip installthey ship atsite-packages/pdfspine/_llms/.
Status: alpha / pre-1.0, but the core is feature-complete. pdfspine can already parse/repair/decrypt PDFs, extract text & tables, search, edit / merge / split / save (incl. byte-exact incremental), encrypt, annotate, fill & flatten forms, redact (destructively), open image files as documents, render pages to images, and OCR (Tesseract + a pure-Rust PaddleOCR engine, stronger on CJK). 89.3% (687 / 769) of the PyMuPDF 1.24 public API is implemented and tested (climbing), with 1,702 Rust tests + 814 Python tests passing in the 0.7.0 release gate. In the dated 58-document benchmark, its aggregate mean text scores trail fitz by 0.2–1.4 percentage points (and it beats fitz on Arabic / RTL); rendering is near-parity and ~1.74× faster, and the pure-Rust PaddleOCR engine beats fitz on CJK scans (see Accuracy). Now on PyPI:
pip install pdfspine(see Install); or build from source.
Why pdfspine?
PyMuPDF is excellent, but it is AGPL-3.0 (or a commercial license from Artifex) — a non-starter for many closed-source products, SaaS backends, and permissively-licensed open-source projects.
pdfspine is a drop-in-shaped, permissively-licensed (Apache-2.0) alternative:
- Apache-2.0 throughout — permissive, with an explicit patent grant. The
dependency graph is gated by
cargo-denyto exclude GPL / AGPL / LGPL / MPL / SSPL from the shipped wheel. License cleanliness is CI-enforced, not a promise. - Pure Rust, no C blob. Self-contained wheels, no system
zlib/C linkage, no bundled prebuilt engine (the differentiator vs pdfium-based wrappers). import fitzcompatible (opt-in). A compatibility shim lets much existing PyMuPDF code run unmodified — available asimport pdfspine.fitz as fitz, or registered under the globalfitz/pymupdfnames with one call topdfspine.install_fitz_shim(). A default install is collision-safe: it does not claim those global names, so it coexists with a real PyMuPDF in the same environment. A machine-readableCOMPAT.tomldocuments every symbol's status.- Memory-safe by construction.
#![forbid(unsafe_code)]in every first-party crate except the single audited PyO3 FFI chokepoint. - Clean-room. No code, tests, or fixtures derived from MuPDF / PyMuPDF / any AGPL source.
What works today
| Area | Capabilities |
|---|---|
| Read | open (file/bytes), malformed-PDF repair, encrypted PDFs (RC4 / AES-128 / AES-256, R2–R6) |
| Text | get_text (text/words/blocks/dict/rawdict/json/rawjson/html/xhtml/xml), per-glyph rendering geometry, search_for, TextPage, fonts/images inventory |
| Tables | find_tables with merged-cell detection → extract() / to_markdown() / to_html(); optional Microsoft TATR vision backend for borderless tables |
| Edit & save | full + byte-exact incremental save, garbage collection, page insert/delete/copy/move/select, insert_pdf merge, metadata/XMP, TOC, links, encryption write |
| Annotate | all common annotation types with /AP appearance streams; AcroForm read / fill / flatten + Widget; destructive redaction (verified content removal) |
| Render | get_pixmap (vector + text + image + shadings via a tiny-skia rasterizer), Pixmap (buffer-protocol/numpy), DisplayList, get_svg_image |
| Images | open PNG/JPEG/TIFF/GIF/BMP/WEBP as documents, convert_to_pdf, image-XObject decode (DCT/CCITT/JBIG2/JPX), extract_image |
| Markdown | markdown_to_pdf() — a pdfspine original extension (not a PyMuPDF API): CommonMark + GFM tables / strikethrough / task lists → PDF via a deterministic pure-Rust layout engine; local & data:-URI images (never the network); optional user TTF via font= / cjk_font= (CJK) |
| Layers | Optional Content Groups read/write (get_ocgs / add_ocg / set_layer) |
| OCR | pure-Rust PaddleOCR by default (PP-OCRv5, weights from the shared ocrspine-models package, stronger on CJK), with an explicit Tesseract compatibility adapter → searchable-sandwich PDF |
| CLI | pdfspine info / text / render / merge / split / pages / images / toc |
Planned next: reading-order accuracy improvements, Type1/Type3 glyph rendering,
broader CJK coverage. See PRD.md / docs/ROADMAP.md.
Out of scope: digital-signature creation.
Glyph geometry (0.7.0)
The structured dict / rawdict / json / rawjson formats expose span
matrix, text_matrix, ctm, dir, quad, seq, declared_size, and
rendered_size; raw characters also expose matrix, quad, rendered_size,
seq, and synthetic. The first glyph determines span["size"]; it equals
span["rendered_size"] and sqrt(abs(det(matrix))), while declared_size
preserves the signed PDF Tf operand. Use each raw character's rendered_size
when glyph sizes vary.
matrix / quad use the page's device space, while text_matrix / ctm remain
in PDF user space. HTML / XHTML / XML and get_texttrace() retain their existing
declared-size semantics, and rotated-page coordinates and geometry-aware span
boundaries can differ from PyMuPDF. See the text extraction guide
for the field contract and the geometry,
size parity,
span parity, and
performance reports for the
measured limits and cost.
Install
pip install pdfspine
pdfspine is on PyPI. OCR works out of the box: the PP-OCRv5 weights ship in
the shared ocrspine-models data
package — a runtime dependency pip pulls in automatically — so the wheel itself
stays lean and no longer embeds them. To build from source instead, see
Build & install.
Python 3.12+ is supported. Prebuilt wheels are published for Linux x86-64 / ARM64, macOS Intel / Apple silicon, and Windows x86-64; an sdist is also available for other supported environments.
Quick start
import pdfspine
doc = pdfspine.open("input.pdf")
print(len(doc), "pages", doc.metadata)
page = doc[0]
print(page.get_text()) # plain text
print(page.search_for("invoice")) # list[Rect]
page.get_pixmap(dpi=150).save("page1.png") # render to image
tables = page.find_tables()
for t in tables.tables:
print(t.to_markdown()) # or t.to_html() for merged cells
# Optional: pip install "pdfspine[tatr]", then prefetch the pinned checkpoints.
# vision_tables = page.find_tables(strategy="vision", backend="tatr")
doc.save("output.pdf", garbage=4, deflate=True)
doc.save_html("output.html") # complete UTF-8 HTML5 document
# Markdown → PDF (pdfspine original extension — not part of the PyMuPDF surface)
pdfspine.markdown_to_pdf("# Title\n\nHello **Markdown**!").save("hello.pdf")
Existing PyMuPDF code often runs unchanged via the opt-in compat shim:
import pdfspine.fitz as fitz # the shim, no global-name collision
doc = fitz.open("input.pdf")
text = doc[0].get_text("dict")
# Or make the literal `import fitz` resolve to the shim (one-time opt-in):
import pdfspine
pdfspine.install_fitz_shim()
import fitz # now -> pdfspine's fitz shim
A default install does not claim the global fitz / pymupdf names, so it
is safe alongside a real PyMuPDF; install_fitz_shim() uses setdefault and
never clobbers a PyMuPDF you imported first.
Command line:
pdfspine info report.pdf
pdfspine text report.pdf --pages 1-3 --format json -o out.json
pdfspine render report.pdf --dpi 200 -o images/
pdfspine merge a.pdf b.pdf -o merged.pdf
Accuracy
Validated against an objective ground-truth harness and with PyMuPDF (fitz) as
the differential oracle (clean-room: the AGPL oracle is run locally only and never
committed). See docs/BENCHMARKS.md and the
conformance/gt/ reports for the dated, reproducible evidence.
- In the dated 58-document benchmark, pdfspine's aggregate mean edit-similarity, token-F1, word-set-Jaccard, and reading-order scores trail fitz by 0.2–1.4 percentage points. It reaches parity on selected born-digital metrics and beats fitz on Arabic / RTL (correct bidi reordering).
- Rendering is near-parity with fitz (page-image SSIM ~0.945) and ~1.74× faster after a font-cache fix.
- OCR beats fitz on CJK scans: the pure-Rust PaddleOCR engine (PP-OCRv5, with
weights from the shared
ocrspine-modelspackage) outperforms fitz's OCR path on Chinese/Japanese/Korean documents. - Real-corpus robustness: open rate 100%, 0 panics/hangs, re-saved files
100%
qpdf --check-clean across the public-domain US-government corpus.
Remaining accuracy work (multi-column reading order, Type1/Type3 glyph rendering,
broader CJK) is tracked in docs/PRD-NEXT.md.
Build & install
Requirements: Rust (pinned to 1.96.0 by rust-toolchain.toml), Python ≥
3.12, maturin ≥ 1.12,<2. uv
recommended.
uv venv .venv && source .venv/bin/activate
maturin develop # build + install the extension in-place
python -c "import pdfspine; print(pdfspine.__version__)"
# redistributable wheel:
maturin build --release # -> target/wheels/
Building from source needs a C/asm compiler. The bundled pure-Rust PaddleOCR engine depends on
tract, which compiles target-specific assembly kernels at build time: a C compiler (cc/clang) on Linux/macOS, or the MSVC Build Tools (incl.ml64.exe) on Windows. Prebuilt PyPI wheels need none of this. To build a fully C-free library, compile the Rust crates with--no-default-features(drops thepaddle-ocrfeature). The wheel no longer embeds the OCR models — they ship in the sharedocrspine-modelspackage (a runtime dependency).
Architecture
A Cargo workspace with a strict dependency DAG; the Python bindings touch exactly one façade crate, and core logic is split into independently testable units.
py-bindings (PyO3 cdylib -> pdfspine._core, abi3-py311)
│
▼
pdf-api facade / re-exports
┌──────────┬───┴────┬──────────┐
▼ ▼ ▼ ▼
pdf-text pdf-edit pdf-image pdf-render
│ │ │ │
└────┬─────┘ │ (fonts, text)
▼ │
pdf-fonts ◄────────┘
▼
pdf-core ◄──────── pdf-crypto
| Crate | Responsibility |
|---|---|
pdf-core |
object model, lexer/parser, xref, repair, filters, writer, geometry |
pdf-crypto |
Standard security handler (RC4 / AES-128 / AES-256) |
pdf-fonts |
font mapping (encodings / ToUnicode / CMap / widths) |
pdf-text |
content-stream interpreter, get_text, search, find_tables |
pdf-edit |
page ops, merge, annotations / forms, metadata / TOC, redaction, OCG |
pdf-image |
image documents, image-XObject codecs, Pixmap |
pdf-render |
tiny-skia rasterizer → Pixmap, DisplayList, SVG |
pdf-api |
unified ergonomic façade |
py-bindings |
PyO3 wrappers → the _core extension module |
Develop / test
./ci.sh # complete local/CI/pre-push gate
# Individual focused checks remain available:
python scripts/quality_gate.py --help
python conformance/run_validation.py … # real-corpus accuracy harness
pdfspine is built strictly test-first (red → green → refactor → harden); the
per-function test plan is in docs/test-case-catalog.md.
Documentation
Guide + API reference + PyMuPDF migration guide: build the docs site with
mkdocs serve (see mkdocs.yml / docs/). The
authoritative design lives in PRD.md.
License
Apache-2.0 — see LICENSE and NOTICE. All third-party
dependencies are permissive (MIT / Apache-2.0 / BSD / Zlib / …); the shipped graph
is CI-verified free of copyleft.
Metadata
Release files for pdfspine 0.7.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pdfspine-0.7.1.tar.gz | 4.8 MB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| pdfspine-0.7.1-cp311-abi3-win_amd64.whl | CPython 3.11 | abi3 | Windows x86-64 | Details |
| pdfspine-0.7.1-cp311-abi3-manylinux_2_28_aarch64.whl | CPython 3.11 | abi3 | Linux glibc 2.28+ ARM64 | Details |
| pdfspine-0.7.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.11 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| pdfspine-0.7.1-cp311-abi3-macosx_11_0_arm64.whl | CPython 3.11 | abi3 | macOS 11.0+ ARM64 | Details |
| pdfspine-0.7.1-cp311-abi3-macosx_10_12_x86_64.whl | CPython 3.11 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 62.8 MB
Release files / pdfspine-0.7.1.tar.gz
| Download URL | pdfspine-0.7.1.tar.gz |
|---|---|
| Size | 4.8 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e45c538f09dcaa69b035a8d1d8f26bfce833892cd2dab283c5c771b13036d367
|
|
BLAKE2b-256 checksum How to use checksums |
40dd29c743d03bce1fb1b7f4797db77e546e68d49660395a402a32e21e188d4b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pdfspine-0.7.1-cp311-abi3-win_amd64.whl
| Download URL | pdfspine-0.7.1-cp311-abi3-win_amd64.whl |
|---|---|
| Size | 12.2 MB |
| Tags | CPython 3.11 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
b2d9be7051ef9d5598147876198f72c6c3697e9efc764644c3239844ad16c7ff
|
|
BLAKE2b-256 checksum How to use checksums |
fba0ba19ecb5c663d3b1c8d3eb92f568cf0781bd62aabfdea5df6b74b3ab0506
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pdfspine-0.7.1-cp311-abi3-manylinux_2_28_aarch64.whl
| Download URL | pdfspine-0.7.1-cp311-abi3-manylinux_2_28_aarch64.whl |
|---|---|
| Size | 11.1 MB |
| Tags | CPython 3.11 Linux glibc 2.28+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
46061244b97b54069198269736266b4782796add00980d034ef62927870128cc
|
|
BLAKE2b-256 checksum How to use checksums |
7494f0448a10400bc7b89d2a5ffb047666fcdcc81b9d1409de99096da9eac9f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pdfspine-0.7.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | pdfspine-0.7.1-cp311-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 12.1 MB |
| Tags | CPython 3.11 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
1f26a4d8e303aa3db5bc1a290d912789eb481f48206e03bce38b772b6bce1bb4
|
|
BLAKE2b-256 checksum How to use checksums |
bd7abbabd9ad4efe2c7e3782ac416ef12b3074115225d6ea8b35b7c941b0a167
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pdfspine-0.7.1-cp311-abi3-macosx_11_0_arm64.whl
| Download URL | pdfspine-0.7.1-cp311-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 10.9 MB |
| Tags | CPython 3.11 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
ddbfc8e612318fbff05290447c64b29e67c2c125d8e2a366a4fba64025850990
|
|
BLAKE2b-256 checksum How to use checksums |
7b194423c71d28991453bcbe8a689e1fc814571cb8a1ffa251994332050c33ec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / pdfspine-0.7.1-cp311-abi3-macosx_10_12_x86_64.whl
| Download URL | pdfspine-0.7.1-cp311-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 11.8 MB |
| Tags | CPython 3.11 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
26f856eadc0d8cca89e9e805f81a7e19f890ede6712b5119e0e7b2f6d87710bd
|
|
BLAKE2b-256 checksum How to use checksums |
cc6ccab0dc8e26040dbacdb726d58151eaad1a4e37a2471aad0dbb10ed138daa
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|