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); } }