← Code-Übersicht

TesseractInvoiceExtractor.php

Pfad: src/Infrastructure/Rechnung/TesseractInvoiceExtractor.php

Ext: php

Größe: 2668 Bytes

Geändert: 2026-07-16 16:57:10+02

<?php
declare(strict_types=1);

namespace Demo\Infrastructure\Rechnung;

use Demo\Domain\Rechnung\ExtractedInvoiceData;
use Demo\Domain\Rechnung\TextExtractionPort;
use RuntimeException;

/**
 * Rasterisiert PDF-Seiten und extrahiert Text lokal mit Tesseract.
 */
final class TesseractInvoiceExtractor implements TextExtractionPort
{
    /** Bindet OCR und Rasterizer. */
    public function __construct(private readonly string $binary = 'tesseract', private readonly string $rasterBinary = 'pdftoppm', private readonly ?InvoiceTextParser $parser = null)
    {
    }

    /** Rasterisiert alle Seiten, fuehrt OCR aus und parst den Gesamttext. */
    public function extract(string $pdfPath): ExtractedInvoiceData
    {
        if (!is_file($pdfPath)) {
            throw new RuntimeException('PDF fuer OCR fehlt');
        }
        $base = tempnam(sys_get_temp_dir(), 'invoice-ocr-');
        if ($base === false) {
            throw new RuntimeException('OCR-Temp-Prefix fehlt');
        }
        @unlink($base);
        $images = [];
        try {
            [$status, , $error] = $this->run([$this->rasterBinary, '-png', '-r', '300', $pdfPath, $base]);
            $images = glob($base . '-*.png') ?: [];
            if ($status !== 0 || $images === []) {
                throw new RuntimeException('PDF-Rasterisierung fehlgeschlagen: ' . trim($error));
            }
            $text = '';
            foreach ($images as $image) {
                [$ocrStatus, $stdout, $ocrError] = $this->run([$this->binary, $image, 'stdout']);
                if ($ocrStatus !== 0) {
                    throw new RuntimeException('Tesseract fehlgeschlagen: ' . trim($ocrError));
                }
                $text .= "\n" . $stdout;
            }
            return ($this->parser ?? new InvoiceTextParser())->parse($text, 0.60);
        } finally {
            foreach ($images as $image) {
                @unlink($image);
            }
        }
    }

    /**
     * @param list<string> $command Prozessargumente ohne Shell.
     * @return array{int,string,string} Exitcode, stdout, stderr.
     */
    private function run(array $command): array
    {
        $pipes = [];
        $process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
        if (!is_resource($process)) {
            throw new RuntimeException('OCR-Prozess kann nicht gestartet werden');
        }
        $stdout = stream_get_contents($pipes[1]);
        $stderr = stream_get_contents($pipes[2]);
        fclose($pipes[1]);
        fclose($pipes[2]);
        return [proc_close($process), $stdout === false ? '' : $stdout, $stderr === false ? '' : $stderr];
    }
}

Frühere Versionen

Version Zeitpunkt Operation
28 2026-07-16 19:01:40.815449+02 UPDATE