Recipe: OCR a Scanned PDF¶
Goal¶
Get the text out of a PDF whose pages are pictures, such as a scan, a fax or a photographed contract:
tell that it is scanned, OCR only the pages you need with ocr_pdf, and read a long document in
parts.
Prerequisites¶
-
The
docsandocrextras (orall):pip install "doc-processing-std-tools[docs,ocr] @ git+https://github.com/rzoeb/doc-processing-std-tools.git@v0.6.1" -
LLMWHISPERER_API_KEYin the environment of the process that runs the tools. LLMWhisperer bills per page, so ask for the pages you need rather than"all".
Steps¶
Tool calls are written as Python calls; the model sends the same arguments as JSON.
1. Check whether the PDF has text¶
get_file_overview is cheap and needs no key. A scanned PDF has an empty text preview on every page
and reports images:
get_file_overview(path="scans/contract.pdf")
{
"type": "pdf",
"page_count": 3,
"text_preview_per_page": ["", "", ""],
"has_tables": false,
"has_images": true
}
read_pdf on the same file says so too, and names the tool to use:
--- Page 1 ---
(no extractable text on this page)
--- Page 2 ---
(no extractable text on this page)
--- Page 3 ---
(no extractable text on this page)
[No extractable text found — this looks like a scanned/image PDF. Use ocr_pdf for OCR extraction.]
A PDF with a text layer on some pages and scans on others shows text for some pages only. OCR just the empty ones.
2. OCR the pages you need¶
ocr_pdf(path="scans/contract.pdf", pages="1-2")
--- Page 1 ---
SUPPLY AGREEMENT - PAGE 1
Clause 1.1: The supplier delivers item 100 monthly.
...
--- Page 2 ---
SUPPLY AGREEMENT - PAGE 2
...
pages takes "all", a page ("3"), a range ("1-5"), a list ("1-3,7") or an open-ended range
("21-", page 21 to the end). Pages come back in document order, whatever order you list them in,
and the --- Page N --- headers are their page numbers in the document.
Page labels in 0.6.0
In 0.6.0 the labels could be wrong: an open-ended range was numbered from 1 (pages="21-" came
back as --- Page 1 --- onwards), and a list out of order ("3,1") was labelled in the order
given. The note in step 3 could also name pages you never asked for. This is fixed in 0.6.1
(see the Changelog). On 0.6.0, ask for one closed range at a time, such as
"21-40".
3. Read a long document in parts¶
OCR output longer than DPTTOOLS_OUTPUT_TRUNCATION_LIMIT (50,000 characters by default) is cut
on a page boundary, and the result ends with exactly the pages left out, as a pages value:
ocr_pdf(path="scans/contract.pdf", pages="2-")
--- Page 2 ---
SUPPLY AGREEMENT - PAGE 2
...
[Output truncated on a page boundary. Page 3 was not included. Re-call ocr_pdf with pages='3' to read it.]
With several pages left out, the note lists them, for example pages='5-9,12'.
The model follows the note with another ocr_pdf call. A dense 200-page document is better read in
ranges of about 20 pages from the start: each call is quicker, and a timeout loses less.
4. Give an agent the tools¶
Attach only the tools this job needs, and say when to use OCR:
from doc_processing_toolkit import Agent
from doc_processing_std_tools import document_processing_server
agent = Agent(
model="claude-sonnet-5",
instructions=(
"Read PDFs with read_pdf. When it reports no extractable text, use ocr_pdf, "
"asking for at most 20 pages per call."
),
tools=[{"type": "fastmcp", "server": document_processing_server,
"allowed_tools": ["get_file_overview", "read_pdf", "ocr_pdf"]}],
)
result = agent.run_sync("What does clause 2.4 of scans/contract.pdf say?")
In-process, the tools read LLMWHISPERER_API_KEY from your process's environment. An OCR call
takes seconds to minutes, and with toolkit 0.5.2 or earlier an in-process tool holds up the agent's
event loop until it returns. To avoid that, run the tools out of process, as in
Using with the Toolkit,
and pass LLMWHISPERER_API_KEY in the session's env.
Without an OCR key¶
read_document_pages_as_images(path="scans/contract.pdf", pages="1-2") returns the pages as images,
and a model that reads images can read the text itself. It costs image tokens instead of OCR pages,
and it suits a few pages or a page with a layout that matters, such as a form or a table.
What to validate¶
read_pdfon the scan ends with the note namingocr_pdf.ocr_pdfreturns a--- Page N ---block for each requested page, in document order and numbered as in the document, including for an open-ended range such as"2-"and a list out of order such as"3,1".- A result that was cut ends with the pages left out, and asking for exactly those returns the rest.
- Without
LLMWHISPERER_API_KEY,ocr_pdfreturns anError:naming the variable and uploads nothing. - An empty file, or a file that is not a PDF, is refused before anything is uploaded.
Test sources¶
tests/unit/test_document_processing.py:TestReadPdf::test_scanned_pdf_routes_to_ocr,TestOcrExtraction(page headers, page ranges, cuts on a page boundary),TestOcrLabels,TestPagesSpec,TestOcrInputValidationandTestOcrErrorClassification. The OCR client is mocked.tests/integration/test_ocr_live.py: real LLMWhisperer calls. They are paid, so they are left out of routine runs.
Related pages¶
- Documents server: every reading tool, and the OCR settings
(
DPTTOOLS_OCR_TIMEOUT). - Configuration:
LLMWHISPERER_API_KEYand the output limit.