OCR in R with pdftools, tested on scanned pages
13x
faster than the pdf_ocr_text defaults
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")
| 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.
| 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.
Two ways to skip the work
Run it yourself. Our Apify Actor turns scanned PDFs into Markdown, tables included.
Or send us the scan. Tack, our AI, does the job for $29, up to 50 pages. First 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.