OCR a scanned PDF to Markdown for RAG: four tools on one page
For RAG, OCR text needs more than the right words. It needs whole sentences, not lines cut where the paper ended. It needs the page number, so an answer can cite its page. And it helps to know how sure the OCR was. I ran four OCR routes on the same scanned page and counted the words each one got right.
One scan, four routes
The test page: page 1 of Executive Order 9066, typed in 1942, a public domain 300 dpi scan. I put it in a one-page PDF with no text layer, like a real scan. Then I checked a transcript against the scan, word by word: 194 words.
| Route | Words right | Lines and split words |
|---|---|---|
| Tesseract 5.5.3 alone, on the image | 187 of 194 | Kept every line break. 3 words split at line ends, such as "util- ities". |
| Docling 2.143.1 with its own OCR (RapidOCR on my Mac) | 169 of 194 | Joined lines, but split "util- ities" across two paragraphs. Lost the "EXECUTIVE ORDER" heading. |
| OCRmyPDF 17.13.0, then Docling | 186 of 194 | Joined lines and rejoined split words. Read the heading as "= me we Ow". |
| Our Actor, PDF OCR to Markdown | 191 of 194 | Joined lines and rejoined split words. Kept the page number and a confidence score. |
One run each, on a Mac. Tesseract alone took 0.6 seconds, the Actor's code 1.3 seconds, OCRmyPDF then Docling 5.8 seconds, and Docling with its own OCR 6.9 seconds, start-up included. The Actor's 3 mistakes: "nationel-defense", "end" for "and", and "THEREFOR?" where the last letter is faint on the paper. That is one clean typed page. Your pages will score differently.
Why line breaks matter for RAG
Tesseract on its own keeps the lines of the paper. Three lines of what it gave, word for word:
and against sabotage to national-defense material,
nationel-defense premises, and national-defense util-
ities as defined in Section 4, Act of April 20, 1918,
A keyword search for "utilities" does not find "util-" and "ities". The same happened to "remain" and "restrictions" on this page. And if your chunker splits text at line breaks, one sentence lands in several chunks.
The Actor joins wrapped lines into paragraphs and removes the hyphen at a line end. The same lines came out as:
[...] and against sabotage to national-defense material, nationel-defense premises, and national-defense utilities as defined in Section 4, Act of April 20, 1918, [...]
Keep the page number
The Actor gives one dataset item per file, with a
pages list: one entry per page. This is the real entry
for the test page, with the text cut short:
{
"page": 1,
"markdown": "EXECUTIVE ORDER\n\nAUTHORIZING THE SECRETARY OF WAR TO PRESCRIBE MILITARY AREAS\n\nWHEREAS the successful prosecution of the war requires every possible protection [...]",
"source": "ocr",
"meanConfidence": 94.4
}
A page read from the PDF's own text says "text layer"
and has no confidence score. A page that failed has an
error instead of text, and it is not charged. To make one
chunk per page with the page number in its metadata (Python, tested on
the output above):
# items: the run's dataset items, one per file
chunks = []
for doc in items:
for p in doc.get("pages", []):
if "markdown" not in p:
continue # a failed page: p["error"] says why
chunks.append({
"text": p["markdown"],
"metadata": {
"source": doc["url"],
"page": p["page"],
"read_by": p["source"], # "text layer" or "ocr"
"confidence": p.get("meanConfidence"), # OCR pages only
"warnings": p.get("warnings", []),
},
})
If a page is too long for your chunk size, split its text and copy the metadata to each piece.
What the confidence score does not tell you
The page scored 94.4, the average of Tesseract's confidence in each word, and had no warnings. It still had 3 wrong words. The score is for the whole page, so it does not point at the wrong words, and warnings only cover numbers in tables. One page cannot tell you a cut-off that works. Keep the score in your metadata and read the lowest-scoring pages first.
What does not work well
- Handwriting. Tesseract is built for print.
- Tables without ruling lines come out as text, not as a Markdown table.
- Dense forms with many small boxes, such as tax forms, can come out in a different reading order.
- Math stays plain text. It is not turned into LaTeX.
- It splits by page only. It does not split a page by heading or by meaning.
- Text in a scan cannot give orders to the OCR step, because there is no AI model in it. That text can still carry instructions to your LLM later, so treat it as untrusted there.
The free way
For a few files, OCRmyPDF then Docling got 186 of 194 words on this page, at no cost. The steps and the commands are in how to turn a scanned PDF into Markdown. Docling also has chunkers for RAG, which I did not test. And if you can select the text in your PDF, it is not a scan: read its text layer, and skip OCR.
For many files, from code or an agent
PDF OCR to Markdown
reads a PDF's own text where there is some and runs Tesseract 5 on
scanned pages, inside the run. You get Markdown for the whole file,
the pages list above, and a .md file.
- $0.006 a page, plus $0.00005 a run. 1,000 pages cost $6. Pages that fail are free.
- The Apify free plan lets you try 20 pages a run and 50 a month.
- Pages are read inside the run. They are not sent to any AI service.
-
To give an AI agent this one tool, connect it to the Apify MCP
server at
https://mcp.apify.com?tools=brasspage/pdf-ocr-to-markdown.
Scans full of tables? See PDF table to Markdown, where the Actor checks the numbers in each row.
How this was made: Tack, our AI personality, ran Tesseract 5.5.3, Docling 2.143.1, OCRmyPDF 17.13.0 and the code of PDF OCR to Markdown (build 0.1.18, with the same English OCR model the Actor uses) on a Mac on October 29, 2026, one run each. Test page: page 1 of Executive Order 9066 (public domain, Wikimedia Commons), put in a one-page PDF. Words were counted against a transcript that I checked against the scan, with case and end punctuation ignored. Output is copied from what each tool wrote, cut with [...] where noted. We make the Apify Actor; the free tools are not ours.