Repository navigation
Description
Summary
Since 8.34.0 (#1552), Gotenberg can turn a PDF into a Factur-X / ZUGFeRD invoice (facturxXml, facturxConformanceLevel, facturxVersion), and XRECHNUNG is an accepted value for facturxConformanceLevel. Regardless of the level, the XML is always embedded as factur-x.xml and the XMP says <fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>. The docs say so too: "Embedded as factur-x.xml regardless of the uploaded filename."
For the XRECHNUNG reference profile, the Factur-X / ZUGFeRD specification requires the embedded file and fx:DocumentFileName to be xrechnung.xml, and explicitly forbids factur-x.xml in that profile. So the output for facturxConformanceLevel=XRECHNUNG does not follow the spec. All other levels are fine.
Fix idea: choose the file name from the conformance level: xrechnung.xml for XRECHNUNG, factur-x.xml for everything else.
Steps to reproduce
Gotenberg 8.38.1. invoice.xml is any CII invoice following the XRechnung CIUS.
docker run --rm -p 3000:3000 gotenberg/gotenberg:8.38.1
curl --request POST http://localhost:3000/forms/pdfengines/factur-x \
--form [email protected] \
--form [email protected] \
--form 'facturxConformanceLevel=XRECHNUNG' \
-o out.pdf
pdfdetach -list out.pdf
pdfinfo -meta out.pdf | grep -E 'fx:(DocumentFileName|ConformanceLevel)'The same code path is used by /forms/pdfengines/embed, merge, split, /forms/libreoffice/convert and the Chromium convert routes.
Expected
1 embedded files
1: xrechnung.xml
<fx:DocumentFileName>xrechnung.xml</fx:DocumentFileName>
<fx:ConformanceLevel>XRECHNUNG</fx:ConformanceLevel>
Actual (derived from the code paths linked below)
1 embedded files
1: factur-x.xml
<fx:DocumentFileName>factur-x.xml</fx:DocumentFileName>
<fx:ConformanceLevel>XRECHNUNG</fx:ConformanceLevel>
There is no clean workaround inside Gotenberg:
- The generic
embedsfield keeps the file name but writes nofxXMP. Combined with the Factur-X fields it produces two invoice XMLs, and §7.7 forbids afactur-x.xmlin that profile. injectFacturXIntoXMPdoes nothing when thefxnamespace is already present, so a second pass cannot correct the XMP./forms/pdfengines/metadata/write(ExifTool) usually breaks PDF/A (per the docs), and ExifTool does not know thefxnamespace without a custom config.
Specification references
Factur-X 1.07.2 / ZUGFeRD 2.3.2 (FNFE-MPE & FeRD, 15 Nov 2024), German edition (package: https://www.ferd-net.de/standards/paket-downloads/index.html):
-
§6.2 "Die Einbettung der XML-Datei", p. 27:
Die XML-Datei wird immer unter dem Namen "factur-x.xml" eingebettet. Die einzige Ausnahme bilden Referenzprofile wie das Profil XRECHNUNG; hier muss der Name „xrechnung.xml" lauten.
(The XML file is always embedded as factur-x.xml. The only exception are reference profiles such as XRECHNUNG; there the name must be xrechnung.xml.)
-
§7.7 "Das Referenzprofil XRECHNUNG", p. 68:
Der Name der xml-Komponente von Factur-X / ZUGFeRD muss immer xrechnung.xml lauten, sie darf nicht factur-x.xml sein. Konsequenterweise darf es in einem XRECHNUNG-Profil Factur-X (ZUGFeRD) auch keine Einbettung einer factur-x.xml-Datei geben.
(The name must always be xrechnung.xml and must not be factur-x.xml. Consequently an XRECHNUNG-profile invoice must not embed a factur-x.xml at all.)
-
§7.7 XMP table, p. 70:
fx:DocumentFileName"… muss mit dem Wert des Eintrags /F im Dateispezifikationswörterbuch identisch sein. Dies ist ein fester Wert im Profil XRECHNUNG: xrechnung.xml" -
Per the spec changelog, §7.7 was added in 1.0.06 (2022-03-01, = ZUGFeRD 2.2).
-
Factur-X 1.09.2 / ZUGFeRD 2.5.2, as summarised in phax/kaltblut docs/2.5.2.md: "
factur-x.xml;xrechnung.xmlfor the XRECHNUNG reference profile", with BR-HYBRID-13 (fatal: embedded file name SHALL be from the HybridDocumentFilename list) and BR-HYBRID-14 (warning: embedded file name SHOULD matchfx:DocumentFileName). -
ZUGFeRD FAQ (https://zugferd.org/e-invoicing/1.0.0/faq.en.html): for ZF2 the embedded file is named "zugferd-invoice.xml, factur-x.xml or xrechnung.xml".
Other implementations:
- Mustang:
ZUGFeRDExporterFromA3.getFilenameForVersionreturns"xrechnung.xml"for profileXRECHNUNGand uses it for both the attachment andfx:DocumentFileName(L572-L587). - Telerik RadPdfProcessing: "When using the XRechnung conformance level, the embedded file name is fixed to xrechnung.xml" (docs).
- Same bug fixed elsewhere: microsoft/ALAppExtensions#29716, TeXhackse/LaTeX-ZUGFeRD#26.
Note: Mustang's PDFValidator accepts factur-x.xml for any level, so the current output may pass Mustang. The problem shows up with receivers that look for xrechnung.xml when the level is XRECHNUNG, and with validators implementing the 1.09 BR-HYBRID rules.
Where it happens (v8.38.1, e4ee0f2)
- Hard-coded constant:
pkg/gotenberg/pdfengine.go#L239-L241(FacturXDocumentFileName = "factur-x.xml"); field doc at L253-L255. - Level validated (XRECHNUNG accepted, L586) and constant assigned regardless of level (L613) in
pkg/modules/pdfengines/routes.go#L567-L616. - Embedding uses the constant rather than
facturX.DocumentFileName:routes.go#L721-L744(canonicalPath, L730), while the AF metadata is keyed byfacturX.DocumentFileName(L702-L707). They only match because both arefactur-x.xmltoday. - XMP:
pkg/modules/qpdf/qpdf.go#L1064-L1078writesfx:DocumentFileNamefrom the struct; the extension schema (L1082-L1129) needs no change.validateFacturXonly checks that the name is non-empty. - Tests pinning current behaviour:
pkg/modules/qpdf/facturx_test.go,test/integration/features/libreoffice_convert.feature#L764-L831.
Proposed fix
Derive the name from the level in one place and use facturX.DocumentFileName everywhere downstream.
--- a/pkg/gotenberg/pdfengine.go
+++ b/pkg/gotenberg/pdfengine.go
@@
FacturXDocumentFileName string = "factur-x.xml"
+
+ // XRechnungDocumentFileName is the name mandated for the XRECHNUNG
+ // reference profile (Factur-X 1.07.2 / ZUGFeRD 2.3.2, §6.2 and §7.7).
+ XRechnungDocumentFileName string = "xrechnung.xml"
)
+
+// FacturXDocumentFileNameFor returns the name of the embedded XML invoice for
+// the given conformance level.
+func FacturXDocumentFileNameFor(conformanceLevel string) string {
+ if conformanceLevel == FacturXConformanceXRechnung {
+ return XRechnungDocumentFileName
+ }
+ return FacturXDocumentFileName
+}--- a/pkg/modules/pdfengines/routes.go
+++ b/pkg/modules/pdfengines/routes.go
@@ func FormDataPdfFacturX(form *api.FormData) (gotenberg.FacturX, string) {
- DocumentFileName: gotenberg.FacturXDocumentFileName,
+ DocumentFileName: gotenberg.FacturXDocumentFileNameFor(conformanceLevel),
@@
- err := embedFacturXXml(ctx, engine, facturxXmlPath, inputPaths)
+ err := embedFacturXXml(ctx, engine, facturX.DocumentFileName, facturxXmlPath, inputPaths)
@@
-func embedFacturXXml(ctx *api.Context, engine gotenberg.PdfEngine, facturxXmlPath string, inputPaths []string) error {
+func embedFacturXXml(ctx *api.Context, engine gotenberg.PdfEngine, fileName, facturxXmlPath string, inputPaths []string) error {
@@
- canonicalPath := fmt.Sprintf("%s/%s", embedDir, gotenberg.FacturXDocumentFileName)
+ canonicalPath := fmt.Sprintf("%s/%s", embedDir, fileName)Tests: a table test for FacturXDocumentFileNameFor (all six levels), an injectFacturXIntoXMP case asserting <fx:DocumentFileName>xrechnung.xml</fx:DocumentFileName> for XRECHNUNG, and an integration scenario on /forms/pdfengines/factur-x with facturxConformanceLevel=XRECHNUNG checking PDF/A-3b validity and that xrechnung.xml is embedded with relationship Alternative.
Docs: "Embedded as factur-x.xml (xrechnung.xml for XRECHNUNG), regardless of the uploaded filename." on the Factur-X and Attachments pages.
Side note
facturxVersion defaults to 1.0. The §7.7 table in 1.07.2 shows fx:Version example 2p1 for XRECHNUNG ("always use the currently valid version number"), while the 1.09.2 summary says 1.0 for all profiles. Callers can already set facturxVersion, so maybe a docs sentence is enough.
Environment
- Gotenberg 8.38.1 (commit
e4ee0f29), unchanged onmainat764c7f2c - PDF engine: QPDF (default)
Related: #1552
Activity
gulien commentedon Oct 26, 2026
Fixed in 03963a9: the XRECHNUNG level now embeds
xrechnung.xmland writes it tofx:DocumentFileName. Thanks for the thorough report!nhh commentedon Oct 26, 2026
@gulien Thanks for the immediate implementation!
feat(image): update image gotenberg/gotenberg (8.38.1 → 8.39.0) (#1021)
feat(container): update image docker.io/gotenberg/gotenberg (8.38.1 ➔…