Procesamiento de Documentos e Imágenes mediante OCR en Laravel

- Andrés Cruz - EN In english

Video thumbnail

El reconocimiento óptico de caracteres (OCR, por sus siglas en inglés, Optical Character Recognition) es una tecnología que permite extraer texto a partir de imágenes o archivos PDF. En aplicaciones Laravel, esta funcionalidad se puede integrar fácilmente mediante paquetes especializados de procesamiento de archivos.

A diferencia de otras librerías, la instalación estándar es ligera. La publicación de archivos de configuración y migraciones es opcional, ya que solo se requieren si se desea almacenar en la base de datos el historial de procesamientos y el porcentaje de precisión obtenido.

Instalación y Configuración Básica

Para comenzar a utilizar el módulo de OCR en el proyecto, ejecutamos la instalación del paquete a través de Composer:

$ composer require mayaram/laravel-ocr

De forma opcional, si se requiere personalizar los motores de extracción o registrar el almacenamiento persistente, se pueden publicar los recursos del paquete:

$ php artisan vendor:publish --tag=laravel-ocr-config
$ php artisan vendor:publish --tag=laravel-ocr-migrations
$ php artisan migrate
$ php artisan laravel-ocr:doctor
$ php artisan laravel-ocr:process storage/app/sample-invoice.pdf --type=invoice

Implementación en el Controlador

Para procesar las solicitudes de carga de archivos, creamos un controlador con dos métodos principales: uno para renderizar el formulario de carga y otro para ejecutar el análisis de OCR sobre el documento cargado.

<?php

namespace App\Http\Controllers;

use App\Http\Requests\Ocr\ExtractTextRequest;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Storage;
use Mayaram\LaravelOcr\Exceptions\OCRException;
use Mayaram\LaravelOcr\Facades\LaravelOcr;

class OcrController extends Controller
{
    public function create(): View
    {
        return view('ocr.create');
    }

    public function store(ExtractTextRequest $request): View
    {
        $file = $request->file('document');
        $path = $file->store('ocr', 'local');

        $options = array_filter([
            'language' => $request->string('language')->toString() ?: null,
        ]);

        try {
            $result = LaravelOcr::extract(Storage::disk('local')->path($path), $options);
        } catch (OCRException $e) {
            report($e);

            return view('ocr.create', [
                'error' => 'No se pudo leer el documento: '.$e->getMessage(),
                'oldLanguage' => $request->string('language')->toString(),
            ]);
        }

        return view('ocr.result', [
            'fileName' => $file->getClientOriginalName(),
            'result' => $result,
        ]);
    }
}

Vista y Formulario en Blade

El formulario en Blade permite seleccionar el archivo (imagen o PDF) y definir el idioma del contenido para optimizar el motor de reconocimiento:

<div class="max-w-2xl mx-auto px-6">
    <div class="bg-white rounded-xl shadow p-6">
        <h1 class="text-2xl font-bold mb-2">OCR con Tesseract</h1>
        <p class="text-sm text-gray-500 mb-6">Sube una imagen (png, jpg, tiff, bmp) o un PDF y extraemos su texto.</p>

        @isset($error)
            <div class="mb-6 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm px-4 py-3">
                {{ $error }}
            </div>
        @endisset

        @if ($errors->any())
            <div class="mb-6 rounded-lg bg-red-50 border border-red-200 text-red-700 text-sm px-4 py-3">
                <ul class="list-disc ps-5 space-y-1">
                    @foreach ($errors->all() as $message)
                        <li>{{ $message }}</li>
                    @endforeach
                </ul>
            </div>
        @endif

        <form method="POST" action="{{ route('ocr.store') }}" enctype="multipart/form-data" class="space-y-5">
            @csrf

            <div>
                <label for="document" class="block text-sm font-semibold mb-2">Documento</label>
                <input id="document" name="document" type="file" required
                        accept=".png,.jpg,.jpeg,.pdf,.tiff,.bmp"
                        class="block w-full text-sm text-gray-600 file:mr-4 file:rounded-lg file:border-0 file:bg-blue-600 file:px-4 file:py-2 file:text-sm file:font-semibold file:text-white hover:file:bg-blue-700">
            </div>

            <div>
                <label for="language" class="block text-sm font-semibold mb-2">Idioma</label>
                <select id="language" name="language"
                        class="block w-full rounded-lg border border-gray-300 p-2 text-sm">
                    @foreach (['eng' => 'Inglés', 'spa' => 'Español', 'eng+spa' => 'Inglés + Español'] as $value => $label)
                        <option value="{{ $value }}" @selected(($oldLanguage ?? 'eng') === $value)>{{ $label }}</option>
                    @endforeach
                </select>
            </div>

            <button type="submit"
                    class="w-full bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold px-4 py-2 rounded-lg">
                Extraer texto
            </button>
        </form>
    </div>
</div>

Y para ver el resultado:

<div class="max-w-3xl mx-auto px-6 space-y-6">
    <div class="bg-white rounded-xl shadow p-6">
        <h1 class="text-2xl font-bold mb-6">Texto extraído</h1>

        <dl class="grid grid-cols-2 gap-4 text-sm mb-6">
            <div>
                <dt class="text-gray-500">Archivo</dt>
                <dd class="font-semibold">{{ $fileName }}</dd>
            </div>
            <div>
                <dt class="text-gray-500">Motor</dt>
                <dd class="font-semibold">{{ $result['metadata']['engine'] ?? '—' }}</dd>
            </div>
            <div>
                <dt class="text-gray-500">Idioma</dt>
                <dd class="font-semibold">{{ $result['metadata']['language'] ?? '—' }}</dd>
            </div>
            <div>
                <dt class="text-gray-500">Confianza</dt>
                <dd class="font-semibold">{{ round(($result['confidence'] ?? 0) * 100, 1) }}%</dd>
            </div>
        </dl>

        <h2 class="text-sm font-semibold text-gray-500 mb-2">Texto detectado</h2>
        <pre class="bg-gray-900 text-gray-100 rounded-lg p-4 text-sm whitespace-pre-wrap max-h-96 overflow-auto">{{ $result['text'] ?? '' }}</pre>

        <a href="{{ route('ocr.create') }}"
            class="inline-block mt-6 bg-blue-600 hover:bg-blue-700 text-white text-sm font-semibold px-4 py-2 rounded-lg">
            Subir otro documento
        </a>
    </div>
</div>

Análisis de Casos de Uso y Limitaciones Técnicas

La efectividad del procesamiento mediante OCR depende directamente de la calidad del archivo original y de los recursos de hardware disponibles en el servidor:

  • Capturas de pantalla planas o documentos estándar: Proporcionan un porcentaje de confianza cercano al 90%, logrando una extracción limpia del texto.
  • Imágenes con tipografías complejas o manuscritas: Presentan dificultades en el reconocimiento del trazado, lo que reduce el nivel de confianza o genera resultados inconsistentes.
  • Documentos PDF extensos: El procesamiento sincrónico de archivos de gran volumen (por ejemplo, manuales de más de 30 páginas) puede sobrepasar el tiempo límite de ejecución de PHP (time-out) o agotar la memoria asignada al servidor.

Para la manipulación de documentos extensos en entornos de producción, es recomendable delegar el procesamiento de OCR a colas de trabajo en segundo plano (Queues) o segmentar previamente el documento en bloques de páginas más pequeños.

Paquete con el cual podemos suministrar una imagen o PDF para obtener los textos, soporte para varios idiomas y factor de confianza.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.