PDF-to-JPG sounds simple but the choice of library determines whether you ship a 50MB native dependency (Poppler) or a single Python wheel (PyMuPDF). For one-off scripts, pdf2image is the fastest path. For services, PyMuPDF is more deployment-friendly. The API is the right call when you don't want either dep.

Method 1: pdf2image (pdf2image + Poppler)

pdf2image wraps Poppler's pdftoppm — the same engine PDF readers use. Best fidelity but requires Poppler installed.

pip install pdf2image
apt install poppler-utils  # required
# macOS: brew install poppler
from pdf2image import convert_from_path
from pathlib import Path

def pdf_to_jpg(in_path: str, out_dir: str, dpi: int = 200) -> list[str]:
    out_dir = Path(out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    images = convert_from_path(in_path, dpi=dpi, fmt="jpeg", thread_count=4)
    paths = []
    for i, img in enumerate(images):
        out_path = out_dir / f"page-{i+1:03d}.jpg"
        img.save(out_path, "JPEG", quality=90)
        paths.append(str(out_path))
    return paths

pages = pdf_to_jpg("document.pdf", "./pages", dpi=200)
print(f"wrote {len(pages)} pages")

DPI guide:

  • 72 — screen-only, low quality. Use for thumbnails.
  • 150 — web-quality. Good default for most PDFs.
  • 200-300 — print-quality. Use for documents you'll print.
  • 600+ — archival. Massive files; use sparingly.

thread_count=4 parallelizes page rendering — useful for multi-page PDFs.

Method 2: PyMuPDF (fitz, faster + no Poppler)

PyMuPDF wraps MuPDF — a fast, lightweight PDF rendering engine. Single pip install, no system deps.

pip install PyMuPDF
import fitz  # PyMuPDF
from pathlib import Path

def pdf_to_jpg(in_path: str, out_dir: str, dpi: int = 200) -> list[str]:
    out_dir = Path(out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    doc = fitz.open(in_path)
    paths = []
    zoom = dpi / 72  # 72 = native PDF DPI
    matrix = fitz.Matrix(zoom, zoom)
    for i, page in enumerate(doc):
        pix = page.get_pixmap(matrix=matrix, alpha=False)
        out_path = out_dir / f"page-{i+1:03d}.jpg"
        pix.save(out_path)
        paths.append(str(out_path))
    doc.close()
    return paths

pages = pdf_to_jpg("document.pdf", "./pages", dpi=200)

PyMuPDF is ~2x faster than pdf2image and has no native deps to install. The license is AGPL — for commercial use you may need to buy a license. For most internal tools and open-source projects, it's free.

Method 3: ChangeThisFile API (no installs)

If you don't want Poppler or PyMuPDF in your environment, the API runs Poppler server-side. Free tier covers 1,000 conversions/month (no card).

import requests
import zipfile
from pathlib import Path

API_KEY = "ctf_sk_your_key_here"

def pdf_to_jpg(in_path: str, out_dir: str) -> list[str]:
    with open(in_path, "rb") as f:
        response = requests.post(
            "https://changethisfile.com/v1/convert",
            headers={"Authorization": f"Bearer {API_KEY}"},
            files={"file": f},
            data={"source": "pdf", "target": "jpg"},
            timeout=120,
        )
    response.raise_for_status()

    out_dir = Path(out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    # Multi-page PDFs return a zip with one JPG per page
    if response.headers.get("Content-Type", "").startswith("application/zip"):
        zip_path = out_dir / "pages.zip"
        zip_path.write_bytes(response.content)
        with zipfile.ZipFile(zip_path) as zf:
            zf.extractall(out_dir)
        zip_path.unlink()
        return sorted(str(p) for p in out_dir.glob("*.jpg"))
    else:
        out_path = out_dir / "page-001.jpg"
        out_path.write_bytes(response.content)
        return [str(out_path)]

pages = pdf_to_jpg("document.pdf", "./pages")

The API renders at 150 DPI by default. Pass dpi=300 in form data for higher quality.

When to use each

ApproachBest forTradeoff
pdf2imageSelf-hosted batch jobs, max fidelityRequires Poppler (~50MB system dep)
PyMuPDFSingle pip install, fast, no native depsAGPL license — check before commercial use
ChangeThisFile APINo Poppler/PyMuPDF in your env, edge runtimesNetwork call, file size limit (25MB free)

Production tips

  • Start at 150 DPI. Most use cases (web previews, thumbnails, OCR input) don't need more. 300+ explodes file size and processing time.
  • Use thread_count > 1 for multi-page PDFs. Page rendering is CPU-bound and embarrassingly parallel.
  • Set quality=85-90 for JPEG output. Default 95 is overkill for screen viewing. 85 cuts file size by ~30% with no visible loss.
  • For OCR input, PNG is sometimes better. Tesseract works fine with JPG, but PNG avoids JPEG compression artifacts that can confuse OCR on small text. Use PNG when accuracy matters more than file size.
  • Watch for password-protected PDFs. Both pdf2image and PyMuPDF support passwords (userpw= parameter / doc.authenticate(pwd)). The API supports password= form field.

For most Python projects, PyMuPDF is the easiest deploy. For Poppler-already-installed shops, pdf2image is more idiomatic. For environments without either, the API. Free tier covers 1,000 conversions/month.