Skip to content

OCR (Optical Character Recognition)

Extract text from images and scanned PDFs. Kreuzberg automatically determines when OCR is needed — images always require it, scanned PDFs trigger it per-page, and hybrid PDFs only OCR the pages that lack a text layer. Set force_ocr=True to OCR all pages regardless.

Kreuzberg supports four OCR backends. Pick based on your platform, accuracy needs, and language coverage.

Tesseract PaddleOCR EasyOCR VLM
Speed Fast Very fast Moderate Slow (API latency)
Accuracy Good Excellent Excellent Highest
Languages 100+ 80+ (11 script families) 80+ All (provider-dependent)
Installation System package Built-in (native) or Python package Python package only API key only
Model size ~10 MB Mobile ~8 MB, Server ~120 MB ~100 MB None (cloud-hosted)
GPU support No Yes Yes N/A (server-side)
Platform All (including WASM) All except WASM Python only All
Cost Free Free Free Per-token API cost

When to use which:

  • Tesseract — Default choice. Works everywhere, low overhead, broadest platform support.
  • PaddleOCR — Best speed-to-accuracy ratio. Preferred for CJK languages. Mobile tier is fast; server tier maximizes accuracy with GPU.
  • EasyOCR — Highest accuracy with deep learning models. Python-only, heavier dependency.
  • VLM — Best for handwritten text, poor scans, Arabic/Farsi, and complex layouts. Requires an API key and incurs per-token costs. See LLM Integration for full details.
Terminal
brew install tesseract

Additional language packs:

Terminal
# macOS — all languages
brew install tesseract-lang
# Ubuntu/Debian — individual languages
sudo apt-get install tesseract-ocr-deu # German
sudo apt-get install tesseract-ocr-fra # French
# Verify installed languages
tesseract --list-langs

Built in via the paddle-ocr feature flag. Models download automatically on first use — no extra installation needed.

Cargo.toml (Rust example)
[dependencies]
kreuzberg = { version = "4.0", features = ["paddle-ocr"] }
Terminal
pip install "kreuzberg[easyocr]"
Python
from kreuzberg import extract_file_sync, ExtractionConfig, OcrConfig
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="tesseract", language="eng")
)
result = extract_file_sync("scanned.pdf", config=config)
content: str = result.content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")

Specify multiple language codes separated by + (Tesseract) or as a list (EasyOCR/PaddleOCR):

Python
from kreuzberg import extract_file_sync, ExtractionConfig, OcrConfig
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="tesseract", language="eng+deu+fra")
)
result = extract_file_sync("multilingual.pdf", config=config)
content: str = result.content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")

Process PDFs with OCR even when they have a text layer:

Python
from kreuzberg import extract_file_sync, ExtractionConfig, OcrConfig
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="tesseract"),
force_ocr=True,
)
result = extract_file_sync("document.pdf", config=config)
content: str = result.content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")
Python
from kreuzberg import extract_file_sync, ExtractionConfig, OcrConfig
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="easyocr", language="en")
)
# EasyOCR-specific options (use_gpu, beam_width, etc.) go in easyocr_kwargs,
# not in OcrConfig — OcrConfig only accepts backend, language, and backend-specific configs.
result = extract_file_sync("scanned.pdf", config=config, easyocr_kwargs={"use_gpu": True})
content: str = result.content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")

Skip OCR entirely, even for image files that would normally require it. When disable_ocr is set, image files return empty content instead of raising a MissingDependencyError:

disable_ocr.py
from kreuzberg import ExtractionConfig, extract_file_sync
config = ExtractionConfig(disable_ocr=True)
result = extract_file_sync("scanned.png", config=config)
# result.content will be empty — OCR was skipped
Python
from kreuzberg import extract_file_sync, ExtractionConfig, OcrConfig
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="paddleocr", language="en") # model_tier="server" for max accuracy
)
result = extract_file_sync("scanned.pdf", config=config)
content: str = result.content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")

Use a vision-language model (for example, GPT-4o, Claude) as the OCR backend. Each page is rendered as an image and sent to the VLM for text extraction. Cloud providers require an API key; local engines like Ollama do not — just start the server and use the ollama/ prefix (for example, ollama/llama3.2-vision). See Local LLM Support for setup details.

Python
import asyncio
from kreuzberg import extract_file, ExtractionConfig, OcrConfig, LlmConfig
async def main() -> None:
config = ExtractionConfig(
force_ocr=True,
ocr=OcrConfig(
backend="vlm",
vlm_config=LlmConfig(model="openai/gpt-4o-mini"),
),
)
result = await extract_file("scan.pdf", config=config)
print(result.content)
asyncio.run(main())

For more on VLM OCR, including custom prompts, supported providers, and API key configuration, see LLM Integration.

Image resolution affects both accuracy and speed. Higher DPI improves accuracy but increases processing time and memory usage.

DPI Trade-off
150 Fastest — lower accuracy, less memory
300 (default) Balanced — good accuracy, reasonable speed
600 Best accuracy — slower, more memory
Python
from kreuzberg import (
extract_file_sync,
ExtractionConfig,
OcrConfig,
TesseractConfig,
ImagePreprocessingConfig,
)
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(
backend="tesseract",
tesseract_config=TesseractConfig(
preprocessing=ImagePreprocessingConfig(target_dpi=300),
),
),
)
result = extract_file_sync("scanned.pdf", config=config)
content_length: int = len(result.content)
table_count: int = len(result.tables)
print(f"Content length: {content_length} characters")
print(f"Tables detected: {table_count}")

PaddleOCR supports 80+ languages across 11 script families (PP-OCRv5). Recognition models are downloaded on demand from HuggingFace:

Family Languages
English English, numbers, punctuation
Chinese Simplified/Traditional Chinese, Japanese
Latin French, German, Spanish, Portuguese, Italian, Polish, Dutch, Turkish, Vietnamese, and so on.
Korean Korean (Hangul)
Slavic Russian, Ukrainian, Belarusian, Bulgarian, Serbian, and so on.
Thai Thai script
Greek Greek script
Arabic Arabic, Persian, Urdu
Devanagari Hindi, Marathi, Sanskrit, Nepali
Tamil Tamil script
Telugu Telugu script

Models are cached locally after first download, so subsequent runs start immediately.

Terminal
# Basic OCR extraction
kreuzberg extract scanned.pdf --ocr true
# Specific language
kreuzberg extract french_doc.pdf --ocr true --ocr-language fra
# Specific backend
kreuzberg extract chinese_doc.pdf --ocr true --ocr-backend paddle-ocr --ocr-language ch
# Force OCR on all pages
kreuzberg extract document.pdf --force-ocr true
# VLM OCR backend
kreuzberg extract handwritten.pdf --force-ocr true --vlm-model openai/gpt-4o-mini
# Use a config file
kreuzberg extract scanned.pdf --config kreuzberg.toml --ocr true
Flag Description
--ocr true Enable OCR processing
--ocr-language <code> Language code (eng, deu, fra, ch, ja, ru, etc.)
--ocr-backend <backend> Engine: tesseract, paddle-ocr, easyocr, or vlm
--force-ocr true OCR all pages regardless of text layer
--vlm-model <model> VLM model for OCR (for example, openai/gpt-4o-mini). Implies --ocr-backend vlm