mrkeyoor.com_
Thu 06 Aug 05:57 UTC
PyPIUtilsupdated 06 Aug 2026

pymupdf

PyMuPDF is a Python binding over MuPDF, the C rendering engine from Artifex. The C layer is why it is fast: text extraction, page rasterization, and document surgery all happen in compiled code with the whole engine shipped inside the wheel, so there are no other Python dependencies. One library covers reading and writing. You can pull plain text or a structured dict with per-span font, size, color, and bounding box; detect and export tables; render any page to a PNG at any DPI; add and apply real redactions that delete the underlying content; fill AcroForm fields; merge, split, rotate, and encrypt documents. It opens more than PDF too: XPS, EPUB, CBZ, MOBI, FB2, SVG, plain images, and Markdown. Version 1.28.0 wraps MuPDF 1.29.0.

Verdict

On speed and breadth PyMuPDF has no real competition in Python, and if you are already AGPL or paying Artifex it should be the default. The licence is the decision, not the code: for a closed-source product, settle that question before the first import, because migrating away later means rewriting every coordinate.

API stability4/5Document, Page, and the get_text family have been steady for years and old scripts keep running, but the package renamed itself from fitz to pymupdf, method names picked up snake_case aliases along the way, and the minor version tracks the bundled MuPDF release so upgrades can shift rendering and extraction output.
Docs5/5pymupdf.readthedocs.io covers every class and method with signatures and examples, plus a recipes section for common jobs, a coordinate system explainer, and an explicit page on the AGPL and commercial licensing split.
Maintenance5/5Backed commercially by Artifex, repo pushed 5 August 2026, 1.28.0 released 29 June 2026 tracking MuPDF 1.29.0, and only 50 open issues (62 counting PRs) for a project this large; wheels ship for new CPython versions quickly.
Ecosystem5/5Roughly 26.8M weekly downloads, and it is the extraction layer inside LangChain and LlamaIndex PDF loaders, document RAG pipelines, and most Python OCR preprocessing stacks.

Use it if

  • You are processing documents at volume and per-page cost matters, such as an ingestion pipeline turning thousands of PDFs into text or page images
  • Plain text is not enough and you need layout metadata: font name, size, color, and the bounding box of every span, which get_text('dict') gives you directly
  • You need to render pages to images, whether for thumbnails, a preview UI, or feeding page bitmaps to a vision model at a chosen DPI
  • You need real redaction. apply_redactions removes the text and images under the rectangle instead of drawing a black box over content that a copy-paste would still reveal
  • One library needs to both read and write: extract, annotate, fill forms, merge, split, and save encrypted, without stitching together three packages that disagree about coordinates
Skip it if

Setup reality

pip install pymupdf pulls a single wheel with no Python dependencies, but it is a 17 to 25 MB download that expands to about 64 MB on disk because the MuPDF C library rides along. The wheels are abi3 for CPython 3.10 and up and cover manylinux_2_28 on x86_64 and aarch64, musllinux_1_2 on x86_64 only, macOS on both architectures, Windows 32 and 64 bit, plus a WebAssembly build. Alpine on ARM falls off that list and compiles the 84 MB source distribution, which needs a full C and C++ toolchain and takes a long while. The import name is pymupdf; the historical import fitz still resolves to the same module but is the old spelling and new code should not use it. The part people skip is licensing: AGPL-3.0 or a paid Artifex license, decided before the code ships, not after. Incremental saves have their own trap and need encryption=pymupdf.PDF_ENCRYPT_KEEP or MuPDF refuses with 'Can't do incremental writes when changing encryption'.

Patterns

Pull text out of every pageextract-plain-text

import pymupdf

doc = pymupdf.open('document.pdf')
for page in doc:
    print(page.get_text())

# reading order can be wrong on multi-column pages
print(doc[0].get_text(sort=True))
doc.close()

Default extraction follows the order text was written into the file, which is not reading order on newspapers or two-column papers. sort=True reorders by vertical then horizontal position and fixes most of those cases.

Get font, size, and position for every spanextract-with-layout

page = doc[0]
for block in page.get_text('dict')['blocks']:
    if block['type'] != 0:      # 0 is text, 1 is image
        continue
    for line in block['lines']:
        for span in line['spans']:
            print(span['text'], span['font'], round(span['size'], 1), span['bbox'])

This is how you detect headings without a heuristic on line length: compare span sizes against the document's body size. Image blocks have type 1 and no lines key, so skipping them is required or you get a KeyError.

Rasterize a page at a chosen resolutionrender-page-to-image

page = doc[0]
pix = page.get_pixmap(dpi=150)
pix.save('page_0.png')

img_bytes = pix.tobytes('png')   # for an in-memory pipeline
print(pix.width, pix.height, pix.n)

Memory grows with the square of the DPI, so 600 dpi on an A4 page is roughly 35 megapixels and can be hundreds of megabytes across a worker pool. 150 dpi is enough for OCR; 300 only when the source is genuinely small print.

Find text and annotate the hitssearch-and-highlight

page = doc[0]
hits = page.search_for('confidential')
for rect in hits:
    page.add_highlight_annot(rect)

doc.save('highlighted.pdf')

search_for is case-insensitive and returns Rect objects in the top-left origin system PyMuPDF uses for pages, not the PDF specification's bottom-left. It also cannot match across a line break, so hyphenated or wrapped phrases come back empty.

Permanently remove text from a pageredact-content

page = doc[0]
for rect in page.search_for('social security'):
    page.add_redact_annot(rect, fill=(0, 0, 0))

page.apply_redactions()
doc.save('redacted.pdf')

apply_redactions actually deletes the underlying text and image content inside the rectangle, unlike drawing a black rectangle which leaves the text selectable. Nothing happens until you call it, and it must run before save.

Find tables and export themextract-tables

page = doc[0]
tabs = page.find_tables()
for table in tabs.tables:
    rows = table.extract()          # list of lists of strings
    print(table.to_markdown())
    df = table.to_pandas()          # needs pandas installed

Detection leans on ruling lines, so borderless tables often come back as zero tables or as one merged blob. Recent versions also print a hint suggesting the separate pymupdf_layout package for better page analysis. to_pandas raises ImportError if pandas is absent.

Combine documents and cut pages outmerge-and-split

merged = pymupdf.open()
for path in ['a.pdf', 'b.pdf']:
    with pymupdf.open(path) as src:
        merged.insert_pdf(src)
merged.save('merged.pdf', garbage=4, deflate=True)

with pymupdf.open('big.pdf') as src:
    out = pymupdf.open()
    out.insert_pdf(src, from_page=0, to_page=9)
    out.save('first10.pdf')

pymupdf.open() with no argument creates an empty PDF to build into. garbage=4 plus deflate=True is what actually shrinks the output; without them a merge of two small files can be larger than the inputs combined.

Work on a document you never wrote to diskopen-from-bytes

data = await response.read()
doc = pymupdf.open(stream=data, filetype='pdf')

text = doc[0].get_text()
out_bytes = doc.tobytes(garbage=4, deflate=True)
doc.close()

filetype is required with stream, because there is no filename to sniff from. tobytes gives you the saved document back as bytes so nothing touches the filesystem, which matters in read-only containers.

Append changes to an existing file in placeincremental-save

doc = pymupdf.open('report.pdf')
doc[0].insert_text((72, 200), 'reviewed')

doc.save('report.pdf', incremental=True, encryption=pymupdf.PDF_ENCRYPT_KEEP)
doc.close()

Without encryption=pymupdf.PDF_ENCRYPT_KEEP this raises FzErrorArgument: Can't do incremental writes when changing encryption. Incremental saves are fast and preserve signatures, but they only append, so the file grows with every edit.

Save with a password and permission flagsencrypt-document

perm = int(
    pymupdf.PDF_PERM_ACCESSIBILITY
    | pymupdf.PDF_PERM_PRINT
)

doc.save(
    'protected.pdf',
    encryption=pymupdf.PDF_ENCRYPT_AES_256,
    owner_pw='owner-secret',
    user_pw='user-secret',
    permissions=perm,
)

Opening an encrypted file gives you a Document with needs_pass set and no readable pages until authenticate(password) returns non-zero. Permission flags are advisory and enforced only by well-behaved viewers.

Pull the original images out of a PDFextract-embedded-images

for page_index, page in enumerate(doc):
    for img_index, img in enumerate(page.get_images(full=True)):
        xref = img[0]
        pix = pymupdf.Pixmap(doc, xref)
        if pix.n > 4:                                   # CMYK
            pix = pymupdf.Pixmap(pymupdf.csRGB, pix)
        pix.save(f'p{page_index}_i{img_index}.png')

get_images returns the same xref once per page that references it, so a repeated logo gets extracted many times; deduplicate on xref. The CMYK conversion is required because Pixmap.save cannot write a 5-component image to PNG.

Run OCR on a page with no text layerocr-scanned-page

page = doc[0]
if not page.get_text().strip():
    tp = page.get_textpage_ocr(language='eng', dpi=300, full=True)
    text = page.get_text(textpage=tp)
    print(text)

Tesseract has to be installed at the OS level and reachable, and TESSDATA_PREFIX must point at the language data or this raises rather than falling back. OCR is orders of magnitude slower than native extraction, so gate it on an empty text layer as shown.

Alternatives

PackageRegistryPick it when
pypdfPyPIYou only need to split, merge, rotate, encrypt, or pull rough text, and you need a permissive license
pdfplumberPyPITable and layout extraction accuracy matters more than speed, and you want per-character positional data under an MIT license
pikepdfPyPIYou are editing PDF structure, objects, and streams rather than extracting content, and you want qpdf semantics with a permissive license
pymupdf4llmPyPIYou want chunked Markdown output for a RAG pipeline instead of writing the layout-to-Markdown conversion yourself