Migrating from PyMuPDF
An honest account of pdfspine's PyMuPDF compatibility — the opt-in fitz shim, coverage at a glance, how gaps behave, the feature mapping, and what differs or is out of scope.
pdfspine is designed so that existing PyMuPDF code can run unmodified for the supported subset. This page is the honest, no-marketing account of what works, what differs, and what isn't there yet.
import fitz — one opt-in step
The compatibility shim maps PyMuPDF's exact names onto pdfspine. It is opt-in so a default install never collides with a real PyMuPDF in the same environment. Either import the shim under its submodule name (no global-name collision):
import pdfspine.fitz as fitz # the shim, always available
doc = fitz.open("input.pdf")
page = doc[0]
text = page.get_text()
pix = page.get_pixmap(dpi=150)
pix.save("out.png")
doc.save("out.pdf")…or, to keep an unmodified import fitz working, opt in once at startup:
import pdfspine
pdfspine.install_fitz_shim() # registers global `fitz` / `pymupdf`
import fitz # now resolves to the pdfspine shiminstall_fitz_shim() is idempotent and uses setdefault, so it never clobbers a
real PyMuPDF you already imported. import pymupdf (and from pdfspine import pymupdf) is supported the same way. For new code, prefer the native package:
import pdfspine
doc = pdfspine.open("input.pdf")Both expose the identical open, Document, Page, Pixmap, DisplayList,
TextPage, Annot, Widget, Shape, Table, and geometry classes.
Coverage at a glance
The baseline is PyMuPDF 1.24.x. The machine-readable COMPAT.toml in the
repository tracks the disposition of every public PyMuPDF symbol:
| Disposition | Count | What it means |
|---|---|---|
| Implemented | 682 | Works today; does not raise on use. |
| Deferred | 21 | Known and planned for a later milestone. |
| Out-of-scope | 66 | Intentionally never in v1. |
| Total | 769 | 88.7% implemented |
"Implemented" means the method exists and returns a result of the right shape. Byte-for-byte / pixel-for-pixel agreement with PyMuPDF across a real PDF corpus is still being validated. Verify output on your own documents before relying on it.
How gaps behave
Anything not yet implemented raises a typed, catchable
pdfspine.PdfUnsupportedError (aliased as fitz.PdfUnsupportedError) with a
hint — never a bare AttributeError. That means you can detect and handle gaps
cleanly:
import pdfspine
try:
doc.some_unimplemented_method()
except pdfspine.PdfUnsupportedError as e:
print("not yet:", e)PyMuPDF exception names are aliased onto the typed hierarchy, so existing
except clauses keep working:
| PyMuPDF name | pdfspine type |
|---|---|
fitz.FileDataError | PdfSyntaxError |
fitz.EmptyFileError | PdfSyntaxError |
fitz.FileNotFoundError | built-in FileNotFoundError |
fitz.mupdf_display_errors | PdfError |
What is 100% compatible
The geometry layer (Point, Rect, IRect, Matrix, Quad) mirrors
PyMuPDF 1.24.x arithmetic exactly — operators, transforms, inversion,
morph / torect, quad convexity — as a documented contract. These classes are
also sequences, so r[0], tuple(r), and unpacking all behave like PyMuPDF.
Feature mapping
| Area | PyMuPDF | pdfspine | Status |
|---|---|---|---|
| Open / pages | fitz.open, doc[i], page_count | same | ✅ Implemented |
| Metadata | doc.metadata, set_metadata | same | ✅ Implemented |
| XMP | get_xml_metadata / set_xml_metadata | same | ✅ Implemented |
| Encryption (read) | authenticate, needs_pass, permissions | same | ✅ Implemented |
| Encryption (write) | save(encryption=...) | RC4 / AES-128 / AES-256 | ✅ Implemented |
| Text | get_text("text"/"words"/"blocks"/"dict"/"rawdict"/"json"/"html"/"xhtml"/"xml") | same | ✅ Implemented |
| Search | search_for (rects / quads) | same | ✅ Implemented |
| TextPage | get_textpage, extract* | same | ✅ Implemented |
| Tables | find_tables, to_markdown | + to_html | ✅ Implemented |
| Render | get_pixmap (DPI / matrix / clip / colorspace / alpha) | same | ✅ Implemented |
| DisplayList | get_displaylist, get_pixmap | same | ✅ Implemented |
| SVG | get_svg_image | same | ✅ Implemented |
| Pixmap | save / tobytes / samples / buffer protocol | same | ✅ Implemented |
| Save | save, ez_save, tobytes/write, incremental= | same | ✅ Implemented |
| Page ops | new_page, delete_page, select | same | ✅ Implemented |
| Merge | insert_pdf | same | ✅ Implemented |
| Image → PDF | convert_to_pdf, image inputs to open | same | ✅ Implemented |
| Markdown → PDF | — | markdown_to_pdf (pdfspine extra) | ✅ Implemented |
| TOC | get_toc, set_toc | same | ✅ Implemented |
| Links | get_links, insert_link, delete_link | same | ✅ Implemented |
| Annotations | add_*_annot, annots, delete_annot | same | ✅ Implemented |
| Forms | is_form_pdf, widgets, form_fill, form_flatten | same | ✅ Implemented |
| Redaction | add_redact_annot, apply_redactions | same | ✅ Implemented |
| Sanitize | scrub, bake | same | ✅ Implemented (subset of toggles) |
| Embedded files | embfile_* | same | ✅ Implemented |
| OCG / layers | get_ocgs, add_ocg, get_layer, set_layer, set_oc | same | ✅ Implemented (read + add/toggle/bind) |
| xref read | xref_object, xref_stream, xref_get_key, … | same | ✅ Implemented |
What differs
scrubtoggles — the full PyMuPDF toggle set is accepted, but only a subset (metadata, JavaScript, attached/embedded files, links, XMP) is acted on; the rest are no-ops.insert_image(pixmap=...)— not yet supported; passstream=bytes orfilename=instead.- Deprecated camelCase aliases — PyMuPDF's old
getText/getPixmap/setMetadatastyle names are provided as aliases where they existed, so legacy code keeps working. to_html()on tables — an pdfspine extra beyond PyMuPDF.
What is not yet implemented
These are the 21 deferred (planned) symbols — they raise PdfUnsupportedError
today:
- OCG / layer-config nesting —
add_layer,get_layers,switch_layer,set_layer_ui_config,get_oc,get_ocmd,set_ocmd(the basicget_ocgs/set_layer/set_ocread+toggle+bind surface works today). - Low-level text writing —
Page.write_text,Page.insert_font(the higher-levelinsert_text/insert_textbox/TextWriterwork today). - Device-callback replay —
Page.run,DisplayList.run,DisplayList.get_textpage(get_pixmapcovers the raster path). - Misc
Pagehelpers —remove_rotation,refresh,extend_textpage. Pixmap.warp,Annot.get_textbox,Document.insert_file(non-image attachments; image inputs toopenare supported),Document.FormFonts.Tools.set_annot_stem,Tools.set_subset_fontnames.
Out of scope for v1
Intentionally never in v1 (these raise PdfUnsupportedError):
- EPUB-class reflow (
doc.layout,next_location/prev_location,make_bookmark/find_bookmark). - HTML/CSS layout (
Story,insert_htmlbox,Xml,Archive). - Journalling op-naming / persistence (
journal_start_op,journal_stop_op,journal_op_name,journal_position,journal_save,journal_load,save_snapshot) — the basicjournal_enable/journal_undo/journal_redo/journal_can_dosurface is implemented. - Full Unicode shaping (complex scripts).
get_textpage_ocr and the pdfocr_* sandwich-PDF export are implemented,
not out of scope — see Rendering and the
OCR benchmark.
Consult the repository's COMPAT.toml for the authoritative, per-symbol
disposition — it is CI-enforced to stay in sync with the code.