pdfmux/blog
pdf-extraction

How to extract embedded images and figures from a PDF in Python

TL;DRExtract embedded images and figures from PDFs in Python with PyMuPDF, pdfplumber, pdf2image, and pdfmux. Code for raw bytes, bounding boxes, CMYK, and dedup.

Direct answer: To pull the raw embedded images out of a PDF in Python, use PyMuPDF (fitz): iterate page.get_images(), then doc.extract_image(xref) to get the original bytes and format. To render a whole page to an image (for OCR or a vision model), use pdf2image or PyMuPDF’s page.get_pixmap(). And when you need each figure paired with its caption and reading-order position — the version a RAG or LLM pipeline can actually use — run the page through pdfmux, which keeps the figure reference and caption in the Markdown instead of dumping anonymous bitmaps. Below: working code for all four, plus the edge cases (CMYK, alpha masks, inline images, duplicate logos) that break naïve scripts.


Three different things people mean by “extract images”

“Extract images from a PDF” hides three distinct tasks. Picking the wrong one is the most common reason a script returns garbage:

  1. Embedded raster images — actual JPEG/PNG/JBIG2 objects stored inside the PDF (a scanned photo, a chart exported as PNG). You want the original bytes at native resolution. → PyMuPDF.
  2. Rendering a page to an image — flatten everything on a page (text, vectors, images) into a single bitmap, usually to feed OCR or a vision model. There’s no embedded image to “extract”; you rasterize. → pdf2image or get_pixmap().
  3. Vector graphics — charts and diagrams drawn with PDF path operators. These aren’t images at all; there are no pixels to extract. Your only option is to render the region. → render the bounding box.

Most “why is my extracted image blank/tiny/wrong” problems are a mismatch between which of these you have and which method you used. Knowing whether your PDF is scanned or digital tells you which case you’re in before you write a line of code.

PyMuPDF is the fastest, most reliable way to get the original embedded image bytes. Each image is a PDF object referenced by an xref; you list the xrefs on a page, then extract each one.

pip install pymupdf
import fitz  # PyMuPDF

doc = fitz.open("report.pdf")

for page_index in range(len(doc)):
    page = doc[page_index]
    for img in page.get_images(full=True):
        xref = img[0]
        base = doc.extract_image(xref)        # original bytes, no re-encoding
        image_bytes = base["image"]
        ext = base["ext"]                      # 'jpeg', 'png', 'jbig2', ...
        with open(f"page{page_index+1}_x{xref}.{ext}", "wb") as f:
            f.write(image_bytes)

doc.close()

extract_image() returns the image in its native format and resolution — no re-encoding, no quality loss. That’s the key advantage over rendering: a 300-DPI scanned page stays 300 DPI.

Get each image’s position on the page

The bytes alone don’t tell you where the image sits — which you need to associate a figure with its caption, or to crop in context. Use page.get_image_rects():

page = doc[0]
for img in page.get_images(full=True):
    xref = img[0]
    for rect in page.get_image_rects(xref):
        print(f"xref {xref} at {rect}")  # rect is (x0, y0, x1, y1) in points

Filter out logos, icons, and noise

A real document is full of tiny repeated images — header logos, bullet icons, signature scribbles. Filter by dimension and dedupe by xref so a logo on every page is saved once:

seen = set()
MIN_W, MIN_H = 100, 100  # points; tune to your docs

for page in doc:
    for img in page.get_images(full=True):
        xref = img[0]
        if xref in seen:
            continue
        seen.add(xref)
        base = doc.extract_image(xref)
        w, h = base["width"], base["height"]
        if w < MIN_W or h < MIN_H:
            continue  # skip icons/logos
        with open(f"fig_x{xref}.{base['ext']}", "wb") as f:
            f.write(base["image"])

Handle CMYK and alpha masks

Two edge cases silently corrupt output if you ignore them:

  • CMYK images (common in print-origin PDFs) come back as 4-channel data that most viewers render with inverted colours. Convert through a Pixmap to RGB.
  • Soft masks (SMask) carry the alpha/transparency channel as a separate image object. Extracting the base image without recombining the mask drops transparency.
for page in doc:
    for img in page.get_images(full=True):
        xref = img[0]
        pix = fitz.Pixmap(doc, xref)
        if pix.n - pix.alpha >= 4:          # CMYK or more → convert to RGB
            pix = fitz.Pixmap(fitz.csRGB, pix)
        pix.save(f"x{xref}.png")            # PNG preserves alpha if present
        pix = None

Using fitz.Pixmap(doc, xref) instead of extract_image() lets PyMuPDF recombine the SMask and normalize the colourspace for you — at the cost of re-encoding to PNG. Use extract_image() when you want the untouched original; use Pixmap when you want a correct, viewable RGB/RGBA file.

Method 2: pdfplumber — images by bounding box

pdfplumber is handy when you care about where images sit relative to text — for example, cropping a figure plus its caption together. It exposes images as metadata, not bytes, so you render the crop.

pip install pdfplumber
import pdfplumber

with pdfplumber.open("report.pdf") as pdf:
    page = pdf.pages[0]
    for image in page.images:
        bbox = (image["x0"], image["top"], image["x1"], image["bottom"])
        crop = page.crop(bbox).to_image(resolution=200)
        crop.save(f"fig_{int(image['x0'])}_{int(image['top'])}.png")

pdfplumber is slower than PyMuPDF and renders rather than extracts native bytes, but its bbox model is the most ergonomic for layout-aware cropping. We compared the two libraries in depth in PyMuPDF vs pdfplumber.

Method 3: pdf2image — render full pages to images

When there’s no embedded image to extract — a vector chart, or you simply need the whole page as a bitmap for OCR or a vision model — rasterize the page. pdf2image wraps Poppler:

pip install pdf2image
# macOS: brew install poppler   |   Debian/Ubuntu: apt-get install poppler-utils
from pdf2image import convert_from_path

pages = convert_from_path("report.pdf", dpi=200)  # list of PIL images
for i, page in enumerate(pages):
    page.save(f"page_{i+1}.png", "PNG")

PyMuPDF can do the same without the Poppler system dependency, which matters in slim containers:

page = doc[0]
pix = page.get_pixmap(dpi=200)
pix.save("page_1.png")

Render at 150–200 DPI for OCR; 72–96 DPI is usually enough for a vision LLM and keeps the token/byte cost down. This is the path you take to feed pages to a local vision model like Gemma or any OCR step — see OCR PDF extraction in Python for the downstream half.

Method 4: pdfmux — figures with their captions for RAG

The previous methods give you anonymous bitmaps. For an LLM or RAG pipeline, an image with no context is nearly useless — the model can’t tell “Figure 3: Q4 revenue by region” from a header logo. pdfmux takes the opposite approach: it preserves the figure reference and its caption in reading order inside the Markdown, so the surrounding text tells the model what each image is.

pip install pdfmux
from pdfmux import process

result = process("report.pdf", quality="standard")
print(result.text)
# ...Markdown keeps figure references inline, e.g.:
# ![Figure 3: Q4 revenue by region](fig-3)
# *Figure 3: Q4 revenue by region.*

pdfmux is an extraction pipeline focused on producing clean, LLM-ready Markdown — it doesn’t dump raw image bytes, and it’s not trying to replace PyMuPDF for that job. The pattern that works in practice is to use both: pdfmux for the structured text-and-figure-context layer, PyMuPDF for the raw image bytes, joined on page and bounding box.

import fitz
from pdfmux import process

doc = fitz.open("report.pdf")
result = process("report.pdf", quality="standard")  # text + figure context

# Markdown (with captions) → your RAG index
# Raw bytes (from PyMuPDF) → object storage, keyed by page + xref
for page in doc:
    for img in page.get_images(full=True):
        xref = img[0]
        base = doc.extract_image(xref)
        # store base["image"] under f"{page.number}:{xref}", link from the chunk

Because pdfmux runs entirely on CPU with no GPU and attaches a confidence score to every page, you can gate low-confidence pages for review before they ever reach your index.

Library comparison

LibraryGives youBest forNative bytes?System deps
PyMuPDFRaw embedded images + bboxExtracting original bitmaps fastYesNone
pdfplumberImage bbox metadataLayout-aware croppingNo (renders)None
pdf2imageFull page rastersOCR / vision-model inputNo (renders)Poppler
pdfmuxMarkdown w/ figure contextRAG / LLM pipelinesNo (context, not bytes)None

For raw extraction speed and correctness, PyMuPDF is the workhorse — it handled the 4,180-page test corpus in our 200-PDF benchmark at roughly 0.01s per page for image listing. Reach for the others when bytes aren’t actually what you need.

Common pitfalls

  1. Inline images (BI ... EI operators) aren’t returned by get_images(). They’re rare and usually tiny; if you must capture them, render the page region by bbox instead.
  2. Duplicate images appear once per page they’re placed on. Dedupe by xref (one object, reused) — not by page — or you’ll save the same logo 40 times.
  3. JBIG2 / CCITT (fax-style bilevel scans) extract as their native codec. Most viewers won’t open a raw .jbig2; convert via fitz.Pixmap to PNG.
  4. “My image is all black.” Almost always a CMYK or SMask issue — route through fitz.Pixmap(fitz.csRGB, pix) as shown above.
  5. Vector charts return nothing. There’s no raster to extract; render the bounding box with get_pixmap(clip=rect).

FAQ

What’s the best library to extract images from a PDF in Python? PyMuPDF (pip install pymupdf). It returns the original embedded image bytes at native resolution via doc.extract_image(xref), exposes bounding boxes with get_image_rects(), and has no system dependencies. Use pdf2image only when you need to rasterize whole pages.

How do I extract images without losing quality? Use PyMuPDF’s extract_image(), which returns the untouched original bytes — no re-encoding. Avoid rendering methods (get_pixmap, pdf2image) for embedded images; rendering resamples and can degrade a high-DPI scan.

How do I get the image’s position on the page? page.get_image_rects(xref) returns the rectangle(s) where an image is placed, in PDF points. Combine it with the text layout to pair a figure with its caption.

Can I extract images from a scanned PDF? A scanned PDF usually is one full-page image per page — extract it directly with get_images(), or render the page if it’s stored as vector-wrapped content. To tell which you have, see detecting scanned vs digital PDFs.

Why is my extracted image inverted or all black? It’s CMYK or has a soft-mask alpha channel. Convert through fitz.Pixmap(fitz.csRGB, fitz.Pixmap(doc, xref)) and save as PNG to normalize the colourspace and recombine transparency.

Keep reading

Last updated: June 2026