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 pymupdfships prebuilt wheels with the rendering engine inside, unlikepdf2image, 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 useimport 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
| Task | PyMuPDF call |
|---|---|
| Open a PDF | pymupdf.open("file.pdf") |
| Page count / metadata | doc.page_count, doc.metadata |
| Plain text of a page | page.get_text() |
| Words with coordinates | page.get_text("words") |
| Page to PNG/JPG | page.get_pixmap(dpi=150).save("p.png") |
| Embedded images | page.get_images() + doc.extract_image(xref) |
| Find text | page.search_for("word") |
| Highlight | page.add_highlight_annot(rect) |
| Redact | page.add_redact_annot(rect) + page.apply_redactions() |
| Save smaller file | doc.save("out.pdf", garbage=3, deflate=True) |
Common problems and fixes
ModuleNotFoundError: No module named 'frontend'afterimport fitz: you installed the unrelatedfitzpackage from PyPI. Runpip uninstall fitz, thenpip install pymupdf, and useimport 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_passand calldoc.authenticate("password")before reading pages. - Huge output files: save with
garbage=3, deflate=Trueto drop unused objects and compress streams.
Where to go next
- Merge, split and rotate the PDFs you produce with pypdf: Merge and Split PDF Files in Python.
- When you need clean tables instead of raw text, use Extract Tables from PDF with pdfplumber.
- For scanned pages with no text layer, feed the PNGs from Step 3 into OCR in Python: Extract Text from Images.
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.