PyMuPDF Tutorial: PDF to Image, Extract Text and Images in Python (2026)

Automation Nov 11, 2026

Watch on YouTube

Turning PDF pages into images for a preview, pulling the text out of a stack of reports, saving every logo or chart embedded in a file, or blacking out e-mail addresses before you share a document: these are some of the most common PDF chores, and PyMuPDF handles all of them with one fast library. It is a Python binding for the MuPDF engine, so it renders pages itself; you do not need Poppler, Ghostscript or any other system tool. In this tutorial we build a small sample PDF in code, read its text and word positions, convert pages to PNG and JPG, extract the embedded image, then highlight and permanently redact text. Everything was tested on Python 3.13.5 with PyMuPDF 2.1 (MuPDF 1.28.6) and Pillow 13.0.0, and every output block below is the real output of the script above it.

Why PyMuPDF in 2026?

  • No external binaries: pip install pymupdf ships prebuilt wheels with the rendering engine inside, unlike pdf2image, which needs Poppler installed separately.
  • One library, many jobs: rendering, text and word extraction, image extraction, search, annotations, redaction, metadata and creating new PDFs.
  • Fast: the C engine renders and extracts much faster than pure-Python readers, which matters when you batch thousands of pages.
  • Modern import name: use import pymupdf. Old tutorials use import fitz; that alias still works, but new code should use the real package name.
  • License to know: PyMuPDF is dual licensed under AGPL-3.0 or a commercial Artifex license. Internal scripts are fine; if you ship it inside a closed-source product, check the license first.

Install

python -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install pymupdf pillow
python -c "import pymupdf; print(pymupdf.__version__, pymupdf.mupdf_version)"

Pillow is only used to draw a demo logo; PyMuPDF itself does not need it. Recent PyMuPDF releases also pull in the optional pymupdf4llm and pymupdf-layout helpers, which you can ignore for this guide.

Step 1: Create a sample PDF to work with

So that you can run every step without hunting for a test file, this script creates a three-page A4 report with a title, an invoice line, a customer e-mail address, a status, a border and an embedded PNG logo on page 1. Positions are in PDF points (1/72 inch) measured from the top-left corner.

import io
import pymupdf
from PIL import Image, ImageDraw

# A small PNG logo made with Pillow, so the PDF contains a real embedded image
logo = Image.new("RGB", (240, 120), "#1e3a8a")
ImageDraw.Draw(logo).text((70, 50), "ACME LTD", fill="white")
buf = io.BytesIO()
logo.save(buf, format="PNG")

doc = pymupdf.open()                      # new, empty PDF
for n in range(1, 4):
    page = doc.new_page(width=595, height=842)   # A4 in points
    page.insert_text((72, 80), f"Monthly Report - Page {n}", fontsize=20)
    page.insert_text((72, 120), f"Invoice INV-2026-00{n} total: ${n * 1250:,}.00", fontsize=12)
    page.insert_text((72, 140), "Customer email: buyer@example.com", fontsize=12)
    page.insert_text((72, 160), "Status: paid" if n != 2 else "Status: overdue", fontsize=12)
    if n == 1:
        page.insert_image(pymupdf.Rect(72, 200, 312, 320), stream=buf.getvalue())
    page.draw_rect(pymupdf.Rect(60, 60, 535, 180), color=(0.2, 0.4, 0.8), width=1)

doc.set_metadata({"title": "Monthly Report", "author": "Automation Bot"})
doc.save("report.pdf")
print("pages:", doc.page_count)
doc.close()
pages: 3

Step 2: Read metadata, page text and word positions

pymupdf.open() works as a context manager, a document is iterable page by page, and page.get_text() returns plain text. Pass "words" to get every word with its bounding box, which is ideal for finding values next to a label or building your own layout logic.

import pymupdf

with pymupdf.open("report.pdf") as doc:
    print("pages:", doc.page_count)
    print("title:", doc.metadata["title"], "| author:", doc.metadata["author"])
    for page in doc:
        first_line = page.get_text().splitlines()[0]
        print(f"page {page.number + 1}: {first_line}")

    # Words with coordinates: (x0, y0, x1, y1, word, block, line, word_no)
    words = doc[1].get_text("words")
    print("words on page 2:", len(words))
    print("first word box:", [round(v) for v in words[0][:4]], words[0][4])
pages: 3
title: Monthly Report | author: Automation Bot
page 1: Monthly Report - Page 1
page 2: Monthly Report - Page 2
page 3: Monthly Report - Page 3
words on page 2: 14
first word box: [72, 58, 142, 86] Monthly

Other useful modes are "blocks" (paragraph-level boxes), "dict" (fonts, sizes and spans) and "html". If a page returns no text at all, it is probably a scanned image; render it as in the next step and run OCR on it.

Step 3: Convert PDF pages to PNG or JPG

page.get_pixmap() renders a page to a raster image. The dpi argument controls sharpness: 72 is the native PDF size, 150 is good for previews and OCR, and 300 is print quality. For a quick thumbnail, scale with a Matrix instead.

from pathlib import Path
import pymupdf

out = Path("pages")
out.mkdir(exist_ok=True)

with pymupdf.open("report.pdf") as doc:
    for page in doc:
        pix = page.get_pixmap(dpi=150)           # 72 dpi is the PDF default
        path = out / f"page-{page.number + 1}.png"
        pix.save(path)
        print(path, f"{pix.width}x{pix.height}", f"{path.stat().st_size // 1024} KB")

    # Thumbnail of page 1 only, as JPEG
    thumb = doc[0].get_pixmap(matrix=pymupdf.Matrix(0.5, 0.5))
    thumb.save("thumb.jpg", jpg_quality=80)
    print("thumb.jpg", f"{thumb.width}x{thumb.height}")
pages/page-1.png 1240x1755 34 KB
pages/page-2.png 1240x1755 32 KB
pages/page-3.png 1240x1755 32 KB
thumb.jpg 298x421

An A4 page at 150 dpi is 1240 x 1755 pixels, exactly 595 x 842 points times 150/72. To render only part of a page, pass clip=pymupdf.Rect(...); to save memory on huge pages, lower the dpi or use colorspace=pymupdf.csGRAY.

Step 4: Extract embedded images

Rendering gives you a picture of the whole page. To get the original image files stored inside the PDF, list them with page.get_images() and decode each one by its cross-reference number (xref) with doc.extract_image(), which also tells you the original format.

from pathlib import Path
import pymupdf

out = Path("images")
out.mkdir(exist_ok=True)

with pymupdf.open("report.pdf") as doc:
    for page in doc:
        for img in page.get_images(full=True):
            xref = img[0]
            info = doc.extract_image(xref)
            path = out / f"p{page.number + 1}-x{xref}.{info['ext']}"
            path.write_bytes(info["image"])
            print(path, f"{info['width']}x{info['height']}", info["ext"])
images/p1-x10.png 240x120 png

The logo comes back at its original 240 x 120 size, not the size it was drawn on the page. The same image used on many pages has one xref, so keep a set of seen xrefs if you want to save each image only once.

Step 5: Highlight words and redact private data

page.search_for() returns the rectangle of every match. Turn those rectangles into highlight annotations for reviewers, or into redaction annotations that page.apply_redactions() burns in, removing the underlying text from the file rather than just covering it with a black box.

import pymupdf

with pymupdf.open("report.pdf") as doc:
    # 1) Highlight every "overdue" so reviewers spot it
    for page in doc:
        for rect in page.search_for("overdue"):
            page.add_highlight_annot(rect)
            print(f"highlighted 'overdue' on page {page.number + 1}")

    # 2) Permanently remove e-mail addresses before sharing
    removed = 0
    for page in doc:
        for rect in page.search_for("buyer@example.com"):
            page.add_redact_annot(rect, fill=(0, 0, 0))
            removed += 1
        page.apply_redactions()
    print("redacted:", removed)
    doc.save("report_shared.pdf", garbage=3, deflate=True)

with pymupdf.open("report_shared.pdf") as check:
    text = "".join(p.get_text() for p in check)
    print("email still present?", "buyer@example.com" in text)
    print("annotations on page 2:", [a.type[1] for a in check[1].annots()])
highlighted 'overdue' on page 2
redacted: 3
email still present? False
annotations on page 2: ['Highlight']

The final check opens the saved copy and confirms the e-mail address is really gone from the text layer. Always verify like this before sending a redacted file; a black rectangle drawn with draw_rect() hides nothing, because the text is still underneath.

Quick reference

TaskPyMuPDF call
Open a PDFpymupdf.open("file.pdf")
Page count / metadatadoc.page_count, doc.metadata
Plain text of a pagepage.get_text()
Words with coordinatespage.get_text("words")
Page to PNG/JPGpage.get_pixmap(dpi=150).save("p.png")
Embedded imagespage.get_images() + doc.extract_image(xref)
Find textpage.search_for("word")
Highlightpage.add_highlight_annot(rect)
Redactpage.add_redact_annot(rect) + page.apply_redactions()
Save smaller filedoc.save("out.pdf", garbage=3, deflate=True)

Common problems and fixes

  • ModuleNotFoundError: No module named 'frontend' after import fitz: you installed the unrelated fitz package from PyPI. Run pip uninstall fitz, then pip install pymupdf, and use import pymupdf.
  • Empty text from get_text(): the page is a scanned image. Render it at 300 dpi and run OCR on the PNG.
  • Blurry images: raise dpi; the default render is only 72 dpi.
  • Encrypted PDF: check doc.needs_pass and call doc.authenticate("password") before reading pages.
  • Huge output files: save with garbage=3, deflate=True to drop unused objects and compress streams.

Where to go next

Conclusion

With PyMuPDF a few lines cover most PDF automation: get_text() for content and word positions, get_pixmap() for PNG and JPG pages, extract_image() for embedded pictures, and search_for() with highlight and redaction annotations for review and privacy. Because it needs no external tools and runs fast, it is a solid default for batch PDF jobs in 2026.

Share:
Last updated: November 2026
Built-in Functions in Python 2026 str() in Python 2026: String Conversion + Modern Formatting & Best Practices memoryview with JAX in Python 2026: Zero-Copy NumPy → JAX Array Interop + Efficient ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + GPU Pinning & ML Examples memoryview with TensorFlow in Python 2026: Zero-Copy NumPy → Tensor Interop + ML Examples memoryview with NumPy & PyTorch in Python 2026: Zero-Copy Views, Efficient Slicing & ML Interop Examples memoryview with NumPy in Python 2026: Zero-Copy Views, Efficient Slicing & Real ML Examples memoryview() in Python 2026: Zero-Copy Magic for Large Binary Data + Real Examples import() in Python 2026: How It Works, Security Risks & Modern Alternatives (importlib) zip() in Python 2026: How to Use It, strict=True Behavior & Real-World Examples vars() in Python 2026: Accessing Object Namespace + Modern Introspection Patterns type() in Python 2026: Dynamic Type Inspection & Object Creation + Modern Patterns tuple() in Python 2026: Immutable Sequences + Modern Patterns & Best Practices super() in Python 2026: Method Resolution & Modern Inheritance Patterns sum() in Python 2026: Summing Iterables + Modern Numeric Patterns & Best Practices staticmethod() in Python 2026: Static Methods, Utility Functions & Modern Best Practices sorted() in Python 2026: Sorting Iterables + Modern Patterns & Best Practices slice() in Python 2026: Creating Slice Objects + Modern Patterns & Best Practices setattr() in Python 2026: Dynamic Attribute Setting + Modern Patterns & Safety set() in Python 2026: Mutable Sets Creation + Modern Patterns & Best Practices