diff --git a/invoice_logic.py b/invoice_logic.py index 20ff525..21118f2 100644 --- a/invoice_logic.py +++ b/invoice_logic.py @@ -912,6 +912,34 @@ def generate_compliant_zugferd_cii(data): SRGB_ICC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "sRGB.icc") +def normalize_pdf_bytes(pdf_bytes: bytes) -> bytes: + """Oeffnet die PDF mit pikepdf und speichert sie neu. Das erzeugt eine saubere, + standardkonforme PDF-Struktur (Seitenbaum/XRef). Manche Ursprungs-PDFs (z.B. von PDF-XChange + erzeugte, die bereits ein OutputIntent tragen) haben eine Struktur, an der factur-x sonst + scheitert: die factur-x.xml wird nicht eingebettet und der Seitenbaum geht verloren + ('unable to find page tree'). Schlaegt das Normalisieren selbst fehl, werden die Original-Bytes + unveraendert zurueckgegeben, damit der Export nicht komplett blockiert.""" + try: + pdf = pikepdf.open(io.BytesIO(pdf_bytes)) + buf = io.BytesIO() + pdf.save(buf) + pdf.close() + return buf.getvalue() + except Exception: + return pdf_bytes + + +def pdf_has_embedded_facturx(pdf_bytes: bytes) -> bool: + """True, wenn die PDF wieder lesbar ist UND die eingebettete 'factur-x.xml' enthaelt. Dient als + Erfolgskontrolle nach der ZUGFeRD-Erzeugung - factur-x kann bei manchen PDF-Strukturen die XML + still weglassen bzw. eine kaputte PDF produzieren.""" + try: + with pikepdf.open(io.BytesIO(pdf_bytes)) as pdf: + return "factur-x.xml" in pdf.attachments + except Exception: + return False + + def add_pdfa_output_intent(pdf_bytes: bytes, icc_path: str = SRGB_ICC_PATH) -> bytes: """ Fügt der PDF ein OutputIntent mit sRGB-ICC-Profil hinzu, falls noch keines vorhanden ist. diff --git a/main.py b/main.py index 8070e76..275e817 100644 --- a/main.py +++ b/main.py @@ -22,6 +22,8 @@ from invoice_logic import ( format_cii_date, generate_compliant_xrechnung, generate_compliant_zugferd_cii, + normalize_pdf_bytes, + pdf_has_embedded_facturx, add_pdfa_output_intent, fix_xrechnung_xmp_conformance, compute_invoice_totals, @@ -99,21 +101,36 @@ def build_zugferd_pdf(invoice_json: str, original_pdf_bytes: bytes) -> bytes: Rechnung tatsaechlich aendert - so kann der Export-Button direkt herunterladen, ohne bei jedem Rerun neu zu generieren.""" invoice_dict = json.loads(invoice_json) - cii_xml_string = generate_compliant_zugferd_cii(invoice_dict) - # factur-x: XSD-Validierung der CII-XML, korrektes Einbetten und PDF/A-3-konforme XMP-Struktur. - pdf_bytes = facturx_generate_from_binary( - original_pdf_bytes, - cii_xml_string.encode("utf-8"), - flavor="factur-x", - level="en16931", - check_xsd=True, - check_schematron=False, # benötigt optional 'saxonche' für Schematron-Regeln - afrelationship="data", - ) - # Zusätzliches OutputIntent (sRGB-ICC) + XRechnung-Konformitaets-Fix in den XMP-Metadaten. - pdf_bytes = add_pdfa_output_intent(pdf_bytes) - pdf_bytes = fix_xrechnung_xmp_conformance(pdf_bytes) - return pdf_bytes + cii_bytes = generate_compliant_zugferd_cii(invoice_dict).encode("utf-8") + + def _attempt(src_pdf_bytes): + # factur-x: XSD-Validierung der CII-XML, Einbetten, PDF/A-3-konforme XMP-Struktur; danach + # OutputIntent (sRGB-ICC) und XRechnung-Konformitaets-Fix in den XMP-Metadaten. + pdf_bytes = facturx_generate_from_binary( + src_pdf_bytes, + cii_bytes, + flavor="factur-x", + level="en16931", + check_xsd=True, + check_schematron=False, # benötigt optional 'saxonche' für Schematron-Regeln + afrelationship="data", + ) + pdf_bytes = add_pdfa_output_intent(pdf_bytes) + pdf_bytes = fix_xrechnung_xmp_conformance(pdf_bytes) + # Erfolgskontrolle: factur-x laesst bei manchen PDF-Strukturen die XML still weg bzw. + # produziert eine kaputte PDF - dann als Fehlschlag behandeln. + if not pdf_has_embedded_facturx(pdf_bytes): + raise RuntimeError("factur-x.xml wurde nicht eingebettet") + return pdf_bytes + + try: + # 1. Versuch mit der Original-PDF (funktioniert bei den meisten PDFs direkt). + return _attempt(original_pdf_bytes) + except Exception: + # 2. Fallback: Ursprungs-PDF normalisieren (mit pikepdf neu speichern) und erneut versuchen. + # Manche PDFs (z.B. von PDF-XChange) brauchen das, andere funktionieren NUR ohne - daher + # beide Wege, und wir nehmen den, der eine gueltige eingebettete factur-x.xml liefert. + return _attempt(normalize_pdf_bytes(original_pdf_bytes)) # ==========================================