Tools,

OCR in R with pdftools, tested on scanned pages

13x

faster than the pdf_ocr_text defaults

Get the fast R code

OCR a scanned PDF in R

1,873 of 1,955 words found

470 s for 10 pages, Mac CPU

install.packages(c("pdftools", "tesseract"))
library(pdftools)
txt <- pdf_ocr_text("scan.pdf")
writeLines(txt, "scan.txt")

pdf_ocr_text() renders each page to a PNG at 600 dpi, reads it with Tesseract, and deletes the PNG. You get one string a page.

pdf_text gives 0 characters on a scan

library(pdftools)
nchar(pdf_text("scan.pdf"))
 [1] 0 0 0 0 0 0 0 0 0 0

A scan has page images and no text layer, so pdf_text() has nothing to read. With tesseract not installed, pdf_ocr_text() stops:

Error in loadNamespace(x) : there is no package called ‘tesseract’

13x faster: OCR the page JPEGs

system("pdfimages -j scan.pdf p")
library(tesseract)
eng <- tesseract("eng", options = list(tessedit_pageseg_mode = 3))
pages <- sort(Sys.glob("p-*.jpg"))
txt <- sapply(pages, ocr, engine = eng)
writeLines(txt, "scan.txt")
Same 10 pages, 1,955 words
Way Words found Time
pdf_ocr_text(), defaults 1,873 470 s
dpi = 300 1,878 170 s
The code above 1,878 36 s
Tesseract 5.5.4 program 1,877 47 s
RapidOCR 3.10.0 1,894 55 s
Our Actor 1,874

The render is the slow part: pdf_convert() took 134 of the 170 s at dpi = 300. Each page of this scan is one JPEG, so pdfimages (from poppler) takes it out as it is.

Two columns: set page mode 3

The R engine starts in page mode 6, one block of text. The Tesseract program uses mode 3, which finds columns. On 3 German two-column pages, mode 6 read lines across both columns.

3 German pages, 206 umlauts, 1,887 words
Run Umlauts Words right
eng, mode 6 0 1,238
deu, mode 6 139 1,402
deu, mode 3 189 1,826
deu best, mode 3 190 1,835
Tesseract program 192 1,842
Our Actor 192 1,853

On the one-column English pages, mode 3 found 1 word more.

German, French and more

Without the language file, the run stops:

Error: Unable to find training data for: deu. Please consult manual for: ?tesseract_download
library(tesseract)
dir.create("tessdata")
tesseract_download("deu", datapath = "tessdata")
Sys.setenv(TESSDATA_PREFIX = "tessdata")
txt <- pdftools::pdf_ocr_text("german.pdf", language = "deu",
  dpi = 300, options = list(tessedit_pageseg_mode = 3))
writeLines(txt, "german.txt")

The download is the fast model, 1.5 MB for German. The "best" row above used model = "best", 8.6 MB. With no datapath, the file goes into the folder that tesseract_info() names.

Install: binary or source

168 s to build pdftools and 5 packages it needs

22 s for tesseract and 2 more

On Windows and Mac, CRAN R gets binary packages. I used Homebrew R, which builds from source, with Homebrew poppler and Tesseract already there. For Debian and Ubuntu, the package READMEs give:

sudo apt-get install libpoppler-cpp-dev
sudo apt-get install -y libtesseract-dev libleptonica-dev tesseract-ocr-eng

Need text with nothing to install?

Our Actor runs Tesseract, and RapidOCR on English pages that Tesseract reads badly. It takes PDF, JPG and PNG files and returns Markdown.

curl -s -X POST "https://api.apify.com/v2/acts/brasspage~pdf-ocr-to-markdown/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentUrls": ["https://api.apify.com/v2/key-value-stores/sTnmNrGoSsi7tLQvs/records/anythingllm-mixed-11-pages.pdf"]}' \
  | jq -r '.[].markdown' > book.md

It costs $0.006 a page.

How I tested it
  • Pages: the 10 scans of OCR in n8n, proofread Wikisource book pages, 1,955 true words, one 300 ppi JPEG a page. German: the 3 two-column pages of Paperless-ngx OCR.
  • Homebrew R 4.6.2 on an Apple Silicon Mac. pdftools 3.9.1 on poppler 26.03.0, tesseract 5.3.1 (the R package) on Tesseract 5.5.4, built from CRAN source with 2 jobs.
  • CPU, 2 threads (OMP_THREAD_LIMIT=2), lowest priority, other jobs on the Mac. Times are rough. German times include the download.
  • Score: true words found in order, with the scorer of the tessdata test. German words and umlauts: the scorer of Tesseract and German.
  • English: the Tesseract program and RapidOCR rows are from my RapidOCR test. German: the Tesseract program read the page JPEGs as they are, with the same deu file.
  • The Actor rows are real runs on build 0.1.65, November 12.
What I did not test
  • CRAN binary packages, Windows and Linux.
  • Photos, tables, and scans with pages that are not JPEG.
Sources

Two ways to skip the work

Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.

Try the Actor

Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First page free.

Get one page free

Questions people ask

Why does pdf_text return empty strings?

A scanned PDF has page images and no text layer. pdf_text() reads only the text layer, so each page comes back as an empty string. Use pdf_ocr_text() for scans.

Why is pdf_ocr_text so slow?

It renders each page to a PNG at 600 dpi first. In a timed run on 10 pages, the render took 366 s and the OCR 95 s. pdfimages and tesseract::ocr took 36 s in all.

How do I OCR German or French in R?

Download the language with tesseract_download, point TESSDATA_PREFIX at that folder, and pass language = "deu" (or "fra"). With eng on German pages, 0 of 206 umlauts came out.

Why are two columns mixed in the R output?

The R tesseract engine starts in page mode 6, one block of text. Pass options = list(tessedit_pageseg_mode = 3). On 3 German pages, words right went from 1,402 to 1,826 of 1,887.