Tesseract OCR in PHP, tested on 55 scans
3.2x
less CPU: one call, many images
The code, run on 55 scans
The package runs the tesseract command, so install that
first: brew install tesseract on a Mac,
apt-get install tesseract-ocr on Debian or Ubuntu.
composer require thiagoalessio/tesseract_ocr:2.13.0
<?php
require __DIR__ . '/vendor/autoload.php';
use thiagoalessio\TesseractOCR\TesseractOCR;
echo (new TesseractOCR('scan.jpg'))->dpi(300)->run();
55 of 55 scans, the command's text
13,858 of 14,693 words found, both
Works in the terminal, fails on the web
The same code under php-fpm, from Homebrew's PHP:
Error! The command "tesseract" was not found.
Make sure you have Tesseract OCR installed on your system:
https://github.com/tesseract-ocr/tesseract
The current $PATH is
The empty $PATH is the clue. php-fpm clears the
environment of its workers by default, so PHP cannot see
/opt/homebrew/bin. Either fix worked:
echo (new TesseractOCR('scan.jpg'))
->executable('/opt/homebrew/bin/tesseract')
->dpi(300)->run();
Or add this line to the pool's www.conf and restart php-fpm:
env[PATH] = /opt/homebrew/bin:/usr/bin:/bin
Set the dpi to match the command
My scans have no dpi in the file, so without
->dpi(300)
Tesseract guesses it. The page mode needs nothing: the default is psm
3, like the command's.
40 of 55 scans, the command's text without it
13,855 words found, 3 fewer
Same text, more CPU on small images
| Job | shell_exec | Wrapper |
|---|---|---|
| 55 full pages | 143.0 s | 144.6 s |
| 259 line images | 45.4 s | 54.5 s |
Before each image, the wrapper starts tesseract once more
to read its version. A big page hides that. A small image does not.
2 tesseract starts per image
4 with imageData() and
withoutTempFiles()
Many small images: one call
Tesseract reads a text file of image paths in one run. The model loads once:
$list = tempnam(sys_get_temp_dir(), 'list');
file_put_contents($list, implode("\n", $files) . "\n");
$text = (new TesseractOCR($list))
->dpi(300)->psm(7)
->config('page_separator', '[page]')
->run();
unlink($list);
$texts = explode('[page]', $text);
if (count($texts) !== count($files)) {
throw new RuntimeException('Tesseract stopped early.');
}
17.1 s for 259 lines, not 54.5 s
All 259 lines, the command's text
Keep the count check. A file Tesseract cannot read stops the run, and the wrapper returns the text so far with no error.
The wrapper trims form feeds, the default separator, from both ends. With a blank last image, 3 images came back as 2 parts.
PDFs: make gray images first
Two lines of the wrapper's PDF error:
Error! The command did not produce any output.
Error in pixReadStream: Pdf reading is not supported
Make an image of each page at 300 dpi first:
$dir = sys_get_temp_dir() . '/pdf-' . uniqid();
mkdir($dir);
exec('pdftoppm -r 300 -gray ' . escapeshellarg('scan.pdf') . ' ' . escapeshellarg("$dir/page"));
$text = '';
foreach (glob("$dir/page-*") as $img) {
$text .= (new TesseractOCR($img))->dpi(300)->run() . "\n";
unlink($img);
}
rmdir($dir);
| Pages to images | CPU a page | Words |
|---|---|---|
pdftoppm -png |
13.1 s | 1,879 |
pdftoppm -gray |
2.4 s | 1,882 |
pdfimages -all |
3.0 s | 1,878 |
PNG writing took most of the PNG run. -gray writes plain
PGM files.
Debian in Docker: same text as apt's command
FROM php:8.4-cli
RUN apt-get update \
&& apt-get install -y --no-install-recommends tesseract-ocr unzip \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /app
RUN composer require thiagoalessio/tesseract_ocr:2.13.0
COPY index.php scan.jpg ./
CMD ["php", "index.php"]
With the code above as index.php, it matched apt's
tesseract (5.5.0) on 5 of 5 scans.
How I tested it
- Pages: 55 proofread Wikisource scans from 11 printed books, JPEG, 1,920 pixels wide, no dpi in the file. The same set as Tesseract in C#.
- Score: true words found in order, with the scorer of the tessdata test.
- Apple Silicon Mac, PHP 8.5.12 and Composer 2.11.1 from Homebrew, the wrapper 2.13.0, Tesseract 5.5.4, tessdata_fast English.
- CPU time of the whole process tree, PHP start included, on the Mac's efficiency cores. A second round of the line jobs gave 52.8 s, 60.9 s and 18.0 s.
- Web: Homebrew's php-fpm 8.5.12 with the default clear_env, one scan, through a FastCGI client.
- PDF: the first 10 scans in one PDF, JPEG bytes kept, page size for 300 dpi. pdftoppm 26.03.0.
What I did not test
- Windows, Intel Macs, x86-64 Linux, and php-fpm on Linux.
- Apache with mod_php, Laravel Herd and Valet.
- Imagick for PDFs, and other wrapper versions.
- Other languages, tessdata_best, and photos.
Sources
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
Does the wrapper give the same text as the command?
Yes, on 55 of 55 scans, with the same Tesseract, the same model, psm 3 and 300 dpi on both. It runs the command and trims the ends.
Why does it work in the terminal but not in the browser?
php-fpm clears the environment of its workers by default, so PHP has no $PATH and cannot see Homebrew's tesseract. Pass the full path with executable(), or set env[PATH] in the pool.
Can it read a PDF?
No. Tesseract reads images only. Make an image of each page first, for example with pdftoppm -r 300 -gray, then read each image.