Which Python OCR engine reads the most words correctly?
PaddleOCR, docTR, RapidOCR, EasyOCR, Tesseract: each one tested on the same 190 real-world images with known text: 80 receipt photos, 50 document pages and 60 app screenshots. New PyPI releases are tested again by themselves. Updated 06 November 2026.
190 files from OCR test set (CORD receipts, DocLayNet pages, app screenshots);
190 of them give text to at least one library.
| Library | 🔴 Fails where another reads | 🟡 Empty where another reads |
⏱ Hangs or crashes | ✅ Words found | Median ms/file | Total seconds |
|---|---|---|---|---|---|---|
| 1 PaddleOCR 3.7.0 Note: With paddlepaddle 3.2.2, as its maintainers advise: with 3.3.1 (the default install) every image fails on CPU (PaddleOCR issue #18162). why | 0 | 0 | 0 | 95.1% | 3070 | 1,527.6 |
| 2 docTR 1.1.0 | 0 | 0 | 0 | 95.0% | 2286 | 864.8 |
| 3 RapidOCR 3.10.0 | 0 | 0 | 0 | 92.4% | 372 | 136.6 |
| 4 EasyOCR 1.7.2 | 0 | 0 | 0 | 75.2% | 3736 | 620.5 |
| 5 Tesseract 0.3.13 (tesseract 5.5.0) | 0 | 24 | 0 | 67.5% | 132 | 53.8 |
By kind of image: words found
| Images | PaddleOCR | docTR | RapidOCR | EasyOCR | Tesseract |
|---|---|---|---|---|---|
| documents 50 images | 91.6% | 92.0% | 90.2% | 71.5% | 88.2% |
| receipts 80 images | 94.8% | 95.5% | 89.4% | 67.7% | 37.6% |
| screenshots 60 images | 98.5% | 96.9% | 98.4% | 88.2% | 90.0% |
Don't want to run it yourself? I also run Image & Scanned PDF to Text OCR as a pay-per-use API on Apify (it uses RapidOCR for photos and Tesseract for scans, the split this table supports). Disclosure: that tool is mine; the rankings above come only from the measurements.
How this is measured
- Each library runs a short recipe from its own docs (board/targets.py):
open the file, get all text. Installed with the extras the docs recommend for reading
(
pypdf[full]: AES decryption, Brotli), nothing else. One worker process per CPU; per file 60 s / 120 s / 180 s / 300 s at most (the any-document table allows longer: some tools load models), killed above the memory limit. - Fails where another reads: the library raised an error, hung or crashed on a file from which another library in the same table got text. Files that every library fails on (encrypted, truncated, fuzzed) are not counted against anyone.
- Empty where another reads: no error, but no text. This is often the recipe, not the library: for example
python-docx's paragraphs+tables recipe skips headers, text boxes and nested tables. Check the file list. - Any document → text: a file type the tool's docs don't list, or that the tool itself rejects as unsupported, counts as "not supported", not as a failure (lists in board/libs.json). Returning a file's raw RTF source counts as empty, for every tool. Each tool runs with default settings: no OCR switched on by hand.
- OCR: each image comes with the human-written ground-truth text published with its dataset. Words found is the share of its words that appear anywhere in the engine's output (reading order doesn't matter); an image that fails or comes back empty counts as 0. The ground truth of receipts and document pages covers only their annotated text, so a character error rate would punish engines for reading more than was annotated; it is in the raw results but not ranked. English models, CPU, default settings.
- Speech: each clip comes with its dataset's reference transcript. Both texts go through Whisper's English text normalizer (case, punctuation, "twenty" = "20"); word error rate is all substituted, missing and extra words over all reference words, the usual corpus-level WER. A clip that fails or comes back empty counts all its words as errors. The three Whisper implementations use the same base.en model; each other engine its documented default model. English, CPU, default decoding settings.
- Language detection: each text's language is known from Tatoeba. An answer counts as correct if the library's code matches after mapping variants to ISO 639-1 (zh-cn → zh, nb → no, iw → he). Close languages count as different: Croatian detected as Bosnian or Serbian, or Norwegian as Danish, is wrong. One call per text with default settings and all languages enabled.
- Text quality (order, tables, layout) is not ranked in the other tables; they are about not breaking. The raw results have every output's size and hash.
- Same files for every version: the test collections are pinned to one commit. GitHub-hosted runner, so times are comparable within one table, roughly.