TesseractInvoiceExtractor.php
Pfad: src/Infrastructure/Rechnung/TesseractInvoiceExtractor.php
Ext: php
Größe: 3487 Bytes
Geändert: 2026-07-16T14:11:28+02:00
Frühere Version vom 2026-07-16T14:11:28+02:00 · zur aktuellen Fassung
<?php
declare(strict_types=1);
namespace Demo\Infrastructure\Rechnung;
use Demo\Domain\Rechnung\Amount;
use Demo\Domain\Rechnung\ExtractedInvoiceData;
use Demo\Domain\Rechnung\TextExtractionPort;
/**
* OCR-Fallback mit Tesseract.
*/
final class TesseractInvoiceExtractor implements TextExtractionPort
{
/**
* @param string $binary Pfad zu tesseract-Binary.
*/
public function __construct(private string $binary = 'tesseract')
{
}
/**
* @inheritDoc
*/
public function extract(string $pdfPath): ExtractedInvoiceData
{
if (!is_file($pdfPath)) {
throw new \RuntimeException('tesseract: Datei nicht gefunden');
}
$base = $pdfPath . '_ocr';
$command = sprintf(
'%s %s %s txt 2>/dev/null',
escapeshellcmd($this->binary),
escapeshellarg($pdfPath),
escapeshellarg($base)
);
exec($command, $output, $exitCode);
$textFile = $base . '.txt';
if (!is_file($textFile) || $exitCode !== 0) {
throw new \RuntimeException('tesseract: OCR fehlgeschlagen');
}
$text = (string) file_get_contents($textFile);
@unlink($textFile);
return $this->parseText($text);
}
/**
* @param string $text OCR-Rohtext.
* @return ExtractedInvoiceData Parsed.
*/
private function parseText(string $text): ExtractedInvoiceData
{
$normalized = preg_replace('/\s+/', ' ', $text);
$vendorName = $this->extractAddressLine($text);
$invoiceNumber = $this->extractPattern($normalized, [
'/Rechnungsnummer\s*[:#]?\s*([A-Za-z0-9\-\/_]+)/i',
'/Invoice\s*No\.?\s*[:#]?\s*([A-Za-z0-9\-\/_]+)/i',
]) ?? '';
$invoiceDate = $this->extractPattern($normalized, [
'/(\d{4}-\d{2}-\d{2})/',
'/(\d{2}\.\d{2}\.\d{4})/',
]) ?? '';
$netto = (float) ($this->extractPattern($normalized, ['/([0-9]+[\.,][0-9]{2})\s+Netto/i']) ?? 0.0);
$steuer = (float) ($this->extractPattern($normalized, ['/([0-9]+[\.,][0-9]{2})\s+Steuer/i']) ?? 0.0);
$brutto = (float) ($this->extractPattern($normalized, ['/([0-9]+[\.,][0-9]{2})\s+Brutto/i']) ?? 0.0);
$amount = new Amount($netto, $steuer, $brutto, 'EUR');
return new ExtractedInvoiceData(
vendorName: $vendorName,
vendorAddressSignature: $vendorName,
invoiceNumber: $invoiceNumber,
invoiceDate: $invoiceDate,
amount: $amount,
extractionConfidence: 0.4
);
}
/**
* @param string $text Text.
* @param array $patterns Patterns.
* @return string|null Match.
*/
private function extractPattern(string $text, array $patterns): ?string
{
foreach ($patterns as $pattern) {
if (preg_match($pattern, $text, $matches) === 1) {
return trim($matches[1] ?? '');
}
}
return null;
}
/**
* @param string $text Text.
* @return string Erster Kandidat.
*/
private function extractAddressLine(string $text): string
{
$parts = preg_split('/\R/', $text);
foreach ($parts as $line) {
$candidate = trim((string) $line);
if (strlen($candidate) > 3 && preg_match('/[A-Za-zäöüÄÖÜß]/', $candidate)) {
return $candidate;
}
}
return '';
}
}