package ocr import ( "bytes" "context" "image/png" "os/exec" "strings" "testing" "time" ) func requireTools(t *testing.T) { t.Helper() for _, tool := range []string{"tesseract", "pdftotext", "pdftoppm"} { if _, err := exec.LookPath(tool); err != nil { t.Skipf("%s nicht im PATH, Integrationstest übersprungen", tool) } } } // knownTextImage rastert ein hand-gebautes Text-PDF (bekannter Inhalt) // über die echte pdftoppm-Pipeline in ein reales PNG-Bild — liefert damit // ein Bild mit tatsächlich gerenderten, OCR-fähigen Glyphen, ohne eine // zusätzliche Font-Rendering-Bibliothek einzuführen. func knownTextImage(t *testing.T, ctx context.Context, text string) []byte { t.Helper() pdfBytes := buildTextLayerPDF(text) pages, err := rasterizePDF(ctx, pdfBytes) if err != nil { t.Fatalf("testbild rastern: %v", err) } if len(pages) != 1 { t.Fatalf("erwartete genau 1 gerasterte seite, habe %d", len(pages)) } return pages[0] } // TestExtractTextFromImage_KnownTextRecognized ist die geforderte // Pflichtprüfung 1: Bildanhang mit bekanntem Text liefert erwartete // Texterkennung. func TestExtractTextFromImage_KnownTextRecognized(t *testing.T) { requireTools(t) ctx := context.Background() imageBytes := knownTextImage(t, ctx, "Rechnungsnummer 4711") got, err := ExtractTextFromImage(ctx, imageBytes) if err != nil { t.Fatalf("extracttextfromimage: %v", err) } if !strings.Contains(got, "Rechnungsnummer") || !strings.Contains(got, "4711") { t.Fatalf("erwarteten text nicht erkannt, habe: %q", got) } } // TestExtractTextFromPDF_SkipsOCRWhenTextLayerPresent ist die geforderte // Pflichtprüfung 2: PDF mit vorhandener Textebene wird korrekt // übersprungen. func TestExtractTextFromPDF_SkipsOCRWhenTextLayerPresent(t *testing.T) { requireTools(t) ctx := context.Background() pdfBytes := buildTextLayerPDF("Vertragsentwurf Version 3") hasLayer, err := HasTextLayer(ctx, pdfBytes) if err != nil { t.Fatalf("hastextlayer: %v", err) } if !hasLayer { t.Fatal("erwartete erkannte textebene im vektor-pdf") } result, err := ExtractTextFromPDF(ctx, pdfBytes) if err != nil { t.Fatalf("extracttextfrompdf: %v", err) } if result.OCRPerformed { t.Fatal("ocr wurde durchgeführt, obwohl eine textebene vorhanden war") } if !strings.Contains(result.Text, "Vertragsentwurf") { t.Fatalf("erwarteten text aus textebene nicht gefunden, habe: %q", result.Text) } } // TestExtractTextFromPDF_PerformsOCRWhenNoTextLayer deckt // Akzeptanzkriterium 1 (gescannte PDF-Anhänge) zusätzlich end-zu-Ende ab: // ein PDF ohne Textebene, das nur ein Rasterbild enthält, wird real per // OCR erkannt. func TestExtractTextFromPDF_PerformsOCRWhenNoTextLayer(t *testing.T) { requireTools(t) ctx := context.Background() pngBytes := knownTextImage(t, ctx, "Gescannter Anhang Test") img, err := png.Decode(bytes.NewReader(pngBytes)) if err != nil { t.Fatalf("testbild dekodieren: %v", err) } scannedPDF, err := buildImageOnlyPDF(img) if err != nil { t.Fatalf("bild-pdf bauen: %v", err) } hasLayer, err := HasTextLayer(ctx, scannedPDF) if err != nil { t.Fatalf("hastextlayer: %v", err) } if hasLayer { t.Fatal("bild-only-pdf wurde fälschlich als textebene erkannt") } result, err := ExtractTextFromPDF(ctx, scannedPDF) if err != nil { t.Fatalf("extracttextfrompdf: %v", err) } if !result.OCRPerformed { t.Fatal("erwartete durchgeführte ocr bei fehlender textebene") } if !strings.Contains(result.Text, "Gescannter") { t.Fatalf("erwarteten text aus ocr nicht gefunden, habe: %q", result.Text) } } // TestExtractTextFromPDF_ThroughputIsAcceptable ist die geforderte // Pflichtprüfung 3: Durchsatztest bestätigt akzeptable Verarbeitungszeit // je Anhang. Zielzeit 8s je Anhang (großzügig für Rasterung + OCR bei // 300dpi, deckt aber real ab, dass kein pathologischer Ausreißer // entsteht). func TestExtractTextFromPDF_ThroughputIsAcceptable(t *testing.T) { requireTools(t) ctx := context.Background() const attachments = 3 const targetPerAttachment = 8 * time.Second pngBytes := knownTextImage(t, ctx, "Durchsatztest Anhang") img, err := png.Decode(bytes.NewReader(pngBytes)) if err != nil { t.Fatalf("testbild dekodieren: %v", err) } scannedPDF, err := buildImageOnlyPDF(img) if err != nil { t.Fatalf("bild-pdf bauen: %v", err) } start := time.Now() for i := 0; i < attachments; i++ { if _, err := ExtractTextFromPDF(ctx, scannedPDF); err != nil { t.Fatalf("anhang %d: %v", i, err) } } elapsed := time.Since(start) perAttachment := elapsed / attachments t.Logf("Durchsatz: %d Anhänge in %s (%s/Anhang, Ziel %s/Anhang)", attachments, elapsed, perAttachment, targetPerAttachment) if perAttachment > targetPerAttachment { t.Fatalf("verarbeitung zu langsam: %s/anhang, ziel %s/anhang", perAttachment, targetPerAttachment) } }