FLOWLOGIC
module /files-and-documents

unit U33 of 5

PDFs & extraction

text layer vs scan, when OCR, what to trust

PDFs come in two kinds that look identical to a human and could not be more different to a flow. A digital PDF carries a text layer — the characters are really in the file, and extraction is fast, free and exact. A scanned PDF is a photograph of a page wrapped in a PDF container; it contains no text at all, and extraction returns an empty string. Every document pipeline needs to tell them apart before it decides what to do.

The test is simple: extract the text layer and measure it. A meaningful character count means a digital PDF, and you should use that text — it is the ground truth. Near-zero means a scan, and now you need OCR, which is slower, costs money, and is approximate: it will confuse `0` with `O`, `1` with `l`, and lose table structure. That approximation is fine for search and routing, and it is not fine for an amount you are about to post to a ledger. Route those to a human, or to a second check.

Where it breaks: assuming extraction succeeded because it did not throw. An empty string is a perfectly valid return value from a scanned page, and a flow that carries it forward produces a document with no content, an LLM asked to summarise nothing, and a confidently empty result. Assert a minimum character count before trusting extraction, and branch when it fails.

worked example

Telling a digital PDF from a scan, and treating OCR output as approximate.

const MIN_CHARS = 40;   // below this, treat as "no text layer"

export const code = async (inputs) => {
  const text = await extractTextLayer(inputs.pdfBytes);

  // An empty string is a VALID return for a scan. Not throwing proves nothing.
  if (text.trim().length >= MIN_CHARS) {
    return { source: 'text-layer', text, exact: true };
  }

  const ocr = await ocrPages(inputs.pdfBytes);     // slower, costs money
  return {
    source: 'ocr',
    text: ocr.text,
    exact: false,                                   // 0/O and 1/l are guesses
    confidence: ocr.confidence,
    // Anything financial from OCR goes to a human, not straight to the ledger.
    needsReview: ocr.confidence < 0.9 || /total|amount|iban/i.test(ocr.text),
  };
};

field checklist

common failure — A summariser given an empty document

An invoice pipeline extracted PDF text and passed it to a model for structuring. Scanned invoices returned an empty string, the extraction step did not throw, and the model confidently produced a plausible invoice with invented values from nothing. Assert a minimum character count before trusting extraction, branch scans to OCR, and mark anything OCR-derived as approximate so a human sees it before it reaches a ledger.

check your understanding

Text extraction from a scanned PDF returns an empty string without throwing. Which assertion belongs before the text is trusted?

if (text.trim().length >=) { /* real text layer */ }

next unit opens once this is passed

sandbox validation

The check above confirms you followed the unit. Marking the module COMPLETED takes more: build the automation in your own engine and submit the exported flow and its run evidence, signed, to your unique validation URL. See the module page for that spec.