Skip OCR when the PDF already has text
Most PDFs carry their own text. Lifting it is instant and exact — run OCR only on the pages that need it.
OCR is the expensive part of any PDF pipeline, and most PDFs do not need it.
PdfToDocument reads the embedded text layer with word-level boxes, in the same
pixel space PdfToImage renders at.
One pass to find out
import { Pipeline } from '@stabrise/scaledp'
import { PdfToDocument, hasUsableTextLayer } from '@stabrise/scaledp/pdf'
const probe = new Pipeline([new PdfToDocument({ resolution: 300 })])
const pages = await probe.transform(file)
const withText = pages.filter((row) => hasUsableTextLayer(row.document))
const needsOcr = pages.filter((row) => !hasUsableTextLayer(row.document))hasUsableTextLayer(document, minimumBoxes = 1) is the whole decision. Raise
minimumBoxes if your corpus contains PDFs whose "text layer" is a single stamped
watermark.
Then OCR only what is left
import { PdfToImage } from '@stabrise/scaledp/pdf'
import { PaddleTextRecognizer } from '@stabrise/scaledp/ocr'
const ocr = new Pipeline([
new PdfToImage({ resolution: 300, pageLimit: 0 }),
new PaddleTextRecognizer({ keepFormatting: true }),
])
const scanned = needsOcr.length > 0 ? await ocr.transform(file) : []Because both stages use the same resolution, the boxes from the text layer and
the boxes from OCR are in one coordinate space — so the two sets of pages can be
merged, drawn together, or fed to the same NER stage without conversion.
Match the resolution
PdfToDocument's resolution is not a render DPI — it is the pixel space its
boxes are expressed in. Set it to the same number as PdfToImage or the two
box sets will not line up.
One pipeline, OCR only where it is needed
when moves the branch into the pipeline. A page with a usable text layer
passes the OCR stages untouched, and a document whose every page has one never
loads an OCR model at all:
import { pipelineFromDescriptors } from '@stabrise/scaledp/registry'
import { usableTextLayer } from '@stabrise/scaledp/pdf'
const needsOcr = { any: [{ not: usableTextLayer() }, { var: 'forceOcr', truthy: true }] }
const pipeline = pipelineFromDescriptors([
{ type: 'PdfToDocument', options: { resolution: 300 } },
{ type: 'PdfToImage', options: { resolution: 300, keepInputData: true }, when: needsOcr },
{ type: 'PaddleTextRecognizer', options: { outputCol: 'document' }, when: needsOcr },
])
const rows = await pipeline.run(file, { vars: { forceOcr: false } })usableTextLayer(col, minimumBoxes) is hasUsableTextLayer as a condition.
Run with run rather than transform and the text-layer pages come back while
the scanned ones are still being read. See Streaming runs.
Both columns, decided afterwards
If you would rather keep both readings, run both readers and decide per row afterwards:
const pipeline = new Pipeline([
new PdfToDocument({ resolution: 300, outputCol: 'pdf_text' }),
new PdfToImage({ resolution: 300 }),
])PdfToDocument first, because it keeps content and sets the page index;
PdfToImage then renders only the page the row already names, so a five-page
file stays five rows. The reverse order does not work — PdfToImage deletes
content by default, leaving PdfToDocument nothing to read. See
Columns are the wiring.
For most corpora the conditional pipeline above is cheaper and clearer.
When a page is both
A page with a vector-text header and a scanned table is not an either-or case, and neither branch above reads all of it. See A PDF's text layer and its scanned images.