resolveImagePath($documento); if ($relativePath === null) { return ['status' => 'skipped', 'message' => 'Immagine non disponibile']; } if (! Storage::disk('local')->exists($relativePath)) { return ['status' => 'error', 'message' => 'File immagine 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']; } } $tesseract = $this->findBinary('tesseract'); if ($tesseract === null) { $this->updateOcrMetadata($documento, ['status' => 'pending_image_ocr', 'engine' => null, 'message' => 'tesseract non disponibile']); return ['status' => 'skipped', 'message' => 'Tesseract non disponibile']; } $absolutePath = Storage::disk('local')->path($relativePath); $language = (string) config('services.tesseract.lang', 'ita+eng'); $command = escapeshellcmd($tesseract) . ' ' . escapeshellarg($absolutePath) . ' stdout -l ' . escapeshellarg($language) . ' 2>/dev/null'; $output = shell_exec($command); $text = $this->normalizeText(is_string($output) ? $output : ''); if ($text === '') { $this->updateOcrMetadata($documento, ['status' => 'empty', 'engine' => 'tesseract', 'language' => $language]); return ['status' => 'error', 'message' => 'OCR immagine vuota']; } $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 (Schema::hasColumn('documenti', 'metadati_ocr')) { $update['metadati_ocr'] = array_merge((array) ($documento->metadati_ocr ?? []), [ 'status' => 'ok', 'engine' => 'tesseract', 'language' => $language, 'extracted_at' => now()->toIso8601String(), ]); } if ($update === []) { return ['status' => 'error', 'message' => 'Schema documenti senza colonne OCR/testo']; } $documento->forceFill($update)->save(); return ['status' => 'ok', 'chars' => mb_strlen($text), 'engine' => 'tesseract']; } private function resolveImagePath(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); foreach (['.png', '.jpg', '.jpeg', '.webp', '.tif', '.tiff', '.bmp'] as $extension) { if (str_ends_with($lower, $extension)) { return $candidate; } } return null; } 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 { $text = str_replace("\r\n", "\n", $text); $text = str_replace("\r", "\n", $text); $text = str_replace("\0", '', $text); if (! mb_check_encoding($text, 'UTF-8')) { $text = mb_convert_encoding($text, 'UTF-8'); } return trim($text); } private function updateOcrMetadata(Documento $documento, array $metadata): void { if (! Schema::hasColumn('documenti', 'metadati_ocr')) { return; } $documento->forceFill([ 'metadati_ocr' => array_merge((array) ($documento->metadati_ocr ?? []), $metadata), ])->save(); } }