140 lines
4.6 KiB
PHP
140 lines
4.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Documenti;
|
|
|
|
use App\Models\Documento;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Smalot\PdfParser\Parser;
|
|
|
|
class PdfTextExtractionService
|
|
{
|
|
/**
|
|
* Estrae il testo da un PDF e lo salva su `documenti.contenuto_ocr` (se presente).
|
|
*
|
|
* @return array{status: 'ok'|'skipped'|'error', message?: string, chars?: int, engine?: string}
|
|
*/
|
|
public function extractAndStore(Documento $documento, bool $force = false): array
|
|
{
|
|
$relativePath = $this->resolvePdfPath($documento);
|
|
if ($relativePath === null) {
|
|
return ['status' => 'skipped', 'message' => 'PDF non disponibile'];
|
|
}
|
|
|
|
if (! Storage::disk('local')->exists($relativePath)) {
|
|
return ['status' => 'error', 'message' => 'File PDF non trovato su storage'];
|
|
}
|
|
|
|
if (! $force) {
|
|
$existing = null;
|
|
if (Schema::hasColumn('documenti', 'contenuto_ocr')) {
|
|
$existing = $documento->contenuto_ocr;
|
|
} elseif (Schema::hasColumn('documenti', 'testo_estratto_ocr')) {
|
|
$existing = $documento->testo_estratto_ocr;
|
|
}
|
|
|
|
if (is_string($existing) && trim($existing) !== '') {
|
|
return ['status' => 'skipped', 'message' => 'Testo già presente'];
|
|
}
|
|
}
|
|
|
|
$absolutePath = Storage::disk('local')->path($relativePath);
|
|
|
|
try {
|
|
[$text, $engine] = $this->extractText($absolutePath);
|
|
} catch (\Throwable $e) {
|
|
return ['status' => 'error', 'message' => $e->getMessage()];
|
|
}
|
|
|
|
$text = $this->normalizeText($text);
|
|
$chars = mb_strlen($text);
|
|
|
|
if ($chars <= 0) {
|
|
return ['status' => 'error', 'message' => 'Testo PDF vuoto (forse è una scansione)'];
|
|
}
|
|
|
|
$update = [];
|
|
if (Schema::hasColumn('documenti', 'contenuto_ocr')) {
|
|
$update['contenuto_ocr'] = $text;
|
|
}
|
|
if (Schema::hasColumn('documenti', 'testo_estratto_ocr')) {
|
|
$update['testo_estratto_ocr'] = mb_substr($text, 0, 20000);
|
|
}
|
|
|
|
if (empty($update)) {
|
|
return ['status' => 'error', 'message' => 'Schema documenti senza colonne OCR/testo'];
|
|
}
|
|
|
|
$documento->forceFill($update)->save();
|
|
|
|
return ['status' => 'ok', 'chars' => $chars, 'engine' => $engine];
|
|
}
|
|
|
|
private function resolvePdfPath(Documento $documento): ?string
|
|
{
|
|
$candidate = null;
|
|
|
|
if (isset($documento->percorso_file) && is_string($documento->percorso_file) && $documento->percorso_file !== '') {
|
|
$candidate = $documento->percorso_file;
|
|
} elseif (isset($documento->path_file) && is_string($documento->path_file) && $documento->path_file !== '') {
|
|
$candidate = $documento->path_file;
|
|
}
|
|
|
|
if (! is_string($candidate) || trim($candidate) === '') {
|
|
return null;
|
|
}
|
|
|
|
$candidate = trim($candidate);
|
|
$lower = strtolower($candidate);
|
|
if (! str_ends_with($lower, '.pdf')) {
|
|
return null;
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
|
|
/**
|
|
* @return array{0:string,1:string} [text, engine]
|
|
*/
|
|
private function extractText(string $absolutePath): array
|
|
{
|
|
// Prefer pdftotext if available: è più robusto su tanti PDF “reali”.
|
|
$pdftotext = $this->findBinary('pdftotext');
|
|
if ($pdftotext !== null) {
|
|
$cmd = escapeshellcmd($pdftotext) . ' -layout -nopgbrk ' . escapeshellarg($absolutePath) . ' -';
|
|
$output = shell_exec($cmd);
|
|
$output = is_string($output) ? $output : '';
|
|
if (trim($output) !== '') {
|
|
return [$output, 'pdftotext'];
|
|
}
|
|
}
|
|
|
|
// Fallback PHP parser (no system deps).
|
|
$parser = new Parser();
|
|
$pdf = $parser->parseFile($absolutePath);
|
|
return [$pdf->getText(), 'smalot/pdfparser'];
|
|
}
|
|
|
|
private function findBinary(string $name): ?string
|
|
{
|
|
$path = shell_exec('command -v ' . escapeshellarg($name) . ' 2>/dev/null');
|
|
$path = is_string($path) ? trim($path) : '';
|
|
return $path !== '' ? $path : null;
|
|
}
|
|
|
|
private function normalizeText(string $text): string
|
|
{
|
|
// Normalize line endings and strip null bytes.
|
|
$text = str_replace("\r\n", "\n", $text);
|
|
$text = str_replace("\r", "\n", $text);
|
|
$text = str_replace("\0", '', $text);
|
|
|
|
// Best-effort encoding normalization.
|
|
if (! mb_check_encoding($text, 'UTF-8')) {
|
|
$text = mb_convert_encoding($text, 'UTF-8');
|
|
}
|
|
|
|
return trim($text);
|
|
}
|
|
}
|