Tools,

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

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.

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.