Skip to content

OCR (Optical Character Recognition)

Extract text from images and scanned PDFs. Xberg 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.

See the OcrConfig reference for all configuration options.

Eight OCR backends — pick based on platform, accuracy needs, and language coverage.

Tesseract PaddleOCR Sceptre Candle GLM-OCR Candle TrOCR Candle DeepSeek-OCR Candle PaddleOCR-VL VLM
Speed Fast Model-dependent Fast on CPU Moderate Moderate Moderate Moderate Slow (API latency)
Accuracy Good Excellent Good Excellent Good Excellent Excellent Highest
Languages 100+ 80+ (11 script families) 6 EasyOCR gen2 groups All 100+ 20+ (CJK + Latin) 20+ (CJK + Latin) All (provider-dependent)
Installation System package Built-in (native) or Python package Built-in or Cargo feature Built-in Built-in Built-in Built-in API key only
Model size ~10 MB Mobile ~8 MB, Server ~120 MB Downloaded on first use ~3 GB ~250 MB ~4 GB ~2.5 GB None (cloud-hosted)
GPU support No Yes No (CPU-only in Xberg 1.1) Yes (Metal/CUDA) Yes (Metal/CUDA) Yes (Metal/CUDA) Yes (Metal/CUDA) N/A (server-side)
Platform All (including Wasm) All except Wasm Native desktop/server only Native only Native only Native only Native only All
Cost Free Free Free Free Free Free Free Per-token API cost
Backend Strengths Trade-offs Choose it when
Tesseract Broad language and platform support; low runtime overhead; hierarchical hOCR output Requires a system package and separately installed language data; less reliable on scene text and difficult scans You need the default backend, WebAssembly support, or a small deployment footprint
PaddleOCR Strong recognition quality; mobile and server model tiers; GPU support; strong CJK coverage Larger model downloads; server models can be slow and memory-intensive on CPU Recognition quality is the priority, especially for CJK documents, and you can budget for the model/runtime cost
Sceptre CRAFT detection and CRNN recognition; eight EasyOCR Gen2 language groups CPU-only; tract mobile builds and opt-in worker-hosted WASM have larger artifacts You need EasyOCR Gen2 recognition without a Python runtime

The remaining backends cover larger-model and hosted use cases:

  • Candle GLM-OCR — Excellent accuracy with VLM-level reasoning on 0.9B-param GLM model. Pure Rust, GPU-accelerated (Metal on macOS, CUDA on Linux). Region-aware layout dispatch. First download ~3 GB.
  • Candle TrOCR — Smaller model footprint (~250 MB) with solid accuracy across languages. Pure Rust, GPU-accelerated. Good balance of speed and quality.
  • Candle DeepSeek-OCR — Deep learning-based OCR combining SAM + CLIP + Qwen2 + DeepSeek MoE. Multilingual with strong CJK coverage. Pure Rust, GPU-accelerated. First download ~4 GB.
  • Candle PaddleOCR-VL — SigLIP vision encoder + Ernie-4.5 text decoder. Lightweight multilingual model with CJK and Latin support. Pure Rust, GPU-accelerated. First download ~2.5 GB.
  • 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]
xberg = { version = "1", features = ["paddle-ocr"] }

ONNX Runtime cannot link on wasm32 or the Android x86_64 emulator. On those targets, enable paddle-ocr-tract instead of paddle-ocr-ort (or the paddle-ocr alias) to run PaddleOCR through the pure-Rust tract engine, with no ONNX Runtime in the dependency graph. It runs the same DBNet detector, CRNN recognizer, and PP-LCNet text-line orientation classifier. paddle-ocr-tract is included in android-target. See Pure-Rust Inference (tract) for how the engine handles PaddleOCR’s shape requirements.

Cargo.toml
[dependencies]
xberg = { version = "1", features = ["paddle-ocr-tract"] }

Do not enable both an ORT PaddleOCR feature and paddle-ocr-tract in the same build — as with auto-rotate / auto-rotate-tract and layout-detection / layout-tract, the ORT and tract variants of a feature are mutually exclusive.

Limitations on tract:

  • CPU-only. GPU/execution-provider settings apply to the ONNX Runtime path only and are ignored on tract.
  • Slower than ONNX Runtime. tract trades throughput for portability on targets where ONNX Runtime cannot link at all; see Latency for measured ratios on other models.
  • Detection builds a plan per page shape. DBNet’s shape cannot be left symbolic under tract, so its plan is pinned to the exact dimensions each page resizes to and cached (four plans, least-recently-used eviction). Pages of a document nearly always share one extent, so the plan is built once; a new extent costs one build. Detection results are identical to the ONNX Runtime path — see PaddleOCR under tract for why the page is never padded into a fixed canvas.

Detection cost grows steeply with page size. Measured on macOS arm64 (single run, indicative, not a benchmark):

Model 640² 960² 1280²
v6 det medium 821 ms / 518 MiB 1650 ms / 981 MiB 4038 ms / 1652 MiB
v2 det mobile 140 ms / 90 MiB 224 ms / 175 MiB 706 ms / 306 MiB

det_limit_side_len defaults to 1024, at which the medium detector extrapolates to roughly 2 s and ~1.1 GiB per page under tract. On tract targets, prefer the mobile detection tier and lower det_limit_side_len — around 640 — to keep per-page latency and memory in check. The ORT default is unchanged.

Sceptre is included in published native desktop/server packages and the CLI. These builds use ONNX Runtime and download checksum-pinned models to the Hugging Face cache on first use. For a custom desktop/server Rust build, enable sceptre-ocr; add auto-rotate if you also need automatic orientation correction.

Cargo.toml
[dependencies]
xberg = { version = "1", features = ["sceptre-ocr"] }

Android and iOS builds use the pure-Rust tract engine through sceptre-ocr-tract; use auto-rotate-tract for orientation correction. Mobile builds exclude Sceptre’s Hugging Face downloader. Resolve packaged application assets to accessible filesystem paths and set both backend_options.model.detector_path and backend_options.model.recognizer_path. WebAssembly support is excluded from the published default bundle because of model and runtime size. Source builds can enable the separate byte-fed class with wasm-pack build crates/xberg-wasm --target web --features sceptre-wasm; application code must instantiate and call that synchronous class inside its own Web Worker. The JavaScript host fetches the CRAFT and recognizer models and supplies their bytes. All Sceptre paths are CPU-only.

Python
import asyncio
from xberg import ExtractInput, extract, ExtractionConfig, OcrConfig
async def main() -> None:
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="tesseract", language="eng")
)
result = await extract(ExtractInput(uri="scanned.pdf"), config)
content: str = result.results[0].content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")
asyncio.run(main())

Specify multiple language codes separated by + (Tesseract) or as a list (PaddleOCR and VLM backends):

Python
import asyncio
from xberg import ExtractInput, extract, ExtractionConfig, OcrConfig
async def main() -> None:
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="tesseract", language="eng+deu+fra")
)
result = await extract(ExtractInput(uri="multilingual.pdf"), config)
content: str = result.results[0].content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")
asyncio.run(main())

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

Python
import asyncio
from xberg import ExtractInput, extract, ExtractionConfig, OcrConfig
async def main() -> None:
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="tesseract"),
force_ocr=True,
)
result = await extract(ExtractInput(uri="document.pdf"), config)
content: str = result.results[0].content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")
asyncio.run(main())

When disable_ocr is set, image files return empty content instead of raising MissingDependencyError:

disable_ocr.py
from xberg import ExtractInput, ExtractionConfig, extract
config = ExtractionConfig(disable_ocr=True)
output = await extract(ExtractInput(kind="uri", uri="scanned.png"), config=config)
result = output.results[0]
# result.content will be empty — OCR was skipped
Python
import asyncio
from xberg import ExtractInput, extract, ExtractionConfig, OcrConfig
async def main() -> None:
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(backend="paddleocr", language="en") # model_tier="server" for max accuracy
)
result = await extract(ExtractInput(uri="scanned.pdf"), config)
content: str = result.results[0].content
preview: str = content[:100]
total_length: int = len(content)
print(f"Extracted content (preview): {preview}")
print(f"Total characters: {total_length}")
asyncio.run(main())

Select the backend with backend = "sceptre". Put Sceptre’s detection, recognition, concurrency, and model sections directly under backend_options; do not add a nested sceptre key.

xberg.toml
[ocr]
backend = "sceptre"
language = ["eng", "deu"]
[ocr.backend_options.recognition]
batch_size = 4
[ocr.backend_options.concurrency]
max_threads = 2
Path Purpose
backend_options.detection.* CRAFT thresholds, canvas size, magnification, box size, and line grouping
backend_options.recognition.* Decoder, beam width, batch size, allow/block lists, contrast retry, and confidence filtering
backend_options.concurrency.max_threads Cap Sceptre’s worker and inference thread pools
backend_options.model.cache_dir Override the Hugging Face cache root on desktop/server ORT builds
backend_options.model.registry_owner Use an approved mirror of the checksum-pinned model repositories
backend_options.model.detector_path Required CRAFT model path on Android/iOS tract builds
backend_options.model.recognizer_path Required selected Gen2 recognizer path on Android/iOS tract builds

Set languages with ocr.language; Xberg overrides backend_options.model.languages. Xberg also selects ORT or tract for the build target, so portable configuration should not set backend_options.model.backend.

xberg.toml (Android/iOS)
[ocr]
backend = "sceptre"
language = ["deu"]
[ocr.backend_options.model]
detector_path = "/app/models/craft_mlt_25k.onnx"
recognizer_path = "/app/models/latin_g2.onnx"

The paths must be resolved by the application from its bundle or asset system before extraction starts.

Sceptre provides all eight EasyOCR Gen2 recognition groups: english, latin, chinese_simplified, japanese, korean, cyrillic, telugu, and kannada. Xberg accepts group tokens and ISO aliases such as en/eng, de/deu, zh/zho, ja/jpn, ko/kor, ru/rus, te/tel, and kn/kan. English may be combined with one other group; two distinct non-English groups require different recognizers and fail validation before OCR.

Sceptre emits line-level quadrilaterals and recognition confidence. It does not provide Tesseract’s word/symbol hierarchy or a separate detection-confidence value. Tract inference uses a fixed CRAFT canvas and can require more memory than the default browser OCR path; run WebAssembly inference in a worker.

Ships in the published package by default on native desktop/server platforms — no feature flag or custom build needed. Set ocr.backend = "candle-glm-ocr" and the GLM-OCR model downloads automatically on first use (~3 GB), cached at ~/.cache/huggingface/. Not available on WebAssembly, Android, iOS, Dart, or Swift.

For a custom Rust crate build, the model is gated behind the candle-glm-ocr feature (or the candle-vlm-ocr umbrella feature):

Cargo.toml (Rust example)
[dependencies]
xberg = { version = "1", features = ["candle-glm-ocr"] }

GPU support:

  • Metal (macOS) — Default, F32 dtype (BF16 matmul unavailable in candle 0.10)
  • CUDA (Linux/Windows with NVIDIA GPU) — Auto-detected
  • CPU fallback — Slowest, but always available

Candle GLM-OCR dispatches by detected layout region using PP-DocLayout-V3. Each region runs through the appropriate task prompt (ocr/table/formula/chart/caption) and outputs are merged into reading-order markdown.

candle_glm_ocr.py
from xberg import ExtractInput, ExtractionConfig, OcrConfig, extract
# Paired mode: per-region dispatch (default)
config = ExtractionConfig(
force_ocr=True,
ocr=OcrConfig(
backend="candle-glm-ocr",
language=["en"],
backend_options={"layout_mode": "paired"},
),
)
output = await extract(ExtractInput(kind="uri", uri="document.pdf"), config=config)
result = output.results[0]
print(result.content)
# Whole-page mode: single OCR pass over entire page
config_whole = ExtractionConfig(
force_ocr=True,
ocr=OcrConfig(
backend="candle-glm-ocr",
language=["en"],
backend_options={"layout_mode": "whole_page"},
),
)
whole_page_output = await extract(
ExtractInput(kind="uri", uri="document.pdf"),
config=config_whole,
)
result_whole_page = whole_page_output.results[0]

Backend options:

Option Values Description
layout_mode "paired" (default), "whole_page" Paired: dispatch per-region via PP-DocLayout-V3. Whole-page: single OCR pass on entire page.
task "ocr" (default), "table", "formula", "chart", "caption" Task prompt for whole-page mode only; ignored in paired mode where the region type determines the prompt.
device "auto" (default), "cpu", "metal", "cuda" Device selection. Auto detects Metal on macOS, CUDA on Linux, CPU fallback.

DeepSeek-OCR — combination of SAM + CLIP encoder fused with Qwen2 decoder and DeepSeek V2 MoE for comprehensive multilingual document understanding. Markdown output.

Ships in the published package by default on native desktop/server platforms — no feature flag or custom build needed. Set ocr.backend = "candle-deepseek-ocr" and the model downloads automatically on first use (~4 GB), cached at ~/.cache/huggingface/. Not available on WebAssembly, Android, iOS, Dart, or Swift.

For a custom Rust crate build, the model is gated behind the candle-deepseek-ocr feature (or the candle-vlm-ocr umbrella feature):

Cargo.toml (Rust example)
[dependencies]
xberg = { version = "1", features = ["candle-deepseek-ocr"] }

GPU support:

  • Metal (macOS) — Default, F32 dtype
  • CUDA (Linux/Windows with NVIDIA GPU) — Auto-detected
  • CPU fallback — Slowest, but always available
candle_deepseek_ocr.py
from xberg import ExtractInput, ExtractionConfig, OcrConfig, extract
config = ExtractionConfig(
force_ocr=True,
ocr=OcrConfig(
backend="candle-deepseek-ocr",
language=["en"],
backend_options={"device": "auto", "model_path": "~/.cache/huggingface/"},
),
)
output = await extract(ExtractInput(kind="uri", uri="document.pdf"), config=config)
result = output.results[0]
print(result.content)

Supported languages: English, Chinese, Japanese, Korean, French, German, Spanish, Italian, Portuguese, Russian, Arabic, Hindi, Thai, Vietnamese, and others.

Model source: Download from Hugging Face Hub.

PaddleOCR-VL 1.5 — SigLIP vision encoder + Ernie-4.5 text decoder for lightweight multilingual document understanding. Markdown output.

Ships in the published package by default on native desktop/server platforms — no feature flag or custom build needed. Set ocr.backend = "candle-paddleocr-vl" and the model downloads automatically on first use (~2.5 GB), cached at ~/.cache/huggingface/. Not available on WebAssembly, Android, iOS, Dart, or Swift.

For a custom Rust crate build, the model is gated behind the candle-paddleocr-vl feature (or the candle-vlm-ocr umbrella feature):

Cargo.toml (Rust example)
[dependencies]
xberg = { version = "1", features = ["candle-paddleocr-vl"] }

GPU support:

  • Metal (macOS) — Default, F32 dtype
  • CUDA (Linux/Windows with NVIDIA GPU) — Auto-detected
  • CPU fallback — Slowest, but always available
candle_paddleocr_vl.py
from xberg import ExtractInput, ExtractionConfig, OcrConfig, extract
config = ExtractionConfig(
force_ocr=True,
ocr=OcrConfig(
backend="candle-paddleocr-vl",
language=["en"],
backend_options={"device": "auto", "model_path": "~/.cache/huggingface/"},
),
)
output = await extract(ExtractInput(kind="uri", uri="document.pdf"), config=config)
result = output.results[0]
print(result.content)

Supported languages: English, Chinese, Japanese, Korean, French, German, Spanish, Italian, Portuguese, Russian, and others.

Model source: Download from PaddlePaddle Hub.

Use a vision-language model (e.g. GPT-4o, Claude) as the OCR backend — each page is rendered and sent to the VLM. Cloud providers need an API key; local engines (Ollama, etc.) use the ollama/ prefix — see Local LLM Support.

Python
import asyncio
from xberg import ExtractInput, extract, 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(ExtractInput(uri="scan.pdf"), config)
print(result.results[0].content)
asyncio.run(main())

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

Higher DPI improves accuracy but increases processing time and memory.

DPI Trade-off
150 Fastest — lower accuracy, less memory
300 (default) Balanced — good accuracy, reasonable speed
600 Best accuracy — slower, more memory
Python
import asyncio
from xberg import (
ExtractInput,
extract,
ExtractionConfig,
OcrConfig,
TesseractConfig,
ImagePreprocessingConfig,
)
async def main() -> None:
config: ExtractionConfig = ExtractionConfig(
ocr=OcrConfig(
backend="tesseract",
tesseract_config=TesseractConfig(
preprocessing=ImagePreprocessingConfig(target_dpi=300),
),
),
)
result = await extract(ExtractInput(uri="scanned.pdf"), config)
content_length: int = len(result.results[0].content)
table_count: int = len(result.results[0].tables)
print(f"Content length: {content_length} characters")
print(f"Tables detected: {table_count}")
asyncio.run(main())

Beyond backend and DPI selection, OcrConfig and ExtractionConfig expose finer control over when OCR runs, page orientation correction, the native-text-to-OCR fallback decision, multi-backend fallback, and structured element output. See the OcrConfig reference for every field.

Set force_ocr_pages on ExtractionConfig to OCR only the listed pages (1-indexed). Unlisted pages use native text extraction. PDF only. Ignored when force_ocr is true; duplicates are deduplicated. Provide an ocr config for backend and language selection — defaults are used if absent.

force_ocr_pages.py
from xberg import ExtractInput, ExtractionConfig, extract
config = ExtractionConfig(force_ocr_pages=[1, 3, 5])
output = await extract(ExtractInput(kind="uri", uri="document.pdf"), config=config)
result = output.results[0]

Set auto_rotate on OcrConfig to detect page orientation (0/90/180/270 degrees) with Tesseract’s orientation-and-script detection before recognition. Pages rotated with high confidence are corrected before OCR — important for rotated scans. Defaults to false.

auto_rotate.py
from xberg import ExtractInput, ExtractionConfig, OcrConfig, extract
config = ExtractionConfig(
force_ocr=True,
ocr=OcrConfig(backend="tesseract", auto_rotate=True),
)
output = await extract(ExtractInput(kind="uri", uri="rotated_scan.pdf"), config=config)
result = output.results[0]

The native-text-to-OCR fallback decision (should a PDF page with an embedded text layer be re-OCR’d?) and multi-backend stage acceptance are governed by quality_thresholds (OcrQualityThresholds) on OcrConfig. When unset, compiled defaults apply. Frequently tuned fields:

Field Default Purpose
pipeline_min_quality 0.5 Minimum quality score (0.0-1.0) for a pipeline stage result to be accepted; below this, the next backend runs.
min_meaningful_words 3 Minimum count of meaningful words before native text is accepted instead of OCR.
min_alnum_ratio 0.3 Minimum alphanumeric ratio of non-whitespace characters.
critical_fragmented_word_ratio 0.80 Fraction of 1-2 character words that forces OCR regardless of other signals.
min_undecodable_ratio 0.5 Minimum fraction of non-whitespace characters that must be undecodable (PUA, replacement, or control garbage) before a page’s text layer is treated as unreadable and routed to OCR. See Automatic OCR for Undecodable Text Layers.
quality_thresholds.py
from xberg import ExtractionConfig, OcrConfig, OcrQualityThresholds, extract
config = ExtractionConfig(
ocr=OcrConfig(
quality_thresholds=OcrQualityThresholds(pipeline_min_quality=0.6),
),
)

PDFs with a font whose CID/glyph indices have no ToUnicode mapping (common with subset Identity-H fonts) often “extract” successfully but decode to garbage — Unicode Private Use Area (PUA) codepoints, replacement characters (U+FFFD), or non-whitespace control characters instead of legible text. Previously this garbage passed the native-text quality check and OCR never ran. Xberg now detects this case and routes the page to OCR automatically, the same as a scanned page (issue #1254).

A page is treated as having an undecodable text layer, and routed to OCR under OcrStrategy.Auto, when:

  • it has at least min_total_non_whitespace (default 64) non-whitespace characters — short snippets with a stray symbol don’t trip this check, and
  • at least min_undecodable_ratio (default 0.5) of those non-whitespace characters are undecodable (PUA, U+FFFD, or non-whitespace control characters).

Tune the ratio via OcrQualityThresholds.min_undecodable_ratio on OcrConfig.quality_thresholds:

undecodable_text_layer.py
from xberg import ExtractInput, ExtractionConfig, OcrConfig, OcrQualityThresholds, extract
config = ExtractionConfig(
ocr=OcrConfig(
quality_thresholds=OcrQualityThresholds(min_undecodable_ratio=0.3),
),
)
output = await extract(ExtractInput(kind="uri", uri="identity-h-subset-font.pdf"), config=config)
result = output.results[0]
# Pages whose text layer decoded to >=30% PUA/replacement/control garbage were OCR'd.

Set pipeline (OcrPipelineConfig) to try several backends in priority order (highest first) with quality-based fallback. Each stage’s output is scored; if it meets pipeline_min_quality, it is accepted, otherwise the next stage runs. Each stage (OcrPipelineStage) has a backend and priority (default 100), plus optional language, tesseract_config, paddle_ocr_config, vlm_config, and backend_options.

When pipeline is set, the top-level backend, vlm_fallback, and backend_options are ignored — configure each stage directly instead.

ocr_pipeline.py
from xberg import OcrConfig, OcrPipelineConfig, OcrPipelineStage, OcrQualityThresholds
ocr = OcrConfig(
pipeline=OcrPipelineConfig(
stages=[
OcrPipelineStage(backend="tesseract", priority=100),
OcrPipelineStage(backend="paddleocr", priority=50),
],
quality_thresholds=OcrQualityThresholds(pipeline_min_quality=0.6),
),
)

vlm_fallback (VlmFallbackPolicy) is ergonomic sugar over an explicit pipeline. When set and pipeline is None, an equivalent pipeline is synthesised automatically. It requires vlm_config to be set. When pipeline is explicitly set, vlm_fallback is ignored.

  • disabled (default) — single-backend mode.
  • on_low_quality — run the classical backend first; if the result scores below quality_threshold, retry the page with the VLM.
  • always — skip the classical backend; send every page to the VLM.
vlm_fallback.py
from xberg import LlmConfig, OcrConfig, VlmFallbackPolicy
ocr = OcrConfig(
vlm_fallback=VlmFallbackPolicy.on_low_quality(0.6),
vlm_config=LlmConfig(model="openai/gpt-4o-mini"),
)

OCR Elements and Word-Level Bounding Boxes

Section titled “OCR Elements and Word-Level Bounding Boxes”

Set element_config (OcrElementConfig) on OcrConfig to emit structured OCR elements with spatial and confidence data. When include_elements is true, the result’s ocr_elements field is populated with OcrElement entries — each carries text, geometry (a rectangle with left/top/width/height in pixels, or a 4-point quadrilateral for rotated text), confidence (detection and recognition, 0.0-1.0), level, optional rotation, and a 1-indexed page_number.

  • include_elements — populate ocr_elements. Defaults to false.
  • min_level — minimum hierarchy level to include: word, line (default), block, or page. Elements below this level are dropped.
  • min_confidence — drop elements with recognition confidence below this (0.0-1.0).
  • build_hierarchy — populate parent_id from spatial containment (Tesseract only).
ocr_elements.py
from xberg import (
ExtractInput,
ExtractionConfig,
OcrConfig,
OcrElementConfig,
OcrElementLevel,
extract,
)
config = ExtractionConfig(
force_ocr=True,
ocr=OcrConfig(
element_config=OcrElementConfig(
include_elements=True,
min_level=OcrElementLevel.WORD,
min_confidence=0.5,
),
),
)
output = await extract(ExtractInput(kind="uri", uri="scan.png"), config=config)
result = output.results[0]
for element in result.ocr_elements or []:
print(element.text, element.confidence.recognition, element.page_number)

PP-OCRv6 is the default PaddleOCR model generation (model_version defaults to "pp-ocrv6"). It adds a unified CJK+Latin+Japanese/Korean recognition model with three size tiers, selected via model_tier:

Tier Notes
medium Default.
small Smaller/faster, lower accuracy.
tiny Smallest/fastest, lowest accuracy.

model_tier defaults to "mobile", which auto-resolves to the v6 "medium" tier — you don’t need to set model_tier explicitly to get v6’s default behavior. A legacy "mobile"/"server" tier value under v6 also falls back to "medium".

Scripts outside v6’s unified coverage — Arabic, Cyrillic, Devanagari, Greek, Tamil, Telugu, and Thai — transparently fall back to the PP-OCRv5 per-script recognition models; no configuration change is needed to use those languages under v6.

To pin the legacy PP-OCRv5 fleet (per-script + unified recognition models, mobile/server tiers), set model_version to "pp-ocrv5" explicitly:

pin_pp_ocrv5.py
from xberg import ExtractInput, ExtractionConfig, OcrConfig, extract
config = ExtractionConfig(
ocr=OcrConfig(
backend="paddleocr",
backend_options={"model_version": "pp-ocrv5", "model_tier": "mobile"},
),
)
output = await extract(ExtractInput(kind="uri", uri="scanned.pdf"), config=config)

The candle-paddleocr-vl backend now defaults to PaddleOCR-VL 1.6 (SigLIP vision encoder + Ernie-4.5 text decoder), auto-downloading xberg-io/paddleocr-vl-1.6 — a checksum-pinned mirror of PaddlePaddle/PaddleOCR-VL-1.6 — on first use. It runs one of four tasks via backend_options.task: ocr (default), table, formula, or chart. See Candle PaddleOCR-VL above for installation and usage.

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
xberg extract scanned.pdf --ocr true
# Specific language
xberg extract french_doc.pdf --ocr true --ocr-language fra
# Specific backend
xberg extract chinese_doc.pdf --ocr true --ocr-backend paddle-ocr --ocr-language ch
# Sceptre on a native desktop or server
xberg extract german_doc.pdf --ocr true --ocr-backend sceptre --ocr-language deu
# Force OCR on all pages
xberg extract document.pdf --force-ocr true
# VLM OCR backend
xberg extract handwritten.pdf --force-ocr true --vlm-model openai/gpt-4o-mini
# Use a config file
xberg extract scanned.pdf --config xberg.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, sceptre, a candle-* backend, 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