distillPDF OCR — setup guide¶
distillPDF OCRs scanned and image-only pages with one of two engines:
- Tesseract — bundled in the wheel, works out of the box. Fast, flat text.
- granite-docling — a vision-language model that also recovers tables, headings and reading order. Slower, and you install its runtime yourself (see below).
Tesseract — available by default¶
The base install already contains everything: Tesseract is compiled into the wheel, so there is no extra package, no model download, and it runs fully offline.
pip install distillpdf
import distillpdf
doc = distillpdf.open("scan.pdf")
doc.run_ocr() # bundled Tesseract — no setup needed
doc.to_pdf("scan.searchable.pdf") # original scan + invisible text layer
doc.to_html("scan.html") # reuses the same OCR pass (cached)
Or from the command line:
distillpdf scan.pdf --ocr # → scan.searchable.pdf
English, Portuguese and Norwegian language data ship in the wheel, and the document
language is auto-detected from a few sample pages. Pin a language with
doc.run_ocr(languages=["eng"]), or point TESSDATA_PREFIX at a tessdata folder for other
languages.
Tesseract produces flat text only — no table structure. When that matters, use granite.
granite-docling — the accurate engine¶
For higher fidelity, granite-docling is the recommended model: it reads each page as an
image and emits structure — tagged headings, reading order, and real tables (OTSL) that
distillPDF renders as proper <table> HTML.
The trade-off is speed:
| Engine | Speed | Char accuracy | Output |
|---|---|---|---|
| Tesseract (default) | ~0.8 s/page | ~95% | flat text |
| granite-docling | ~6 s/page (GPU) | ~97% | headings, reading order, tables |
granite needs a GPU to be practical — on CPU it runs at minutes per page. If you have no GPU, stay on Tesseract.
Because the model runtime is genuinely platform- and hardware-specific, it isn't a pip extra — you install one of the paths below. Not sure which? distillpdf prints the right commands for your machine:
print(distillpdf.ocr.install_help("granite"))
Installation paths¶
macOS (Apple Silicon) — MLX¶
Recommended on Apple Silicon: granite-docling on MLX runs on the Metal GPU.
pip install mlx-vlm "transformers>=4.57,<5" pillow
Windows / Linux / Intel Mac — PyTorch¶
Recommended elsewhere: torch ships prebuilt wheels for every platform and Python, so this
installs cleanly with no C++ compiler.
pip install torch "transformers>=4.57,<5" pillow
NVIDIA GPU: PyPI's default torch is CPU-only and slow for a VLM. Install the CUDA
build instead and the engine uses it automatically:
pip install torch --index-url https://download.pytorch.org/whl/cu124
pip install "transformers>=4.57,<5" pillow
Force a device with OcrConfig(device="cuda"|"cpu"). (PyTorch-on-Mac MPS is not used —
granite/idefics3 misbehaves there; Mac uses MLX for GPU.)
llama.cpp (GGUF) — lightweight alternative¶
A smaller runtime with no PyTorch, selected explicitly with
engine="granite-docling-gguf":
pip install llama-cpp-python huggingface-hub pillow
On Windows, llama-cpp-python may build from source when no prebuilt wheel matches your
Python (needs MSVC). Use a prebuilt wheel:
pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu
pip install huggingface-hub pillow
or just use the PyTorch path above (no compiler).
Why
transformers>=4.57,<5? transformers 5.x changed the idefics3 image processor and fails to load granite-docling;>=4.57is the floor that supports it. mlx-vlm's own dependency range can pull in 5.x, so pin it explicitly.Optional:
pip install tqdmto get a progress bar during OCR.
Using granite¶
engine="granite" picks the right runtime for your platform automatically (MLX on Apple
Silicon, PyTorch elsewhere); name "granite-docling-gguf" to use the llama.cpp runtime.
import distillpdf
doc = distillpdf.open("scan.pdf")
doc.run_ocr(engine="granite") # MLX on Apple Silicon, PyTorch elsewhere
doc.to_html("scan.html") # tables come out as real <table> markup
doc.to_pdf("scan.searchable.pdf") # same pass, cached — the model never re-runs
doc.run_ocr(engine="granite-docling-gguf") # the llama.cpp runtime, explicitly
doc.run_ocr(engine="granite", only={1, 2}) # try a couple of pages before committing
distillpdf scan.pdf --ocr --ocr-engine accurate # granite from the CLI
distillpdf --list-ocr-engines # what's installed and runnable
Models / cache¶
The default granite models are public and download on first use. The PyTorch and
GGUF runtimes put weights in a visible ./ocr_model/ folder (override with
OcrConfig(model_dir=...)); the MLX runtime fetches the model automatically (mlx-vlm
manages it). No token is needed for the default models. For a gated/private repo, set
HF_TOKEN (env or a .env; or OcrConfig(hf_token=..., store_token=True)).