thiagoalessio/tesseract_ocr
A simple PHP wrapper for Tesseract OCR that lets you extract text from images with a clean, fluent API. Configure languages, page segmentation, and other options easily, without dealing with shell commands directly.
The package is a thin, dependency-free wrapper around the Tesseract binary — there is no service provider, facade, or config file shipped with it. You need both halves installed.
1. Install the system binary first (this is what people forget):
# Debian/Ubuntu
sudo apt-get install tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu
# macOS
brew install tesseract tesseract-lang
# Verify
tesseract --version
tesseract --list-langs
2. Then the PHP side:
composer require thiagoalessio/tesseract_ocr
3. First use case — extract text from an uploaded invoice/receipt:
use thiagoalessio\TesseractOCR\TesseractOCR;
$text = (new TesseractOCR(storage_path('app/invoices/2024-001.png')))
->lang('eng')
->run();
// $text is a plain string of the recognized text
Where to look first:
vendor/thiagoalessio/tesseract_ocr/src/TesseractOCR.php is worth skimming once to see every option.TesseractOCR::availableLanguages() is the fastest way to confirm your binary, path, and language packs are all wired correctly before you write any feature code.->run(). Do not start with PDFs, multi-page TIFFs, or photos from a phone — you will conclude the package is broken when the real problem is input quality.Because the library is stateless and CLI-backed, the cleanest Laravel integration is a small service in the container. Put the binary path and defaults in config so staging/production can differ.
// config/ocr.php
return [
'executable' => env('TESSERACT_PATH', '/usr/bin/tesseract'),
'lang' => env('TESSERACT_LANG', 'eng'),
'psm' => 3,
'temp_dir' => storage_path('app/ocr/tmp'),
];
namespace App\Services;
use thiagoalessio\TesseractOCR\TesseractOCR;
class OcrService
{
public function make(string $absolutePath, array $opts = []): TesseractOCR
{
return (new TesseractOCR($absolutePath))
->executable(config('ocr.executable'))
->lang(...(array) ($opts['lang'] ?? config('ocr.lang')))
->psm((int) ($opts['psm'] ?? config('ocr.psm')))
->tempDir(config('ocr.temp_dir'));
}
public function text(string $absolutePath, array $opts = []): string
{
return trim($this->make($absolutePath, $opts)->run());
}
}
Binding the executable() explicitly is the single highest-value thing you can do — it removes the entire class of "works on my machine" bugs when the binary lives outside $PATH (Forge, Docker, shared hosting).
OCR is CPU-bound and routinely takes 1–30 seconds per page. Doing it inline in a controller will blow up your request time and, on multi-page documents, hit PHP's max_execution_time.
class ExtractDocumentText implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $timeout = 120; // must be < queue retry_after
public int $tries = 3;
public int $backoff = 30;
public function __construct(public Document $document) {}
public function handle(OcrService $ocr): void
{
$path = Storage::disk('local')->path($this->document->path);
$text = $ocr->text($path, ['psm' => 6]);
$this->document->update([
'text' => $text,
'ocr_at' => now(),
]);
}
}
Add WithoutOverlapping if a user can re-trigger extraction, and consider a dedicated ocr queue so a backlog of documents doesn't starve your mail jobs.
Tesseract's accuracy is dominated by input quality. Greyscale, upscale small images to ~300 DPI equivalent, and binarize. Intervention Image v3 or Imagick both work:
$image = Image::read($uploaded)
->greyscale()
->resize(2000, null) // upscale helps a lot for screenshots/phone photos
->sharpen(10);
$image->save($cleanPath);
For license plates, MRZ lines, invoice totals, or meter readings, an allowlist is worth more than any tuning:
(new TesseractOCR($plate))
->allowlist('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
->psm(7) // treat as a single text line
->run();
psm(7) (single line) and psm(6) (single uniform block) are the two you'll reach for most often; psm(3), the default, is a fully automatic page segmentation that can be surprisingly bad on cropped regions.
configFile('tsv') or configFile('hocr') gives you per-word bounding boxes, confidences, and line grouping — essential if you want to highlight matched text in the UI or crop regions:
$tsv = (new TesseractOCR($path))->configFile('tsv')->run();
// tab-separated: level page_num block_num par_num line_num word_num
// left top width height conf text
foreach (array_filter(explode("\n", $tsv)) as $row) {
[$level, $page, $block, $par, $line, $word,
$left, $top, $w, $h, $conf, $text] = explode("\t", $row);
if ((int) $level === 5 && (float) $conf > 60) {
// $text at ($left, $top) with confidence $conf
}
}
OCR output for a given image + parameter set never changes. Hash the file and the options and cache the result:
$key = 'ocr:'.hash_file('xxh128', $path).':'.md5(json_encode($opts));
return Cache::remember($key, now()->addMonth(), fn () => $ocr->text($path, $opts));
This turns re-rendered admin pages from 8-second operations into instant ones.
Ship an artisan command that verifies the binary and language packs, and run it in your deploy pipeline:
$this->info('Languages: '.implode(', ', TesseractOCR::availableLanguages()));
The binary, not the package, is the dependency. 90% of "it doesn't work" issues are TesseractNotFoundException. On Forge/Docker, the binary is often at /usr/bin/tesseract — set TESSERACT_PATH explicitly rather than trusting $PATH. On Vapor/Lambda you must bundle it in the Docker image or a layer; it is not installable at runtime.
proc_open / exec may be disabled. The wrapper shells out. Shared hosts that disable these functions in php.ini will fail with a confusing error. Check disable_functions before promising OCR to a client.
Exceptions are your control flow. run() throws UnsuccessfulCommandException, ImageNotFoundException, FileNotFoundException, and TesseractNotFoundException. Catch them in the job, mark the document as failed with a reason, and don't let one bad image retry forever — three retries against a corrupt PNG is pure waste. Conversely, Tesseract exits 0 on blank or unreadable images and returns an empty string: always check trim($text) === '' and treat that as a soft failure.
Language packs are per-language packages. ->lang('deu') fails if tesseract-ocr-deu isn't installed, even though tesseract-ocr is. Multiple languages are passed variadically (->lang('eng', 'deu')) and slow OCR roughly linearly — only enable what you need. ->lang('osd') is required for orientation detection.
Do not JSON-encode configFile('pdf') output. Any non-hocr/tsv/txt config file returns binary. Write it to disk with Storage::put() and stream it back — piping a PDF body into a JSON response produces a very confusing encoding error.
Prefer Storage::disk('local')->path() over URLs. Tesseract needs a real filesystem path. S3, GCS, and remote disks must be Storage::get() + tempnam() + cleanup first. This is the most common integration mistake in Laravel apps.
Set a temp directory you control. ->tempDir(config('ocr.temp_dir')) avoids failures in read-only or tiny /tmp mounts (common in containers). Create the directory in your deploy script. If you're hitting disk pressure, ->withoutTempFile() avoids intermediate files when capturing stdout.
Watch the concurrency. Tesseract uses OpenMP and will happily eat every core. On a queue worker running 4 processes, that's 4× N threads. ->threadLimit(2) plus a queue maxProcesses cap keeps your box from thrashing.
Queue retry_after must exceed job $timeout. A 120-second job with the default 90-second retry_after will be picked up twice and run in parallel — you'll see duplicate writes and doubled CPU load.
PDFs need rasterizing first. Hand Tesseract a PDF and you'll get partial or no results. Render pages to PNG via Imagick/Ghostscript (or use spatie/pdf-to-image) and OCR each page. For multi-page input, chain ->image() per page and concatenate output yourself.
Locale and encoding. Non-ASCII output can be mangled if the worker's locale is unset. Set LC_ALL=C.UTF-8 in the supervisor environment if you're seeing broken accented characters.
Treat config values as code, not data. Never interpolate user input into ->config() or ->configFile() — those are passed to Tesseract as arguments and can reference files on disk. Validate uploads (mime, size, dimensions) and re-encode through Intervention before OCR; that both normalizes the input and strips malicious payloads from the file.
Make it testable. run() is a hard external dependency, so define a small interface (DocumentOcr) around it and bind a fake in tests. Assertions on real OCR output are flaky across Tesseract versions — test your pre/post-processing logic, not the engine.
psm and oem are version-sensitive. Modes valid in Tesseract 4 may behave differently in 5. Pin the binary version in your Docker image so a apt-get upgrade doesn't silently change accuracy across environments.
How can I help you explore Laravel packages today?