PDF Rendering and OCR Pipeline#
The PDF rendering and OCR pipeline covers two sequential stages within Docling's StandardPdfPipeline:
- Preprocess —
PagePreprocessingModelrasterizes each page to PIL images via a PDFium backend, extracts PDF text cells, and computes a text-quality score. - OCR — A pluggable OCR engine (Tesseract, EasyOCR, RapidOCR, etc.) receives cropped region images and produces
TextCellobjects that are merged with the existing PDF-extracted cells.
These are the first two of five concurrent threaded stages . OCR runs after Preprocess because it depends on both the page image and the layout-cluster predictions generated there.
Key source files:
| File | Role |
|---|---|
page_preprocessing_model.py | Page image rendering and cell extraction |
pypdfium2_backend.py | PDFium get_page_image() with supersampling |
base_ocr_model.py | get_ocr_rects() and cell-merging logic |
tesseract_ocr_cli_model.py | Tesseract CLI OCR invocation |
pipeline_options.py | OcrMode, OcrOptions, PdfPipelineOptions |
Page Rendering (Preprocess Stage)#
PagePreprocessingModel.__call__() runs two sub-steps for each valid page: image population then cell extraction.
Image population — _populate_page_images() always renders the page at scale=1.0 to warm the image cache, then renders again at options.images_scale if set, storing that as page._default_image_scale. Rendering delegates to page._backend.get_page_image(scale, cropbox).
Inside PyPdfiumPageBackend.get_page_image() : the page is rendered by pypdfium2 at scale × 1.5 — a supersampling factor applied to sharpen sub-pixel edges in vector content — and the resulting bitmap is then PIL-resized down to (round(width × scale), round(height × scale)). The native bitmap is closed immediately after .to_pil().copy() to release the underlying C memory before the lock is released .
Cell extraction — _parse_page_cells() calls page._backend.get_segmented_page() to extract PDF text/bitmap cells. It also aggregates text-quality scores across cells and stores the 10th-percentile score as conv_res.confidence.pages[page_no].parse_score — a signal that downstream logic can use to detect pages with corrupt or missing text layers .
The images_scale value is threaded from PdfPipelineOptions.images_scale (default 1.0; the CLI tool defaults to 2.0) . Values above 2.0 are known to cause bugs .
Supersampling and Raster-Only Detection#
The 1.5× supersample → resize strategy sharpens vector PDFs but is lossy for scanned (raster-only) pages: the scan's pixels are already rasterized, so the round-trip can silently drop borderline OCR text. PR #3803 documents an isolated case where an entire sentence was lost (page CER 0.184 vs ~0.05 for direct rendering).
The fix (PR #3803 — OPEN) introduces _is_raster_only() in a new shared base ManagedPdfiumPageBackend. The method walks page.get_objects() up to a depth of 16 (traversing form XObjects, since scans are frequently form-wrapped), and classifies a page as raster-only when it contains exclusively image objects with no text, vector, or annotation content. For raster-only pages, get_page_image() renders directly at the requested scale — skipping the supersample entirely — and crops any pixel excess from PDFium's ceil() rounding rather than resampling .
Tuning the supersample factor: A new PdfBackendOptions.supersample_factor field (default 1.5) lets callers override or disable supersampling. Setting it to 1.0 produces direct rendering on all pages regardless of content type .
The shared rendering logic is also hoisted into the base class so the fix applies consistently to both DoclingParseDocumentBackend and PyPdfiumDocumentBackend .
OCR Region Detection#
Before invoking any OCR engine, BaseOcrModel.get_ocr_rects() computes which page regions should be OCR'd. The strategy is controlled by OcrOptions.mode :
| Mode | Behavior |
|---|---|
DEFAULT / PDF_AWARE_LAYOUT_REGIONS | Keep only layout clusters that either overlap a bitmap PDF cell or have no PDF text cell overlap — targets genuinely raster regions |
LAYOUT_REGIONS | Use every layout cluster bbox regardless of PDF text content |
FULL_PAGE | Single rectangle spanning the entire page |
PDF-aware detection (_find_pdf_aware_layout_ocr_rects()) builds two rtree spatial indexes — one for PDF text cells, one for bitmap rects — then iterates over layout clusters and retains any cluster that (a) intersects a bitmap rect or (b) has no intersection with any text cell. This is the mechanism that avoids redundant OCR on pages with a reliable programmatic text layer.
Surviving rects pass through _deduplicate_rects(): they are rasterized into a binary image, dilated by a 20×20 kernel to merge nearby boxes into connected components, and the bounding boxes of those components are returned as the final OCR input rects. The method also returns an area_frac coverage ratio that callers can inspect.
force_full_page_ocr=True (deprecated) is a backward-compatibility shim that sets mode = OcrMode.FULL_PAGE . Prefer setting mode directly.
Tesseract CLI: Per-Region Rendering and Invocation#
TesseractOcrCliModel is the reference implementation of the OCR stage. Its __call__() loop:
- Calls
get_ocr_rects(page)to get the target regions. - For each non-zero-area rect, renders a high-resolution crop:
page._backend.get_page_image(scale=self.scale, cropbox=ocr_rect)— whereself.scale = 3(hard-coded), yielding 216 DPI from a 72 DPI base . - Writes the crop to a temp PNG, then runs OSD (
tesseract --psm 0 -l osd) to detect page orientation. If orientation ≠ 0°, the image is rotated before OCR . - Runs
tesseract <file> stdout tsvand parses the TSV result intoTextCellobjects, remapping Tesseract pixel coordinates back to PDF page space viatesseract_box_to_bounding_rectangle(). - Calls
post_process_cells()to merge OCR cells with the existing PDF text cells, usingPDF_FIRSTpriority inPDF_AWARE_LAYOUT_REGIONSmode (PDF cells win at overlapping positions) andOCR_FIRSTinLAYOUT_REGIONSmode.
Security: Language identifiers, tessdata paths, and file names are sanitized against injection at construction time via static methods .
Debug visualization: Set settings.debug.visualize_ocr = True to write annotated images (yellow OCR rects, magenta OCR cells, gray PDF cells) to settings.debug.debug_output_path .
Configuration Reference#
All rendering and OCR options are set on PdfPipelineOptions and OcrOptions subclasses :
| Parameter | Default | Effect |
|---|---|---|
PdfPipelineOptions.images_scale | 1.0 | Rasterization scale for page images; CLI default 2.0; max 2.0 |
PdfPipelineOptions.do_ocr | True | Enables the OCR stage entirely |
PdfPipelineOptions.ocr_options | OcrAutoOptions() | Selects and configures the OCR engine |
OcrOptions.mode | OcrMode.DEFAULT | Drives get_ocr_rects() strategy (see table above) |
OcrOptions.force_full_page_ocr | False | Deprecated — sets mode = FULL_PAGE; use mode directly |
TesseractCliOcrOptions.psm | None | Tesseract page segmentation mode (0–13) |
TesseractCliOcrOptions.tesseract_cmd | "tesseract" | Path/name of the Tesseract binary |
PdfBackendOptions.supersample_factor | 1.5 | PDFium render upscale ratio (set 1.0 to disable); see PR #3803 |
OcrMode.DEFAULT is wired to PDF_AWARE_LAYOUT_REGIONS — the most selective mode, which skips OCR on pages that already have a good text layer .
For fully scanned PDFs, use OcrMode.FULL_PAGE. In that mode post_process_cells() discards all non-OCR word/char cells so stale programmatic cells don't corrupt downstream table structure extraction . If the layout model classified the scan as a PictureItem, export with traverse_pictures=True to surface the OCR text .