Fix pdf2image Poppler Not Installed Error

The script that converts PDF pages to images works on a colleague's machine and fails everywhere else:

pdf2image.exceptions.PDFInfoNotInstalledError: Unable to get page count. Is poppler installed and in PATH?

Variants of the same failure include FileNotFoundError: [WinError 2] The system cannot find the file specified on Windows, PDFPageCountError: Unable to get page count. I/O Error: Couldn't open file when a path is wrong rather than Poppler, and the error appearing only inside a scheduled job or a Docker container while the same command works in an interactive terminal.

Root Cause

pdf2image is not a renderer. It is a thin Python wrapper that runs two command-line programs from the Poppler project: pdfinfo to count pages and pdftoppm (or pdftocairo) to render them. pip install pdf2image installs only the wrapper; the binaries come from the operating system. The first call, pdfinfo, fails when the executable is not found on the PATH of the running process — which is why the error mentions page count even when your code only asked for images. Interactive shells, cron, systemd services, IDE run configurations and containers each have their own PATH, so Poppler installed in one place, or added to PATH in .bashrc, is invisible to a job launched elsewhere.

What pdf2image actually runs convert_from_path first calls pdfinfo_from_path, which starts the pdfinfo executable through subprocess using the PATH of the current process. If pdfinfo is found it returns the page count and pdftoppm renders the pages to images. If it is not found, pdf2image raises PDFInfoNotInstalledError before any rendering happens. convert_from_path Python wrapper pdfinfo_from_path count pages first subprocess looks up pdfinfo on PATH pdftoppm renders pages Not on PATH PDFInfoNotInstalledError pip installs only the wrapper; the pdfinfo and pdftoppm binaries come from the OS

Minimal Diagnostic

Check what the Python process sees, not what your shell sees. Run this from the same environment that fails — inside the container, from the cron job, or from the IDE's run configuration.

# pip install pdf2image
import os
import shutil
import subprocess
import sys
from pathlib import Path

SAMPLE = Path("in/sample.pdf")

def poppler_report() -> None:
    print(f"python: {sys.executable}")
    print("PATH entries:")
    for entry in os.environ.get("PATH", "").split(os.pathsep):
        print(f"  {entry}")
    for tool in ("pdfinfo", "pdftoppm", "pdftocairo"):
        location = shutil.which(tool)
        print(f"{tool:>10}: {location or 'NOT FOUND'}")
        if location:
            try:
                out = subprocess.run([location, "-v"], capture_output=True, text=True, timeout=10)
                print(f"{'':>12}{(out.stderr or out.stdout).splitlines()[0]}")
            except (OSError, subprocess.TimeoutExpired) as exc:
                print(f"{'':>12}cannot execute: {exc}")
    if SAMPLE.exists() and shutil.which("pdfinfo"):
        result = subprocess.run(["pdfinfo", str(SAMPLE)], capture_output=True, text=True)
        print("pdfinfo on sample:", "ok" if result.returncode == 0 else result.stderr.strip())

if __name__ == "__main__":
    poppler_report()
python: /opt/app/.venv/bin/python
PATH entries:
  /opt/app/.venv/bin
  /usr/bin
  /bin
   pdfinfo: NOT FOUND
  pdftoppm: NOT FOUND
pdftocairo: NOT FOUND

NOT FOUND for all three means Poppler is missing or outside this PATH. If the tools are found but the sample fails, the problem is the PDF path or file permissions, and the error is PDFPageCountError rather than the not-installed error.

Fix: Install Poppler Where the Job Runs

Install the binaries through the platform's package manager, in the same environment that executes the code.

# Debian / Ubuntu (servers, WSL, most CI runners)
sudo apt-get update && sudo apt-get install -y poppler-utils

# RHEL / Fedora / Amazon Linux
sudo dnf install -y poppler-utils

# Alpine (small containers)
apk add --no-cache poppler-utils

# macOS
brew install poppler

# conda environments (any OS, including Windows)
conda install -c conda-forge poppler

For containers, install Poppler in the image, not at runtime:

FROM python:3.12-slim
# changed: install the Poppler binaries into the image itself
RUN apt-get update \
 && apt-get install -y --no-install-recommends poppler-utils \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "render_pages.py"]

Then confirm from Python, with a clear failure message if the environment is still wrong:

# pip install pdf2image
import shutil
from pathlib import Path
from pdf2image import convert_from_path
from pdf2image.exceptions import PDFInfoNotInstalledError, PDFPageCountError

SOURCE = Path("in/sample.pdf")

def render_first_page(pdf_path: Path, dpi: int = 150):
    if shutil.which("pdftoppm") is None:                      # changed: fail early with guidance
        raise SystemExit("Poppler not found on PATH: install poppler-utils in this environment")
    try:
        return convert_from_path(pdf_path, dpi=dpi, first_page=1, last_page=1)[0]
    except PDFInfoNotInstalledError as exc:                  # changed: explicit, actionable error
        raise SystemExit(f"pdfinfo missing although pdftoppm exists: {exc}")
    except PDFPageCountError as exc:
        raise SystemExit(f"Poppler ran but could not read {pdf_path}: {exc}")

if __name__ == "__main__":
    image = render_first_page(SOURCE)
    image.save("out/page-1.png")
    print(image.size)
Installing Poppler by platform On Debian and Ubuntu, apt-get install poppler-utils places binaries in /usr/bin, which is on PATH. RHEL and Fedora use dnf with the same result. Alpine uses apk. macOS uses Homebrew, which is on PATH for interactive shells but not always for launchd jobs. Windows has no system package, so a downloaded build must be added to PATH or passed as poppler_path. conda installs into the environment's own bin or Library folder. Platform Install On PATH for jobs Debian / Ubuntu apt-get poppler-utils yes RHEL / Fedora dnf poppler-utils yes Alpine apk poppler-utils yes macOS brew install poppler not for launchd Windows zip build set poppler_path conda conda-forge poppler only when activated

Variant Fix 1: Windows, or a Job with a Different PATH

On Windows there is no system package: download a Poppler build (the poppler-windows releases are widely used), unzip it, and either add its Library\bin folder to the system PATH or pass it explicitly. Explicit is more robust for scheduled tasks, whose PATH often differs from your user account's.

# pip install pdf2image
import os
from pathlib import Path
from pdf2image import convert_from_path

POPPLER_BIN = Path(os.environ.get("POPPLER_BIN", r"C:\tools\poppler-24.08.0\Library\bin"))

def render(pdf_path: Path, dpi: int = 200):
    if not (POPPLER_BIN / "pdftoppm.exe").exists():
        raise SystemExit(f"pdftoppm.exe not found in {POPPLER_BIN}; set POPPLER_BIN")
    return convert_from_path(pdf_path, dpi=dpi, poppler_path=str(POPPLER_BIN))

Reading the location from an environment variable keeps the path out of code, so the same script runs on a developer laptop and a server with different install locations. The same approach works for macOS launchd jobs, whose PATH does not include /opt/homebrew/bin: set POPPLER_BIN=/opt/homebrew/bin. For cron on Linux, the minimal cron PATH still includes /usr/bin, so package-manager installs work; tools installed under /usr/local/bin or a user directory need the variable. The broader story of environment differences in scheduled jobs is covered in fix a cron job not running a Python script, and the Windows equivalent in run Python scripts with Windows Task Scheduler.

Variant Fix 2: Remove the External Dependency

If you cannot install system packages — locked-down servers, serverless functions with size limits, users on managed laptops — replace pdf2image with PyMuPDF, which bundles its renderer inside the Python wheel. The return values can match pdf2image's list of PIL images so the rest of the code stays unchanged:

# pip install pymupdf pillow
from pathlib import Path
import pymupdf
from PIL import Image

def convert_from_path(pdf_path: Path, dpi: int = 200, first_page: int | None = None,
                      last_page: int | None = None, grayscale: bool = False) -> list[Image.Image]:
    """Drop-in replacement for pdf2image.convert_from_path using PyMuPDF."""
    images = []
    try:
        doc = pymupdf.open(pdf_path)
    except (pymupdf.FileDataError, RuntimeError) as exc:
        raise RuntimeError(f"cannot open {pdf_path}: {exc}") from exc
    with doc:
        start = (first_page or 1) - 1
        stop = last_page or doc.page_count
        for page in doc.pages(start, stop):
            pix = page.get_pixmap(dpi=dpi, alpha=False,
                                  colorspace=pymupdf.csGRAY if grayscale else pymupdf.csRGB)
            mode = "L" if grayscale else "RGB"
            images.append(Image.frombytes(mode, (pix.width, pix.height), pix.samples))
    return images

The wheel adds tens of megabytes but no system dependencies, and rendering is typically faster than spawning pdftoppm per call. Check the licence before shipping it inside a distributed product: MuPDF is AGPL unless a commercial licence is purchased, while Poppler is GPL. For internal scripts and services, neither usually matters. Rendering options and DPI guidance are in Converting PDFs to Images and Back.

pdf2image call versus the PyMuPDF replacement The left panel shows the original code importing convert_from_path from pdf2image, which depends on pdfinfo and pdftoppm binaries on PATH. The right panel shows the same call signature imported from a local module that uses PyMuPDF get_pixmap and returns PIL images, so the calling code does not change and no system packages are needed. Needs Poppler binaries from pdf2image import ( convert_from_path) pages = convert_from_path( path, dpi=300) # spawns pdfinfo + pdftoppm No system dependency from pdf_render import ( convert_from_path) pages = convert_from_path( path, dpi=300) # PyMuPDF inside the wheel

Verification

Add a start-up self-test so the failure appears at deploy time with a clear message, not hours later on the first document. The check renders a tiny generated PDF, so it needs no sample file on the server.

# pip install pdf2image pymupdf
import shutil
import tempfile
from pathlib import Path
import pymupdf
from pdf2image import convert_from_path

def poppler_self_test() -> None:
    missing = [t for t in ("pdfinfo", "pdftoppm") if shutil.which(t) is None]
    assert not missing, f"missing Poppler tools on PATH: {missing}"
    with tempfile.TemporaryDirectory() as tmp:
        sample = Path(tmp) / "selftest.pdf"
        with pymupdf.open() as doc:                         # build a one-page PDF on the fly
            page = doc.new_page(width=200, height=100)
            page.insert_text((20, 50), "poppler self-test")
            doc.save(sample)
        images = convert_from_path(sample, dpi=72)
        assert len(images) == 1 and images[0].size == (200, 100), f"unexpected render {images}"
    print("poppler self-test passed")

if __name__ == "__main__":
    poppler_self_test()

Run it as a container health check or as the first step of the scheduled job. A passing self-test proves the binaries are found and can execute in exactly the environment that matters, which the earlier manual checks in a shell cannot.

Keeping CI and Production in Step

The error keeps returning when system dependencies live only in someone's memory. Declare them next to requirements.txt and install them the same way everywhere the code runs — including the test pipeline, so a missing binary fails a pull request instead of a production job.

# .github/workflows/tests.yml
name: tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: System packages
        run: sudo apt-get update && xargs -a apt-packages.txt sudo apt-get install -y
      - name: Python packages
        run: pip install -r requirements.txt
      - name: Poppler self-test
        run: python -c "from selftest import poppler_self_test; poppler_self_test()"
      - name: Tests
        run: pytest -q
# apt-packages.txt — one system package per line, read by CI and the Dockerfile
poppler-utils
tesseract-ocr

Reading the same apt-packages.txt in the Dockerfile (COPY apt-packages.txt . && xargs -a apt-packages.txt apt-get install -y) means a dependency added for one environment is added for all of them. When a job moves to a new host, the file doubles as the checklist of what the host needs. The same list is the natural place to record Java for tabula and LibreOffice for document conversion, both of which fail with similarly confusing messages when absent.

FAQ

Does pip install poppler work? No — there is no official Poppler package on PyPI. Packages with similar names are unrelated or unmaintained. Use the OS package manager or conda-forge.

Why does it work in Jupyter but not in the script? The notebook kernel was probably started from a conda environment or a shell with a different PATH. Run the diagnostic from both and compare the PATH entries.

Do I need both pdfinfo and pdftoppm? Yes for convert_from_path: it counts pages first. pdftocairo is only needed when you pass use_pdftocairo=True.

Is the error ever caused by the PDF itself? Not this one. A corrupt or encrypted PDF raises PDFPageCountError or PDFSyntaxError after Poppler starts successfully.

Part of Converting PDFs to Images and Back with Python.