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.
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)
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.
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.
Related
- Converting PDFs to Images and Back with Python — rendering options, DPI and image-to-PDF assembly
- Fix TesseractNotFoundError in Python — the same wrapper-without-binary problem for OCR
- Fix tabula-py Java Not Found Error — another Python library that shells out to an external runtime
- How to Extract Tables from Scanned PDFs — a typical consumer of pdf2image renders