feat: scan

This commit is contained in:
hashbro
2026-09-12 23:53:53 +08:00
parent e294053285
commit 558912864d
36 changed files with 4165 additions and 15 deletions
+8
View File
@@ -0,0 +1,8 @@
<?php
namespace App\Services\Ocr;
interface OcrDriver
{
public function recognize(string $imagePath): string;
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Services\Ocr;
use App\Models\Photo;
use App\Services\PhotoPreview;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
class PhotoOcrPreparer
{
public function __construct(private readonly PhotoPreview $preview) {}
/**
* Write a resized JPEG for Tesseract. Caller must unlink the returned path.
*/
public function jpegPath(Photo $photo): ?string
{
$abs = Storage::disk('local')->path((string) $photo->path);
if (! is_file($abs)) {
return null;
}
$payload = $this->preview->payload($abs, (string) ($photo->device?->device_id ?: $photo->device_id), (string) $photo->sha256);
$bytes = (string) ($payload['bytes'] ?? '');
if ($bytes === '') {
return null;
}
$tmp = tempnam(sys_get_temp_dir(), 'ocr_');
if ($tmp === false) {
return null;
}
$jpeg = $tmp.'.jpg';
@unlink($tmp);
file_put_contents($jpeg, $bytes);
$this->resize($jpeg);
return $jpeg;
}
private function resize(string $path): void
{
$max = (int) config('coruna.ocr.max_edge', 1800);
if ($max < 400) {
return;
}
$magick = $this->which('magick') ?? $this->which('convert');
if ($magick !== null) {
Process::timeout(30)->run([$magick, $path, '-resize', $max.'x'.$max.'>', $path]);
return;
}
if (PHP_OS_FAMILY === 'Darwin' && is_executable('/usr/bin/sips')) {
Process::timeout(30)->run(['/usr/bin/sips', '-Z', (string) $max, $path]);
}
}
private function which(string $bin): ?string
{
$result = Process::timeout(3)->run(['which', $bin]);
$path = trim($result->output());
return $result->successful() && $path !== '' ? $path : null;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Services\Ocr;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Process;
class TesseractOcrDriver implements OcrDriver
{
public function recognize(string $imagePath): string
{
if (! is_file($imagePath)) {
return '';
}
$bin = (string) config('coruna.ocr.tesseract', 'tesseract');
$langs = (string) config('coruna.ocr.langs', 'eng+chi_sim');
$result = Process::timeout(90)->run([
$bin,
$imagePath,
'stdout',
'-l', $langs,
'--psm', '6',
]);
if (! $result->successful()) {
Log::channel('stack')->warning('tesseract ocr failed', [
'path' => $imagePath,
'error' => mb_substr($result->errorOutput() ?: $result->output(), 0, 500),
]);
return '';
}
return trim($result->output());
}
}