← Code-Übersicht

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 '';
    }
}