A ZUGFeRD/Factur-X invoice is a "hybrid" file consisting of two parts:
- Human-Readable PDF: A visual representation of the invoice (specifically in the PDF/A-3 format) that can be opened and read by any standard PDF viewer.
- Machine-Readable XML: A structured data file embedded directly inside the PDF. This allows accounting software or ERP systems to extract and process invoice data automatically without manual entry.
Starting with version v4.1.0, PD4ML supports PDF/A-3b format and embedding XML invoices into PDF files.
The feature requires PD4ML DMS or PD4ML UA licenseEnabling PDF/A-3b output
The PDF file specification can be specified inpd4ml.writePDF() API call:
or you can use the predefined constants:pd4ml.writePDF(os, PdfSpec.PDF_1_7.combine(PdfSpec.PDFA_3B));
andpd4ml.writePDF(os, PdfSpec.ZUGFeRD);
which are technically synonymous, if your application is based on latest ZUGFeRD and Factur-X specifications .pd4ml.writePDF(os, PdfSpec.FacturX);
Defining an HTML template for embedding an XML invoice into a PDF file.
An invoice XML text can be placed as a boy of PD4ML's custom tag<pd4ml:attachment>:
PD4ML v4.1.0 introduces new attachment types for<pd4ml:attachment type="ZUGFeRD-extended" description="ZUGFeRD data" name="factur-x.xml"> <?xml version="1.0" encoding="utf-8"?> <rsm:CrossIndustryInvoice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ... ... </rsm:CrossIndustryInvoice> </pd4ml:attachment>
<pd4ml:attachment> tag:
with the optional modifier suffixes:
- ZUGFeRD
- Factur-X
-basic
-minimum
-basicwl
-extended
-comfort
The name attribute value of the <pd4ml:attachment> tag is optional, and its default value is factur-x.xml. You may need to change it only to ensure compatibility with older versions of ZUGFeRD.
The invoice's XML markup can be placed within the
- ZUGFeRD 1.0 relies on file name ZUGFeRD-invoice.xml
- ZUGFeRD 2.0 file name zugferd-invoice.xml
- ZUGFeRD 2.1 and Factur-X and newer: file name factur-x.xml
<pd4ml:attachment> tag, as shown above. Alternatively, it can be loaded from an external file:
<pd4ml:attachment type="Factur-X" description="Invoice data" src="invoices/factur-x.xml"/>
Validation of the PDF output
Although PD4ML ensures compliance with the PDF/A-3 standard, it cannot guarantee the correctness of the embedded ZUGFeRD/Factur-X document. Internally, PD4ML only parses provided XML invoices and rejects malformed XML documents. Actual compliance checking can be performed using third-party tools.Note: Adobe Acrobat's Preflight tool has a built-in ZUGFeRD validator. Unfortunately, it can't validate modern versions of ZUGFeRD and only works well with older specs.
ZUGFeRD/Factur-X validation with Mustang project
Usage of Mustang validator is quite simple
System.out.print("zugferd validation...");
ZUGFeRDValidator zfv = new ZUGFeRDValidator();
String report = zfv.validate(pdfPath);
if(!zfv.wasCompletelyValid()) {
System.out.println(report);
// Throw an exception?
}
System.out.println(" done.");
The validation result returns an XML string report with diagnostic data. If you don't care about validation warnings, you can simply execute zfv.wasCompletelyValid() and only throw an error if it's not true. A more pedantic approach would be to parse the XML report and iterate through the returned list of observations.
You can add Mustang tools to your project using the Maven dependency definition:
<dependency> <groupId>org.mustangproject</groupId> <artifactId>validator</artifactId> <version>2.22.0</version> <classifier>shaded</classifier> </dependency>
Application example
As it follows from the above, a real-life HTML template for ZUGFeRD PDF generation consists of two parts: HTML layout and content definition, responsible for visual human-readable information representation and machine-readable invisible XML attachment. In the HTML the attachment appears as XML content placed within the<pd4ml:attachment> tag.
Thus, the document contains a lot of duplicate data (customer info, shopping cart etc), which is repeated in both human-readable and machine-readable sections of the document. Also, the duplicate data is something that normally changes from invoice to invoice.
You can generate the entire HTML document template (including the <pd4ml:attachment> tag body) on the fly using your tools for later conversion into a valid ZUGFeRD PDF file with PD4ML. Or you can create a static HTML template and use PD4ML's dynamic parameters feature.
This feature assumes that you place placeholders in your HTML document in the format $[var] and provide dynamic data by passing a var/value hashmap to pd4ml.setDynamicData(map) during HTML to PDF conversion.
<html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>ZUGFeRD Invoice</title> ... </head> <body> ... <tr class="style1"> <td colspan="5">Zwischensumme</td> <td>$[curr] $[totalamt]</td> </tr> <tr> <td colspan="5">zzgl. MwSt. ($[vatprc]%)</td> <td>$[curr] $[vat]</td> </tr> <tr class="style1"> <td colspan="5"><strong>Gesamtsumme</strong></td> <td><strong>$[curr] $[grandtotal]</strong></td> </tr> ... <pd4ml:attachment type="ZUGFeRD" description="ZUGFeRD data"> ... <ram:SpecifiedTradeSettlementHeaderMonetarySummation> <ram:LineTotalAmount>$[totalamt]</ram:LineTotalAmount> <ram:TaxBasisTotalAmount>$[totalamt]</ram:TaxBasisTotalAmount> <ram:TaxTotalAmount currencyID="$[curr]">$[vat]</ram:TaxTotalAmount> <ram:GrandTotalAmount>$[grandtotal]</ram:GrandTotalAmount> <ram:DuePayableAmount>$[grandtotal]</ram:DuePayableAmount> </ram:SpecifiedTradeSettlementHeaderMonetarySummation> ... </pd4ml:attachment> </body></html>
Here you will find the complete HTML template
After applying the parameters below
...
HashMap<String, String> map = new HashMap<String, String>();
map.put("invid", "I202602101524");
map.put("product", "Product");
map.put("nettoamt", "10.00");
map.put("totalamt", "10.00");
map.put("vat", "1.90");
map.put("vatprc", "19");
map.put("grandtotal", "11.90");
map.put("curr", "EUR");
map.put("qty", "1");
map.put("seller", "Petra Mustefrau");
map.put("sellerstr", "Augsburger Str. 7");
map.put("sellercity", "Buchloe");
map.put("sellerzip", "86807");
map.put("sellercountry", "DE");
map.put("sellercountryname", "Deutschland");
map.put("sellervatid", "DE129524000");
map.put("selleremail", "mm@firma.net");
map.put("sellerphone", "+49 8241 9690-0");
map.put("customerid", "12345");
map.put("buyer", "Kunde");
map.put("buyerstr", "Heideweg 9");
map.put("buyercity", "Wolfratshausen");
map.put("buyerzip", "82515");
map.put("buyercountry", "DE");
map.put("buyercountryname", "Deutschland");
map.put("buyervatid", "DE129524000");
map.put("buyeremail", "kunde@gmx.de");
map.put("buyerphone", "+49 8171 42110");
map.put("buyercontact", "Max Mustermann");
map.put("invdatecode", "20260211");
map.put("duedatecode", "20260225");
map.put("invdate", "11.02.2026");
map.put("duedate", "25.02.2026");
pd4ml.setDynamicData(map);
pd4ml.readHTML(new ByteArrayInputStream(s.getBytes("UTF-8")), base, "UTF-8");
...
it results this valid ZUGFeRD PDF
