PdftotextExtractor.php
Pfad: src/Infrastructure/Rechnung/PdftotextExtractor.php
Ext: php
Größe: 4462 Bytes
Geändert: 2026-07-16T14:13:08+02:00
Frühere Version vom 2026-07-16T14:13:08+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-freie Textextraktion mit pdftotext.
*/
final class PdftotextExtractor implements TextExtractionPort
{
/**
* @param string $binary Pfad zu pdftotext-Binary.
*/
public function __construct(
private string $binary = 'pdftotext'
) {
}
/**
* @inheritDoc
*/
public function extract(string $pdfPath): ExtractedInvoiceData
{
if (!is_file($pdfPath)) {
throw new \RuntimeException('pdftotext: Datei nicht gefunden');
}
$textPath = $pdfPath . '.txt';
$command = sprintf(
'%s %s %s 2>/dev/null',
escapeshellcmd($this->binary),
escapeshellarg($pdfPath),
escapeshellarg($textPath)
);
exec($command, $output, $exitCode);
if (!is_file($textPath) || $exitCode !== 0) {
throw new \RuntimeException('pdftotext: Extraktion fehlgeschlagen');
}
$text = (string) file_get_contents($textPath);
@unlink($textPath);
return $this->parseText($text);
}
/**
* @param string $text Extraktionsrohdaten.
* @return ExtractedInvoiceData Gesicherte Werte.
*/
private function parseText(string $text): ExtractedInvoiceData
{
$normalized = preg_replace('/\s+/', ' ', $text);
$vendorName = $this->firstLine($text);
$vendorAddress = $this->extractAddress($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, [
'/Netto\s*:?\s*([0-9]+(?:[\.,][0-9]{1,2})?)/i',
]) ?? 0.0);
$steuer = (float) ($this->extractPattern($normalized, [
'/Steuer\s*:?\s*([0-9]+(?:[\.,][0-9]{1,2})?)/i',
]) ?? 0.0);
$brutto = (float) ($this->extractPattern($normalized, [
'/Brutto\s*:?\s*([0-9]+(?:[\.,][0-9]{1,2})?)/i',
'/Gesamt\s*:?\s*([0-9]+(?:[\.,][0-9]{1,2})?)/i',
]) ?? 0.0);
$amount = new Amount($netto, $steuer, $brutto, 'EUR');
$confidence = 0.0;
if ($vendorName !== '') {
$confidence += 0.2;
}
if ($invoiceNumber !== '') {
$confidence += 0.2;
}
if ($invoiceDate !== '') {
$confidence += 0.2;
}
if ($netto > 0.0) {
$confidence += 0.2;
}
if ($amount->steuer > 0.0) {
$confidence += 0.2;
}
return new ExtractedInvoiceData(
vendorName: $vendorName,
vendorAddressSignature: $vendorAddress,
invoiceNumber: $invoiceNumber,
invoiceDate: $invoiceDate,
amount: $amount,
extractionConfidence: $confidence
);
}
/**
* @param string $text Rohtext.
* @param array $patterns Musterliste.
* @return string|null Treffer.
*/
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 Erste lesbare Zeile.
*/
private function firstLine(string $text): string
{
$lines = preg_split('/\R/', (string) preg_replace('/\s+/', ' ', $text));
foreach ($lines as $line) {
$candidate = trim((string) $line);
if (strlen($candidate) > 3) {
return $candidate;
}
}
return '';
}
/**
* @param string $text Text.
* @return string Adresse als Signatur.
*/
private function extractAddress(string $text): string
{
if (preg_match('/\b(?:[A-Za-zäöüÄÖÜß\-\.\s]+)\s*,\s*\d{5}\s*[A-Za-z]+/i', $text, $matches) === 1) {
return trim($matches[0]);
}
return '';
}
}