package ocr import ( "bytes" "fmt" "image" "image/jpeg" ) // buildTextLayerPDF erzeugt ein minimales, aber ECHTES PDF mit einer // vektorbasierten Textebene (Standardfont Helvetica, kein eingebettetes // Fontprogramm nötig) — von Hand gebaut, keine externe Bibliothek/kein // externes Werkzeug, damit der Test reproduzierbar und unabhängig von // Systempaketen bleibt. pdftotext extrahiert text unmittelbar daraus. func buildTextLayerPDF(text string) []byte { var buf bytes.Buffer offsets := make([]int, 0, 6) buf.WriteString("%PDF-1.4\n") writeObj := func(n int, body string) { offsets = append(offsets, buf.Len()) fmt.Fprintf(&buf, "%d 0 obj\n%s\nendobj\n", n, body) } writeObj(1, "<< /Type /Catalog /Pages 2 0 R >>") writeObj(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>") writeObj(3, "<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 5 0 R >> >> /MediaBox [0 0 400 200] /Contents 4 0 R >>") content := fmt.Sprintf("BT /F1 24 Tf 20 100 Td (%s) Tj ET", text) stream := fmt.Sprintf("<< /Length %d >>\nstream\n%s\nendstream", len(content), content) writeObj(4, stream) writeObj(5, "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>") xrefStart := buf.Len() fmt.Fprintf(&buf, "xref\n0 %d\n", len(offsets)+1) buf.WriteString("0000000000 65535 f \n") for _, off := range offsets { fmt.Fprintf(&buf, "%010d 00000 n \n", off) } fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF", len(offsets)+1, xrefStart) return buf.Bytes() } // buildImageOnlyPDF erzeugt ein minimales, ECHTES PDF, dessen einzige // Seite ausschließlich aus einem eingebetteten Rasterbild besteht (JPEG // via /DCTDecode — von PDF nativ unterstützt, kein zusätzlicher Codec // nötig) — simuliert ein gescanntes PDF ohne Textebene // (Akzeptanzkriterium 1). func buildImageOnlyPDF(img image.Image) ([]byte, error) { var jpegBuf bytes.Buffer if err := jpeg.Encode(&jpegBuf, img, &jpeg.Options{Quality: 90}); err != nil { return nil, fmt.Errorf("jpeg kodieren: %w", err) } bounds := img.Bounds() w, h := bounds.Dx(), bounds.Dy() var buf bytes.Buffer offsets := make([]int, 0, 6) buf.WriteString("%PDF-1.4\n") writeObj := func(n int, header string, body []byte) { offsets = append(offsets, buf.Len()) fmt.Fprintf(&buf, "%d 0 obj\n%s\n", n, header) if body != nil { buf.Write(body) buf.WriteString("\nendstream\n") } buf.WriteString("endobj\n") } writeObj(1, "<< /Type /Catalog /Pages 2 0 R >>", nil) writeObj(2, "<< /Type /Pages /Kids [3 0 R] /Count 1 >>", nil) writeObj(3, fmt.Sprintf("<< /Type /Page /Parent 2 0 R /Resources << /XObject << /Im0 5 0 R >> >> /MediaBox [0 0 %d %d] /Contents 4 0 R >>", w, h), nil) content := fmt.Sprintf("q %d 0 0 %d 0 0 cm /Im0 Do Q", w, h) writeObj(4, fmt.Sprintf("<< /Length %d >>\nstream", len(content)), []byte(content)) imgHeader := fmt.Sprintf( "<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream", w, h, jpegBuf.Len(), ) writeObj(5, imgHeader, jpegBuf.Bytes()) xrefStart := buf.Len() fmt.Fprintf(&buf, "xref\n0 %d\n", len(offsets)+1) buf.WriteString("0000000000 65535 f \n") for _, off := range offsets { fmt.Fprintf(&buf, "%010d 00000 n \n", off) } fmt.Fprintf(&buf, "trailer\n<< /Size %d /Root 1 0 R >>\nstartxref\n%d\n%%%%EOF", len(offsets)+1, xrefStart) return buf.Bytes(), nil }