Constructdocs
section · guides

Scanned Pdfs

1 min read·guides / intake / scanned-pdfs

Scanned PDFs

Construct ingests PDFs through docling, which performs layout-aware extraction and ships its own built-in OCR for image-bearing pages. Most scanned PDFs work without additional setup — docling's OCR runs as part of the Python sidecar.

When extracted text is unusually sparse (below 50 characters per estimated page after OCR), the ingest record emits a droppedInfo entry with kind: "low-text-yield" and recoverable: true. This signals that the document is either heavily image-based with low OCR confidence, an empty/cover page, or genuinely contains little text.

Inspecting low-yield warnings

construct intake extraction-warnings --kind=low-text-yield

The CLI surfaces droppedInfo from every ingested document by default. Pass --strict to construct ingest to fail non-zero on any drops:

construct ingest large-scan-collection/ --strict

Improving recall on hard scans

Docling's OCR is sufficient for the common case. For scans with poor source quality (low contrast, handwritten margins, complex layouts), pre-processing with a dedicated OCR engine before ingestion usually beats expanding docling's defaults:

# Tesseract: produce a searchable PDF, then ingest
tesseract input.pdf output pdf
construct ingest output.pdf

# macOS pdfocr (Ventura+)
pdfocr -o output.pdf input.pdf
construct ingest output.pdf

# PaddleOCR (better on printed Asian scripts, see paddlepaddle.github.io/PaddleOCR)

Behavior trade-off

Docling's OCR runs synchronously during extraction. Multi-page scanned PDFs can take 30–90 s on first pass. The cache at .construct/ingest/\<sha256>/ makes re-ingest a no-op, so the cost is paid once per content hash.

If latency matters more than recall, use construct ingest --legacy-extractor to fall back to the pre-docling pdftotext/textutil/mdls path. The legacy path is faster but produces lower-fidelity text and does no OCR — scanned pages will return empty.