En muchas aplicaciones es común tener que extraer contenido de diferentes tipos de archivos: documentos de Word, archivos de texto plano, PDFs o incluso imágenes que contienen texto. Para lograrlo, podemos aprovechar distintas librerías y técnicas de procesamiento de archivos en Laravel.
En este artículo vamos a ver cómo hacerlo, analizando paso a paso un ejemplo real basado en FilamentPHP y Laravel.
El primer paso consiste en obtener el archivo subido por el usuario y determinar su tipo MIME. Esto nos dirá si se trata de un .txt, .pdf, .docx, .odt o una imagen.
Teniendo en cuenta que tengo guardado el fichero en mi disco local, podemos comprobar su mime con el Facade Storage tal que así:
$mime = Storage::disk('local')->mimeType($path);
Una vez identificado el tipo de archivo, aplicamos una estrategia distinta para extraer su contenido.
En el caso de los archivos .txt, basta con leer el contenido directamente desde el almacenamiento:
if ($mime === 'text/plain') {
$file = Storage::disk('local')->get($data['file']);
$content = $file;
}
Esto devuelve exactamente lo que está escrito en el archivo.
Para los archivos PDF, se puede utilizar la librería Smalot/PdfParser. Esta herramienta analiza la estructura interna del PDF y devuelve el texto:
elseif ($mime === 'application/pdf') {
$parser = new Parser();
$pdf = $parser->parseFile(storage_path('/app/private/' . $path));
$content = $pdf->getText();
}
Es importante tener en cuenta que esta técnica funciona bien con PDFs que contienen texto, pero no con aquellos que son solo imágenes escaneadas (para eso usaremos OCR más adelante).
Los archivos .docx y .odt en realidad son archivos comprimidos ZIP que contienen varios XML internos. Dentro de ellos se encuentra el contenido del documento.
En el ejemplo, se usa ZipArchive para abrir el archivo y extraer el XML que corresponde al texto. De esta manera, podemos obtener texto plano eliminando etiquetas de estilo y estructura.
function extractTextFromDocument(string $filePath, string $extension): string
{
$zip = new \ZipArchive();
if ($zip->open($filePath) === true) {
if ($extension === 'docx') {
$index = $zip->locateName('word/document.xml');
$xml = $zip->getFromIndex($index);
$xml = str_replace(['<w:p>', '<w:br/>'], "\n", $xml);
$text = strip_tags($xml);
} elseif ($extension === 'odt') {
$index = $zip->locateName('content.xml');
$xml = $zip->getFromIndex($index);
$xml = str_replace(['<text:p>', '<text:line-break/>'], "\n", $xml);
$text = strip_tags($xml);
}
$zip->close();
}
return $text;
}
Cuando el archivo es una imagen (image/*), necesitamos un motor de OCR (Reconocimiento Óptico de Caracteres). En este caso se utiliza Tesseract OCR junto con su wrapper en PHP. Esto permite extraer texto de fotografías, escaneos y capturas de pantalla.
elseif (str_starts_with($mime, 'image/')) {
$fileFullPath = storage_path('/app/private/' . $path);
try {
$content = (new TesseractOCR($fileFullPath))
->lang('spa') // idioma español
->run();
if (empty(trim($content))) {
$content = '[No se pudo reconocer texto en la imagen]';
}
} catch (\thiagoalessio\TesseractOCR\UnsuccessfulCommandException $e) {
$content = '[Error al procesar OCR: ' . $e->getMessage() . ']';
}
}
Debajo dejo el código completo que utilizo para un sistema simple en que uso todos estos apartados:
$path = $data['file'];
$mime = Storage::disk('local')->mimeType($path);
$content = null;
// Ficheros de texto plano
if ($mime === 'text/plain') {
$file = Storage::disk('local')->get($data['file']);
$content = $file;
}
//Ficeros de tipo .pdf
elseif ($mime === 'application/pdf') {
$parser = new Parser();
$pdf = $parser->parseFile(storage_path('/app/private/' . $path));
$content = $pdf->getText();
}
// Ficheros de tipo .docx
elseif ($mime === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
$content = $this->extractTextFromDocument(storage_path('/app/private/' . $path), 'docx');
}
// Ficheros de tipo .odt
elseif ($mime === 'application/vnd.oasis.opendocument.text') {
$content = $this->extractTextFromDocument(storage_path('/app/private/' . $path), 'odt');
}
// Ficheros de tipo image/*
elseif (str_starts_with($mime, 'image/')) {
$fileFullPath = storage_path('/app/private/' . $path);
try {
$content = (new TesseractOCR($fileFullPath))
->lang('spa')
->run();
// Si no reconoce texto, podemos dejar un mensaje
if (empty(trim($content))) {
$content = '[No se pudo reconocer texto en la imagen]';
}
} catch (\thiagoalessio\TesseractOCR\UnsuccessfulCommandException $e) {
$content = '[Error al procesar OCR: ' . $e->getMessage() . ']';
}
}
return File::create([
'user_id' => auth()->id(),
'name_original' => $data['file_original_name'], // ✅ nombre original
'name_generated' => $data['file'], // ✅ nombre generado
'extension' => pathinfo($data['file'], PATHINFO_EXTENSION),
'size' => Storage::disk('local')->size($data['file']),
'content' => $content,
]);