fix(zugferd): robust factur-x embedding for varied PDF structures
Fehler 'unable to find page tree' beim ZUGFeRD-Export mancher PDFs (z.B. von PDF-XChange erzeugte degewo-Rechnung): factur-x bettete die XML nicht ein und zerstoerte den Seitenbaum. - build_zugferd_pdf versucht zuerst die Original-PDF; schlaegt das fehl (Absturz ODER XML nicht eingebettet), wird die PDF mit pikepdf normalisiert (neu gespeichert) und erneut versucht. Manche PDFs funktionieren nur original, andere nur normalisiert - beide Wege abgedeckt, Erfolgskontrolle via pdf_has_embedded_facturx. - Neue Helfer normalize_pdf_bytes() und pdf_has_embedded_facturx() in invoice_logic. - Verifiziert auf beiden echten Rechnungen (degewo -> Fallback, Gemeinde -> Original). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
29034d8501
commit
df81c011d3
|
|
@ -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")
|
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:
|
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.
|
Fügt der PDF ein OutputIntent mit sRGB-ICC-Profil hinzu, falls noch keines vorhanden ist.
|
||||||
|
|
|
||||||
47
main.py
47
main.py
|
|
@ -22,6 +22,8 @@ from invoice_logic import (
|
||||||
format_cii_date,
|
format_cii_date,
|
||||||
generate_compliant_xrechnung,
|
generate_compliant_xrechnung,
|
||||||
generate_compliant_zugferd_cii,
|
generate_compliant_zugferd_cii,
|
||||||
|
normalize_pdf_bytes,
|
||||||
|
pdf_has_embedded_facturx,
|
||||||
add_pdfa_output_intent,
|
add_pdfa_output_intent,
|
||||||
fix_xrechnung_xmp_conformance,
|
fix_xrechnung_xmp_conformance,
|
||||||
compute_invoice_totals,
|
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
|
Rechnung tatsaechlich aendert - so kann der Export-Button direkt herunterladen, ohne bei
|
||||||
jedem Rerun neu zu generieren."""
|
jedem Rerun neu zu generieren."""
|
||||||
invoice_dict = json.loads(invoice_json)
|
invoice_dict = json.loads(invoice_json)
|
||||||
cii_xml_string = generate_compliant_zugferd_cii(invoice_dict)
|
cii_bytes = generate_compliant_zugferd_cii(invoice_dict).encode("utf-8")
|
||||||
# factur-x: XSD-Validierung der CII-XML, korrektes Einbetten und PDF/A-3-konforme XMP-Struktur.
|
|
||||||
pdf_bytes = facturx_generate_from_binary(
|
def _attempt(src_pdf_bytes):
|
||||||
original_pdf_bytes,
|
# factur-x: XSD-Validierung der CII-XML, Einbetten, PDF/A-3-konforme XMP-Struktur; danach
|
||||||
cii_xml_string.encode("utf-8"),
|
# OutputIntent (sRGB-ICC) und XRechnung-Konformitaets-Fix in den XMP-Metadaten.
|
||||||
flavor="factur-x",
|
pdf_bytes = facturx_generate_from_binary(
|
||||||
level="en16931",
|
src_pdf_bytes,
|
||||||
check_xsd=True,
|
cii_bytes,
|
||||||
check_schematron=False, # benötigt optional 'saxonche' für Schematron-Regeln
|
flavor="factur-x",
|
||||||
afrelationship="data",
|
level="en16931",
|
||||||
)
|
check_xsd=True,
|
||||||
# Zusätzliches OutputIntent (sRGB-ICC) + XRechnung-Konformitaets-Fix in den XMP-Metadaten.
|
check_schematron=False, # benötigt optional 'saxonche' für Schematron-Regeln
|
||||||
pdf_bytes = add_pdfa_output_intent(pdf_bytes)
|
afrelationship="data",
|
||||||
pdf_bytes = fix_xrechnung_xmp_conformance(pdf_bytes)
|
)
|
||||||
return pdf_bytes
|
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))
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue