Technical Article

Factur-X and ZUGFeRD Hybrid Invoices in Delphi

A compliant electronic invoice is not a PDF with an XML file stapled on the side. It is a single PDF/A-3 document that carries the invoice twice: once as a page a human reads, and once as a machine-readable Cross Industry Invoice XML stored inside the file as an associated file. The two representations describe the same invoice. That dual nature is the whole point of the format families that European mandates now require, Factur-X in France and Germany, ZUGFeRD across German-speaking markets, and XRechnung for German public-sector billing. This article walks through how PDFlibPas assembles such a hybrid invoice in Delphi, where the standards leave room to get it wrong, and why one profile in the catalog needs a completely separate XML builder

What a hybrid invoice actually is

The visible page and the embedded XML serve different readers. A clerk approving a payment looks at the rendered page. An accounts-payable system ingests the XML, reads the totals and tax breakdown as structured fields, and books the entry without a human keying anything. The semantic content of that XML is governed by EN 16931, the European standard that defines the invoice data model: which fields exist, what they mean, and which are mandatory. EN 16931 is a semantic model, not a file format. Factur-X, ZUGFeRD 2.x, and XRechnung all realize that model as a UN/CEFACT Cross Industry Invoice document, the syntax that carries the EN 16931 fields on the wire

For the document to be both archivable and self-describing, the container is PDF/A-3, defined by ISO 19005-3. PDF/A-3 is the conformance level that permits arbitrary embedded files, which is exactly what an invoice XML needs to be. PDF/A-2 forbids embedding files that are not themselves PDF/A, so a Factur-X invoice cannot be PDF/A-2. The choice of PDF/A-3 is therefore not a preference, it is a requirement that follows directly from wanting to embed non-PDF data in an archival document

Why the relationship is Alternative

Embedding the bytes is the easy part. ISO 32000 §7.11.4 defines the embedded file stream, the object that holds the raw XML and its parameters. The part that makes the file a valid associated file is §14.13, which adds the concept of an associated file and the /AFRelationship key. That key states how the embedded data relates to the content it is attached to, and the value Factur-X mandates is Alternative

The choice matters because the other values would assert something false about the document. Source would mean the XML is the material from which the visible content was generated, a master that the page derives from. Supplement would mean the XML adds information beyond what the page shows, an extra not contained in the rendering. Neither is what a Factur-X invoice is. The XML and the page are two equivalent expressions of one invoice, carrying the same legal content in two forms. Alternative is the value that says exactly that: an equivalent alternative representation of the visible content. A validator that reads any other relationship on a Factur-X file will reject it, and rightly, because the relationship is a machine-readable claim about what the attachment is for

The profile catalog

The E-Invoice sample that ships with PDFlibPas drives the same generation path across six profiles, defined as an array of records in InvoiceModel.pas. Each profile carries the values the writer needs: a display name, the embedded file name, a conformance level, the /AFRelationship, a version, an optional country code, and the GuidelineID URN that the XML announces inside its document context

The six are Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED for France, XRechnung 3.0, ZUGFeRD 1.0 COMFORT, and ZUGFeRD 2.0 BASIC. The GuidelineID is the field that tells a receiver precisely which profile to expect, and the values are specific. Factur-X EN16931 announces urn:cen.eu:en16931:2017. XRechnung 3.0 announces urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0. ZUGFeRD 2.0 BASIC announces urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic. The embedded file name is part of the contract too. Factur-X profiles embed factur-x.xml, XRechnung embeds xrechnung.xml, and the ZUGFeRD profiles embed ZUGFeRD-invoice.xml or zugferd-invoice.xml. A receiver scans the attachment names to find the invoice, so the file name is not cosmetic

One detail in the catalog is worth reading carefully. Most profiles use the Alternative relationship, but the XRechnung 3.0 entry in the sample uses Source. The two formats answer to different validators and conventions, and the sample sets each profile's relationship from the catalog rather than hard-coding a single value, which is why the per-profile field exists rather than a constant

The ZUGFeRD 1.0 trap

It is tempting to assume every profile is the EN 16931 Cross Industry Invoice with minor variations in how many optional fields you populate. That holds for five of the six. It does not hold for ZUGFeRD 1.0 COMFORT, and the reason is structural rather than cosmetic

The modern profiles emit a UN/CEFACT Cross Industry Invoice with namespace version :100, whose root element is rsm:CrossIndustryInvoice. ZUGFeRD 1.0 predates that schema. It is the 2014 CrossIndustryDocument with namespace version :1p0, and its root element is rsm:CrossIndustryDocument. The namespace URNs differ, the root element differs, and the element tree differs throughout: the :1p0 schema groups data under ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery, and ApplicableSupplyChainTradeSettlement, where :100 uses ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery, and ApplicableHeaderTradeSettlement. The naming is similar enough to mislead and different enough to break

The word COMFORT in the profile name describes how rich the data is, an automation-grade profile with full line items, tax breakdown, and payment terms, not which schema carries it. So you cannot take a :100 document and relabel it for ZUGFeRD 1.0. The sample handles this with a flag on each profile record and two separate builder functions, selecting the right one before any XML is generated

function BuildInvoiceXMLText(const AProfile: TeInvoiceProfile;
  const Data: TInvoiceData): string;
begin
  // XMLFamily = 1 means the legacy ZUGFeRD 1.0 :1p0 schema; every
  // other profile is the modern UN/CEFACT :100 Cross Industry Invoice.
  if AProfile.XMLFamily = 1 then
    Result := BuildZUGFeRD1Text(AProfile, Data)
  else
    Result := BuildCII100Text(AProfile, Data);
end;

The split is not an implementation nicety. Feeding a :100 tree to a ZUGFeRD 1.0 receiver produces a document that fails schema validation at the root element, so the two families have to be built by code that knows which one it is writing

Selecting the PDF/A-3 level

PDF/A-3 has three conformance levels, and PDFlibPas selects them through SetPDFAMode. Mode 5 is PDF/A-3b, the level that guarantees reliable visual reproduction. Mode 6 is PDF/A-3a, which adds the tagged-structure and accessibility requirements of level a. Mode 7 is PDF/A-3u, which requires that all text be mapped to Unicode. Enabling the mode also embeds the library's built-in sRGB output intent, the colour characterisation that PDF/A demands so that rendered colour is defined rather than device-dependent

Most invoice flows run at 3b, which is sufficient for a faithful visible page plus the embedded XML. If you need an explicit ICC profile rather than the built-in one, LoadOutputIntentProfile swaps it in after the mode is set. The sample loads the repository sRGB profile this way and falls back to the built-in intent when the file is not reachable, so the output intent is always present

PDF := TPDFlib.Create;
try
  // Mode 5 = PDF/A-3b, 6 = PDF/A-3a, 7 = PDF/A-3u.
  if PDF.SetPDFAMode(5) <> 1 then
    raise Exception.Create('PDF/A-3 mode could not be enabled');

  // Optional: swap the built-in sRGB intent for an explicit ICC profile.
  if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
    { fall back to the built-in sRGB intent that SetPDFAMode embedded };
finally
  // ... continue building the document
end;

Building the hybrid invoice

With the container configured, the rest is three steps in order: set the PDF/A-3 mode, draw the human-readable page, then attach the XML as an associated file. The visible page is ordinary content. The one constraint worth remembering is that PDF/A forbids the non-embedded Standard 14 fonts, so the page must embed a real font face rather than reference a built-in one

The attachment is a single call. AddFacturXAssociatedFileFromString takes the raw UTF-8 XML bytes plus the profile metadata, writes the embedded file stream, registers it in the Catalog /AF array that PDF/A-3 requires, applies the /AFRelationship, and generates the XMP e-invoice metadata that identifies the document as Factur-X, ZUGFeRD, or XRechnung. It also checks that the XML's guideline ID matches the conformance level you asked for, so a mismatch between the XML you built and the profile you named is caught rather than silently shipped

// 1. PDF/A-3 mode and output intent are already set.
// 2. Draw the visible page (embeds a real TrueType font).
DrawInvoicePage(PDF, AProfile, Data);

// 3. Build the profile-correct XML and attach it as an
//    associated file with /AFRelationship = Alternative.
InvoiceXML := BuildInvoiceXML(AProfile, Data);   // AnsiString of UTF-8 bytes
FileID := PDF.AddFacturXAssociatedFileFromString(
  InvoiceXML,
  AProfile.ConformanceLevel,   // e.g. 'EN16931'
  AProfile.FileName,           // 'factur-x.xml'
  AProfile.Description,
  AProfile.Relationship,       // 'Alternative'
  AProfile.Version,            // '1.0'
  AProfile.CountryCode);       // '' or 'DE' or 'FR'
if FileID <= 0 then
  raise Exception.Create('Invoice XML could not be attached');

PDF.SaveToFile(TargetFile);

One subtlety in the data path is the encoding. The embedded XML declares encoding="UTF-8", and the method takes its bytes as an AnsiString, so a non-ASCII seller or buyer name must reach the call as raw UTF-8 octets. A plain cast through the system ANSI code page would corrupt those characters and quietly produce an invoice whose XML no longer matches its own declaration. The sample encodes to UTF-8 explicitly before handing the bytes over, which is the safe way to feed any byte-oriented PDF API from a Unicode string

For attaching XML that is not a recognised e-invoice profile, AddPDFA3AssociatedFileFromString is the generic counterpart. It takes a file name, MIME type, description, relationship, and bytes, and writes a plain PDF/A-3 associated file without any invoice-specific metadata or guideline checks. Use it for supplementary data; use the Factur-X method for invoices, so the profile metadata and the guideline match are written for you

Once the document is produced, the next questions are whether it passes PDF/A and accessibility validation, and whether it can be signed without breaking compliance. Those are covered in the PDF/A and PDF/UA preflight walkthrough and the compliance and signing workbench. All of this ships as part of the PDFlibPas Delphi PDF Library, alongside the PDF/A, tagging, and document-property APIs that the e-invoice path builds on