# How to OCR Scanned PDFs with Tesseract and OCRmyPDF in a Sandbox

> Install ocrmypdf with apt, which brings Tesseract 5.3.4, then run ocrmypdf --deskew --jobs N in.pdf out.pdf. On a 45-page typewritten scan, a 4-vCPU default sandbox took 2.2 seconds per page with 4 jobs, and a 2-vCPU small one 3.6 seconds with 2 jobs. Add languages with tesseract-ocr-<lang> packages.

Source: https://sandboxer.com/guides/ocr-pdfs-with-tesseract-in-a-sandbox. Updated 2026-10-27.

**Short answer:** Install ocrmypdf with apt, which brings Tesseract 5.3.4, then run ocrmypdf --deskew --jobs N in.pdf out.pdf. On a 45-page typewritten scan, a 4-vCPU default sandbox took 2.2 seconds per page with 4 jobs, and a 2-vCPU small one 3.6 seconds with 2 jobs. Add languages with tesseract-ocr-<lang> packages.

Everything below ran on prod on 2026-10-27, on one sandbox created as `small` and later resumed as `default`. The test files are three public-domain scans from Wikimedia Commons:

| File | What it is | Pages | Source |
| - | - | - | - |
| `konoye.pdf` | Typewritten English translation of Prime Minister Konoye's December 1938 statement, a Tokyo war crimes trial exhibit | 4 | [National Diet Library scan on Commons](https://commons.wikimedia.org/wiki/File:NDL12920597_Pros._Doc._No._813-_Typewritten_text_of_speech_by_Prince_KONOYE_regarding_establishment_of_Sino-Japanese_friendship.pdf) |
| `ussbs.pdf` | U.S. Strategic Bombing Survey report on Japanese civilian defense, typewritten | 45 | [National Diet Library scan on Commons](https://commons.wikimedia.org/wiki/File:NDL8319032_Typewritten_copy._Report_No._10a(1),_USSBS_Index_Section_2.pdf) |
| `patella.pdf` | *Über Patellafrakturen*, a German medical text | 20 | [Internet Archive scan on Commons](https://commons.wikimedia.org/wiki/File:%C3%9Cber_Patellafrakturen_(IA_b28097981).pdf) |

This page is about scans: images of text that need OCR. If your input is DOCX, XLSX or PPTX, you don't need OCR. See [batch converting documents with LibreOffice headless](/guides/convert-documents-with-libreoffice-headless).

## 1. Install OCRmyPDF and Tesseract

Neither is in the [base image](/guides/whats-preinstalled-in-a-sandbox). The [OCRmyPDF install docs](https://ocrmypdf.readthedocs.io/en/latest/installation.html) list `apt install ocrmypdf` for Ubuntu. It pulls in Tesseract, Ghostscript and the `pikepdf` Python library. Add `poppler-utils` for `pdfinfo`, `pdfimages` and `pdftotext`.

```bash
id=$(sbx new --type small --name ocr-1 --json | jq -r 'select(.event == "ready") | .id')

sbx exec $id --timeout 600 '
  time (sudo apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ocrmypdf poppler-utils >/dev/null)
  ocrmypdf --version; tesseract --version 2>&1 | head -1; gs --version
  tesseract --list-langs'
```

```text
real    0m16.981s
15.2.0+dfsg1
tesseract 5.3.4
10.02.1
List of available languages in "/usr/share/tesseract-ocr/5/tessdata/" (2):
eng
osd
```

The install took 17 seconds. Only English (`eng`) and orientation detection (`osd`) come with it.

## 2. Check what you downloaded

Wikimedia asks scripts to send a [User-Agent](https://meta.wikimedia.org/wiki/User-Agent_policy), so pass `-A` to `curl`. Then check each file for a text layer and for the scan's resolution:

```bash
sbx exec $id --timeout 300 '
  mkdir -p ~/ocr && cd ~/ocr
  curl -sSL -A "my-ocr-job/1.0" -o konoye.pdf "https://upload.wikimedia.org/wikipedia/commons/3/3e/NDL12920597_Pros._Doc._No._813-_Typewritten_text_of_speech_by_Prince_KONOYE_regarding_establishment_of_Sino-Japanese_friendship.pdf"
  pdfinfo konoye.pdf | grep -E "Pages|Encrypted"
  echo "text chars: $(pdftotext konoye.pdf - | tr -d "[:space:]" | wc -c)"
  pdfimages -list konoye.pdf | sed -n 3p'
```

```text
Pages:           4
Encrypted:       yes (print:yes copy:no change:no addNotes:no algorithm:AES-256)
text chars: 0
   1     0 image    2112  2712  rgb     3   8  jpeg   no         9  0    96    96  522K 3.1%
```

Zero characters means there is no text layer, so it needs OCR. Each page is one 2112 x 2712 pixel JPEG. The file is also encrypted, which matters in the next step. We fetched `ussbs.pdf` and `patella.pdf` the same way, from the links in the table above. `ussbs.pdf` (33.5 MB) is also image-only and encrypted. `patella.pdf` already has a text layer.

## 3. Plain Tesseract first

Tesseract reads images, not PDFs. Pull out the page image and run it:

```bash
sbx exec $id --timeout 120 'cd ~/ocr && pdfimages -png -f 1 -l 1 konoye.pdf k && time tesseract k-000.png -'
```

```text
Premier KONOYE's Talk Regarding the
Adjustment of Sino-Japanese Relations.

Tec. 22, 1938.

The Japanese government, as has been made clear in repeated
etatements this year, has constantly leoked forward to the complete
military annihilation of the anti-Japanese Kuomintung Government,
and will strive for the esiablishment of a new order in East Asia
…
put into mtual practice the good neighbor policy, the united anti-
comintern front, and the economic coalition, For this purpose, it will
…
real    0m5.843s
```

That is real output from a 1938 typewriter page, with real mistakes: "Tec." for "Dec.", "etatements", "leoked", "esiablishment". It took 5.8 seconds for one page on `small`. Plain Tesseract gives you text. It doesn't give you a PDF you can search.

## 4. Make a searchable PDF with ocrmypdf

OCRmyPDF runs Tesseract on every page and lays an invisible text layer over the original image. Our first run failed:

```bash
sbx exec $id --timeout 300 'cd ~/ocr && ocrmypdf konoye.pdf konoye-ocr.pdf'
```

```text
EncryptedPdfError: Input PDF is encrypted. The encryption must be removed to
perform OCR.
…
You can remove the encryption using
    qpdf --decrypt [--password=[password]] infilename
```

The `qpdf` command isn't installed, but the `pikepdf` library that came with ocrmypdf can do the same thing. Both NDL files open with an empty password, so this decrypts them:

```bash
sbx exec $id 'cd ~/ocr && python3 -c "import pikepdf; [pikepdf.open(f + \".pdf\").save(f + \"-dec.pdf\") for f in (\"konoye\", \"ussbs\")]" && mv konoye-dec.pdf konoye.pdf && mv ussbs-dec.pdf ussbs.pdf'
```

Then `ocrmypdf konoye.pdf konoye-ocr.pdf` exited 0 in 11.5 seconds for 4 pages and wrote a PDF/A-2B file. Page 1 was fine, but page 2 came out in fragments:

```text
which understands The New Fa
We shall only strive to carr
…
st Asia
y
accordingly, the prin
```

The page image showed why: the typed block is slightly tilted on the page. Tesseract split each slanted line into pieces. `--deskew` straightens each page before OCR:

```bash
sbx exec $id --timeout 300 'cd ~/ocr && time ocrmypdf --deskew -q konoye.pdf konoye-deskew.pdf && pdftotext -f 2 -l 2 konoye-deskew.pdf - | head'
```

```text
which understands The New Fast Asia and is ready to act accordingly.
We shall only strive to carry out Sino-Japanese coalition and collaboration,
sccordingly, the principle of equality between China
and Japan, China shell recognize the freedom of Japanese nationals
…
real    0m13.157s
```

With `--deskew`, the 4 pages took 13.2 seconds instead of 11.5, and page 2 reads as sentences. The output (1.79 MB) was slightly smaller than the input (1.81 MB). `pdffonts` lists one font, `GlyphLessFont`. That is the invisible text layer.

## 5. Time per page by size and job count

`--jobs` sets how many pages ocrmypdf works on at once. We ran the 45-page USSBS report (2640 x 3056 pixel pages, decrypted the same way) with `--deskew` and timed each run end to end:

```bash title="~/bench.sh"
cd ~/ocr
pages=$(pdfinfo ussbs.pdf | awk '/^Pages/{print $2}')
for j in $JOBS; do
  s=$(date +%s.%N)
  ocrmypdf --deskew --jobs $j -q ussbs.pdf /tmp/ussbs-j$j.pdf; rc=$?
  e=$(date +%s.%N)
  awk -v j=$j -v s=$s -v e=$e -v p=$pages -v rc=$rc 'BEGIN{w=e-s; printf "jobs=%d exit=%d pages=%d wall=%6.1fs per_page=%.2fs\n", j, rc, p, w, w/p}'
done
```

On `small` we ran `JOBS="2 1" bash ~/bench.sh`. Then we ran `sbx stop $id --wait` and `sbx resume $id --type default`, and ran `JOBS="4 2"`:

| Size | vCPU | `--jobs` | 45 pages | Per page | Pages per hour |
| - | - | - | - | - | - |
| `small` | 2 | 1 | 285.6 s | 6.35 s | about 570 |
| `small` | 2 | 2 | 163.9 s | 3.64 s | about 990 |
| `default` | 4 | 2 | 148.3 s | 3.30 s | about 1,090 |
| `default` | 4 | 4 | 98.5 s | 2.19 s | about 1,640 |

What the numbers say:

- **Set `--jobs` to the vCPU count.** On `default`, going from 2 to 4 jobs cut the time by a third.
- **`default` costs twice what `small` does per hour, but was 1.66 times as fast.** From these runs and [per-second billing](/docs/billing), 1,000 pages like these come to about $0.018 on `small` with 2 jobs and about $0.022 on `default` with 4 jobs. That's extrapolated from one 45-page file, not a bulk run.
- **The PDF/A step costs almost nothing here.** `--output-type pdf` skips the PDF/A conversion. With 4 jobs it took 98.8 s against 98.5 s for the PDF/A default.

The OCR text was good enough to search. `pdftotext` found 11,460 words, and the word "siren" on 5 lines. This report is typed with wide justified spacing, and `pdftotext` sometimes returned one word per line. Pass `--sidecar out.txt` to get Tesseract's own text in reading order:

```text
and means were provided to cover such contingencies as the disruption
of the central siren control system, Radio was also oul 98 863
transmission of signals and the imparting of pertinent air-raid infor-
mation following the announcement of the first public signal.
```

Tesseract's mistakes stay in the text: "oul 98 863" is not part of the sentence.

## 6. Add a language pack

Tesseract needs a trained model for each language. On Ubuntu each one is an apt package named `tesseract-ocr-<code>`. See the [Tesseract install docs](https://tesseract-ocr.github.io/tessdoc/Installation.html) for the list.

The German PDF already had a text layer from the Internet Archive (39,133 characters), so ocrmypdf refused it:

```bash
sbx exec $id 'cd ~/ocr && ocrmypdf -q patella.pdf /tmp/x.pdf; echo rc=$?'
```

```text
PriorOcrFoundError: page already has text! - aborting (use --force-ocr to force OCR;  see also help for the arguments --skip-text and --redo-ocr
rc=6
```

`--skip-text` leaves pages that have text alone. `--force-ocr` rasterizes them and OCRs them again. We installed German and forced OCR of page 5 twice, once in English and once in German:

```bash
sbx exec $id --timeout 300 '
  sudo apt-get install -y -qq tesseract-ocr-deu >/dev/null
  cd ~/ocr
  for l in eng deu; do
    ocrmypdf -q --force-ocr --pages 5 -l $l --sidecar /tmp/p5-$l.txt patella.pdf /tmp/p5-$l.pdf
  done'
```

The German pack is 1.5 MB and installed in under 3 seconds. Here are the same lines from each sidecar:

```text
-l eng:
schliige, oder wenn die Eminentia auf eine hervorragende Stelle des Bodens
trafe, oder schliefilich das Knie sich in stirkster Beugestellung befainde.
Hamilton, welcher unter 40 Fallen seiner ersten Publikation nur

-l deu:
schlüge, oder wenn die Eminentia auf eine hervorragende Stelle des Bodens
träfe, oder schließlich das Knie sich in stärkster Beugestellung befände.
Hamilton, welcher unter 40 Fällen seiner ersten Publikation nur
```

The English model can't produce umlauts or `ß`, so it guesses. The German model got them right. It did write a dotless `ı` in "ın ganzer" and "ıst". A run with `-l deu+eng` fixed the first one: that line reads "in ganzer Ausdehnung".

## What survives stop and resume

We stopped the sandbox and resumed it as `default`. The resume took 3.4 seconds and reported `memoryRestored: false`, because a resize boots from disk. The apt packages, the German pack and every file in `~/ocr` were still there, so the second benchmark ran with no reinstall. OCR is a batch job with nothing to keep running, so a cold boot costs nothing here. If you'd rather not run `apt` in every new sandbox, see [caching dependencies in a named snapshot](/guides/cache-dependencies-in-a-named-snapshot).

## FAQ

### Do I need a GPU for Tesseract?

No. Tesseract runs on CPU, and every number on this page came from CPU-only sandboxes. Sandboxer [sizes](/docs/machines) have no GPU.

### Why is my OCR output broken into fragments?

The scan is probably tilted. Add `--deskew`. On our test page it turned fragments like "The New Fa" back into full lines, for about 0.4 extra seconds per page.

### What does "page already has text" mean?

The PDF already has a text layer, often from an earlier OCR pass. Use `--skip-text` to OCR only pages without text, or `--force-ocr` to redo every page.

### How do I OCR more than one language in the same document?

Install each pack and join the codes with `+`, for example `-l deu+eng`. List what's installed with `tesseract --list-langs`.

## Related

- [How to batch convert DOCX to PDF with LibreOffice headless](/guides/convert-documents-with-libreoffice-headless)
- [How do I move files in and out of a sandbox?](/guides/move-files-in-and-out-of-a-sandbox)
- [How to run a long job in a cloud sandbox](/guides/run-a-long-job-in-a-cloud-sandbox)
- [Machines and sizes](/docs/machines)
