Skip to main content

M14 — PDF/A-3 and Factur-X

State: to do — Depends on: M12, M13 — PDF/A-3 on 1.7 output by ADR 40; the satellite by ADR 9

Goal​

Generate, from an HTML template and a typed invoice model, an electronic invoice that a French or German platform accepts — a PDF/A-3 document at level b, u or a, accessible under PDF/UA-1 when asked, carrying its Factur-X or ZUGFeRD XML as the specification of its profile and version requires — and read, validate and render the ones a business receives.

Receiving electronic invoices has been mandatory in France since 1 September 2026, and issuing them follows in 2027; Germany has required businesses to accept them since 2025. A Factur-X invoice is two documents in one file — a PDF a person reads and an XML a machine books — and the ways it goes wrong are specific: the XML attached under the wrong name or relationship, an XMP that declares one profile while the XML declares another, a visible total a cent away from the booked one, a PDF/A claim that veraPDF rejects, a MINIMUM profile sent where the law expects an invoice. This milestone makes those failures impossible when we write, and visible when we read — all but the received page's visible total, which needs text extraction (M15, below).

Scope​

In:

  • a public XMP model in the core: packets parsed tolerantly and bounded, every RDF form ISO 16684-1 allows, typed properties (simple, structures, arrays, language alternatives, qualifiers), untouched packets written back byte for byte, a deterministic serializer with padding, /Info kept in step in 1.7 output (M03), custom schemas, and the PDF/A extension schemas that describe them — replacing M03's internal reader and writer;
  • PDF/A-3b, 3u and 3a generation in the core (ADR 7, ADR 17): the conformance targets on PdfGenerationOptions (M08) and on M12's PdfRenderOptions.Conformance, the output intent with an embedded sRGB profile, the identification in XMP, and every constraint ISO 19005-2 and 19005-3 put on what is written — file structure, metadata, fonts, color, images, graphics state, transparency, annotations, actions, optional content, embedded files, implementation limits — enforced where the content is made, under M09's PdfConformancePolicy;
  • the dual claim PDF/A-3a and PDF/UA-1 on M13's structure, with the extension schema pdfuaid needs;
  • associated files under PDF/A-3: M06's attachments API held to part 3's rules — /AF, /AFRelationship, /F and /UF, a MIME type, dates the caller supplies — with attachments streamed;
  • the AdCodicem.Pdf.FacturX satellite:
    • embedding and extraction aware of every Factur-X and ZUGFeRD profile and version, kept as data: the attachment's name, its relationship, the fx: extension schema and its values, the guideline identifier;
    • validation of the XML against its schema, the EN 16931 business rules, the French (BR-FR-*) and German (BR-DE-*) national rules and each profile's restrictions — ported to C#, since the official Schematron needs XSLT 2.0, which .NET does not run — and of the container: the XMP against the XML, the name against the version, the relationship against the profile, the PDF/A claim;
    • a typed EN 16931 invoice model producing CII and UBL, composing an existing open-source model rather than writing one;
    • Factur-X generation from an HTML template and the model, so that the visible invoice and the XML come from one source, and embedding into a document that already claims PDF/A-3;
    • a readable, accessible PDF rendition of a received CII, UBL or XRechnung invoice, through M12;
  • the tool gains html2pdf --pdf-a, metadata, and facturx info|extract|validate|embed|render.

Out, explicitly:

  • PDF/A-2 levels and the conformance profiles that judge third-party documents — M20; M14 enforces what it writes and uses veraPDF as the referee;
  • PDF/A-4, 4f and 4e — M28. A Factur-X on a PDF/A-4f base is read and reported, not written (ADR 40 names Factur-X accepting PDF/A-4f as what would reopen it);
  • converting a received PDF into PDF/A-3 so that it can carry an XML — M21. M14 embeds only into a document that claims PDF/A-3, or PDF/A-2 when the caller explicitly asks for the claim to be raised (below);
  • signing an invoice (PAdES) — M26; embedding into a signed document follows M04's and M09's rules;
  • sending an invoice to a French plateforme agréée, Peppol, Chorus Pro or a German portal, and e-reporting — transport is not a PDF library's job, and not planned;
  • other CIUS and extensions — Peppol BIS Billing 3.0, national CIUS other than XRechnung — added as data when a caller needs one; not in the roadmap;
  • Order-X (purchase orders) — read and identified, never written; not in the roadmap;
  • UBL inside a Factur-X or ZUGFeRD hybrid — the specifications require CII; a UBL payload is reported, never written. UBL is produced as standalone XML only;
  • comparing a received PDF's visible invoice number, dates and totals with its XML — M15, as a slice in this satellite that runs the visible-consistency matcher below over M15's search (the maintainer's decision of 2026-09-27); M14 compares what it lays out itself;
  • writing object-level metadata (an image's or a page's XMP) — read by the model, never written; not planned;
  • legal archiving (NF Z42-013) and qualified time stamps — outside the library, and M26–M27 for time stamps.

Dependencies. M12 renders the invoice and the rendition; M13's structure makes level a and the dual claim possible. From earlier: M03's writer and version policy, M06's attachments and associated-files API and its merge rules for claims, M08's fonts and ToUnicode under a target, M09's PdfConformancePolicy, M11's annotation flags and layer rules, M04's signature coverage for embedding into a signed document.

Design​

Where it lives​

PartWhereWhy
The XMP model and extension schemasCore, Metadata/ (namespace AdCodicem.Pdf.Metadata)M03 already reads and writes XMP; M06 and M09 read claims from it; M18 writes custom schemas; System.Xml is part of the BCL, and AOT-safe as used here
PDF/A-3 targets, the output intent, the constraintsCore, beside PdfGenerationOptionsThe writer, the content builder and the font code enforce them; no dependency
The sRGB output-intent profileCore, an embedded resourceThe core alone must be able to write a PDF/A document
Associated files under PDF/A-3Core, M06's API
Factur-X, EN 16931 validation, the model, generation, the renditionAdCodicem.Pdf.FacturXXML schemas, rule sets, code lists and a third-party model are no business of the core's

The architecture table gives the satellite a dependency on AdCodicem.Pdf.Html for generation and renditions. A caller who only extracts or validates then ships Skia's native libraries. The satellite is laid out so that Container/ and Validation/ never reference Html/; slice 7 measures what the dependency costs a trimmed application, and the maintainer chooses between one package and two (.FacturX and .FacturX.Html). Either is a move of namespaces, not a rewrite.

The XMP model​

PdfXmpMetadata immutable: the parsed packet — schemas by namespace URI, properties in document order,
unknown content preserved; Parse(bytes, diagnostics), ToBytes(options)
XmpProperty a value: Simple (text), Struct (fields), Array (Bag, Seq, Alt), LangAlt (x-default and
languages), each with qualifiers
PdfXmpMetadata.Builder changes: Set, Remove, SetLangAlt, AddToArray — a new PdfXmpMetadata out
XmpNamespaces the known ones: dc, xmp, xmpMM, xmpRights, pdf, pdfaid, pdfuaid, pdfx, the pdfa*
extension namespaces, photoshop, tiff, exif — URIs, never prefixes, identify them
XmpExtensionSchema a schema's description for PDF/A: namespace, prefix, and each property's name, value
type, category and description; Read from pdfaExtension:schemas, Write into it
PdfDocumentMetadata the typed face on a document: Title, Authors, Subject, Keywords, Creator, Producer,
dates, Language, conformance claims, custom properties — kept in step with /Info
  • Reading. The metadata stream is decoded under the document's reader limits and parsed with XmlReader — DTDs prohibited, no resolver, no entity expansion — iteratively, with an explicit stack, so a deep packet costs memory in proportion to its size and never stack; no bound beyond the stream's own length guard is needed. Every form ISO 16684-1 allows is read: property elements and the attribute (abbreviated) form, rdf:parseType="Resource", rdf:value with qualifiers, several rdf:Descriptions for one subject. What the corpus shows besides is read and reported: a packet without x:xmpmeta (xmp.wrapper-missing), without the xpacket processing instructions, two rdf:RDF blocks in one packet (xmp.rdf-repeated), pre-2002 syntax, a legacy prefix on a current URI (read by URI), XML that is not well formed (xmp.malformed: the packet's bytes kept, nothing invented from them).
  • Writing. A packet the caller did not change is written back byte for byte — re-serializing a third party's metadata is a change nobody asked for. A changed packet is serialized deterministically: UTF-8 without a byte order mark, the xpacket header without the bytes and encoding attributes PDF/A forbids, namespaces in a fixed order (known ones first, then by URI), properties in their original order with new ones appended, 2 KB of padding by default so that a later incremental update can rewrite in place, and no identifier or date the caller or the document did not supply (invariant 6).
  • /Info: in 1.7 output the nine equivalents M03 keeps in step, and now custom /Info keys too, carried in the pdfx: namespace as Acrobat does; in 2.0 output XMP is authoritative (ADR 40). A difference found on reading is reported (xmp.info-mismatch), as the weclapp invoice's is.
  • Extension schemas. Under a PDF/A-1, 2 or 3 claim, every namespace used that is not among the part's predefined schemas is described in pdfaExtension:schemas — generated from the properties actually written, with the value types ISO 19005 allows and custom types (pdfaType) for structures. Reading them gives a caller the descriptions a third party wrote; a described property that does not match its description is reported (xmp.extension-schema-mismatch).
  • Custom schemas are first-class: a case number, a matter reference, a retention class (M18) is a property in the caller's namespace, typed, with its description generated for PDF/A.

PDF/A-3 generation​

PdfConformanceTarget gains PdfA3B, PdfA3U and PdfA3A, each combinable with M13's PdfUA1. The target is a promise the generator keeps as it writes, not a pass at the end:

AreaRequirement (ISO 19005-2, which part 3 inherits, and ISO 19005-3)Where it is enforcedWhen it cannot be met
FileA %PDF-1.n header (n at most 7) with a binary comment; /ID in the trailer; nothing after the last %%EOF but an end of line; no encryption (§6.1)M03's writer under the targetEncryption with the target: ArgumentException when the options are built
MetadataAn unfiltered metadata stream typed /Metadata /XML; pdfaid:part 3 and pdfaid:conformance B, U or A; /Info equal to its XMP equivalents; every non-predefined schema described (§6.6)The XMP model—
Output intentOne /GTS_PDFA1 output intent with an embedded ICC profile — our sRGB by default, the caller's otherwise; device color only in its family (§6.2.3, §6.2.4)The builder; M12's colorCMYK or gray device color, or a CMYK JPEG, with no matching profile or DefaultCMYK: a conflict
FontsEmbedded, legally embeddable, widths consistent with the program, no reference to .notdef (§6.2.11)M08 under the targetA face whose fsType forbids embedding: M08's fallback, reported
Unicode (levels u and a)Every glyph maps to Unicode, and not to U+0000, U+FEFF or U+FFFE (§6.2.11.7)M08's ToUnicodeA glyph with no mapping: a conflict
ImagesInterpolate false or absent; no /Alternates, no /OPI; JPEG 2000 with 1, 3 or 4 channels and a color specification the clause allows (§6.2.8)M12.5A JPEG 2000 passed through whose colr box breaks the rule: a conflict
Graphics stateNo TR; TR2 only /Default; no HTP; halftones of type 1 or 5; one of the four rendering intents (§6.2.5)M08's PdfGraphicsState—
TransparencyAllowed; the standard blend modes; a page group color space consistent with the output intent (§6.2.10)M08, M12—
XObjectsNo reference XObjects, no PostScript XObjects (§6.2.9)M08—
Annotations/F with Print set and Hidden, Invisible, NoView and ToggleNoView clear; an appearance for every annotation but Popup and Link (§6.3)M11, M12.6—
ActionsNo JavaScript, Launch, Sound, Movie, ResetForm, ImportData, Hide, SetOCGState, Rendition, Trans, GoTo3DView; named actions limited to page navigation; no /AA on the catalog or a page (§6.5)M12.6's linksA javascript: link: never written (ADR 37), reported
Optional contentEvery configuration named, no /AS (§6.9)M11's layersA print-only stamp: a conflict, as M11 rules — part 3 forbids the /AS its layer form needs, and every part the NoView flag its annotation form needs
Logical structure (level a)Tagged, /Marked true, standard or role-mapped types, language, alternative text (§6.7)M13M13's conflicts
Embedded filesAny format; each file specification with /F, /UF and /AFRelationship, reached from an /AF; its stream with a MIME /Subtype (ISO 19005-3 §6.8)The associated files APIAn attachment without a MIME type: a conflict
LimitsA string at most 32,767 bytes, a name at most 127, q nested at most 28 deep, at most 8,388,607 indirect objects, at most 32 DeviceN colorants (§6.1.13)The writer and the content builder countA limit passed: a conflict
  • Conflicts follow M09's PdfConformancePolicy: Refuse (the default) throws PdfConformanceException once every conflict of the render is collected, each named with its clause; RemoveClaim writes the document without pdfaid and reports pdfa.conformance-claim-removed (ConformanceLoss). A claim the library knows to be false is never written (invariant 7).
  • Version. PDF/A-3 is defined on ISO 32000-1: the target with 2.0 output is refused when the options are built, and any feature that would raise the version past 1.7 — a UTF-8 text string (M03's table) — is encoded the 1.7 way instead (UTF-16BE), or is a conflict.
  • The output intent profile is an sRGB ICC profile under a license that allows embedding unmodified — the ICC's own sRGB profile or a compact CC0 one; slice 3 chooses between them against veraPDF, pdf.js and MuPDF's color, and records it in NOTICE. Its condition identifier is written as the profile names it.
  • Raising a PDF/A-2 claim to PDF/A-3 is what embedding an XML into a PDF/A-2 document needs, since part 2 allows only PDF/A files as attachments and part 3 is part 2 plus any attachment. It is offered as an explicit option (RaiseClaimToPartThree), reported (pdfa.claim-raised), and never done in silence: the library cannot check the input's claim until M20, and says so.
  • Incremental updates of a PDF/A-3 document keep it: the update carries its own XMP when metadata changes, cross-reference streams are allowed in part 3, and M06's merge rules for claims apply to what is merged.

Associated files under PDF/A-3​

M06's PdfAttachments writes /AF and AFRelationship at document, page and annotation level. Under a PDF/A-3 target it also guarantees, or refuses:

  • one file specification object, referenced from both the EmbeddedFiles name tree and the /AF array — the factur-x Python library's sample in the corpus has two that disagree;
  • /F and /UF (UTF-16BE when not ASCII), /Desc, /AFRelationship from the caller (default Unspecified, which PDF/A-3 accepts and Factur-X does not — the satellite always sets it);
  • the embedded file stream's /Subtype as a MIME type written as a name, its / escaped (/text#2Fxml), /Params with /Size and /ModDate — the date the caller's, never the clock's; left out, and reported once, when the caller gives none. /CheckSum is MD5 by the specification, which browser WebAssembly does not provide (M23): it is optional, and written only on request;
  • the stream compressed on the fly behind an indirect /Length (M03), so an attachment of any size costs a buffer, not its size — and /Params an indirect dictionary written after the data, like /Length, since /Size is known only then.

Version. /AF and AFRelationship are PDF 2.0 keys that ISO 19005-3 brought into PDF 1.7: under a PDF/A-3 target they raise nothing, as M03's version table says. Outside PDF/A they are written as M06 decided.

The dual claim​

PDF/A-3a and PDF/UA-1 together need nothing M13 and the rows above do not already give, except one thing veraPDF checks: pdfuaid is not among PDF/A-3's predefined schemas, so its extension schema is written — as the PDFlib invoice in the corpus writes it. The two claims share the structure, the language, the title and the fonts; a conflict under either removes, under RemoveClaim, only the claim it breaks.

The Factur-X satellite​

AdCodicem.Pdf.FacturX
Profiles/ the flavors, versions and profiles as data: names, relationships, XMP schema, guideline IDs
Container/ reading and writing the hybrid: attachment, relationship, fx: XMP; FacturXReader, FacturXEmbedder
Validation/ schemas, syntax bindings, EN 16931 rules, code lists, profile and national rule sets, the report
Model/ the composed invoice model: profile and version selection, deterministic serialization
Html/ FacturXDocument (generation from HTML) and EInvoiceRenderer (the rendition), on AdCodicem.Pdf.Html

Profiles and versions, as data​

The table below is what the specifications said when this milestone was written; each row is verified against FeRD's and the FNFE-MPE's current publication before it is encoded, with the publication's version and date recorded beside it, as M18 does for court portals.

FlavorVersionsAttachmentXMP schemaProfiles (ConformanceLevel)
ZUGFeRD 11.0ZUGFeRD-invoice.xmlurn:ferd:pdfa:CrossIndustryDocument:invoice:1p0#BASIC, COMFORT, EXTENDED
ZUGFeRD 2.02.0, 2.0.1zugferd-invoice.xmlurn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED
Factur-X, and ZUGFeRD from 2.1Factur-X 1.0.x to 1.09.x; ZUGFeRD 2.1 to 2.5.xfactur-x.xml; xrechnung.xml for XRECHNUNGurn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG (ZUGFeRD), EXTENDED-CTC-FR (the French profile of XP Z12-012, in the versions that define it)
Order-X (read only)1.0order-x.xmlthe generic urn:factur-x:pdfa:CrossIndustryDocument:1p0#, DocumentType ORDERBASIC, COMFORT, EXTENDED

Each row also gives: the fx:DocumentType and fx:Version values, the guideline identifiers (BT-24) of each profile, the relationships the specification admits and the one it recommends — Data for MINIMUM and BASIC WL, Alternative or Source for the others, by country —, the PDF/A parts it admits (3 only, today), the XML namespaces and CII schema version, and whether the profile is a legal invoice in France and in Germany (MINIMUM and BASIC WL are not, in Germany; the French rule is verified with the reform's texts).

Reading a hybrid​

FacturXReader.Read(PdfDocument) returns a FacturXContainer, or nothing when the document carries no candidate:

  1. Find the attachment: the catalog's /AF first, then the EmbeddedFiles name tree, by the names of the table, case-insensitively, then by the name fx:DocumentFileName gives; a candidate reachable only from a page's annotation is reported, since the specifications require document level. Several candidates — the 4s4u sample carries a second XML as Supplement — are all listed, the invoice chosen by name and relationship.
  2. Identify it from what the file says, never by guessing: the XMP schema and its values; the XML's root namespace and element, read from the head of the stream without loading it (CII, ZUGFeRD 1's CrossIndustryDocument, UBL Invoice or CreditNote, Order-X); the guideline identifier. The version is stated as far as the file states it — flavor, fx:Version, guideline identifier, and elements only later versions define — and is Unknown beyond that: Factur-X 1.0.05 and 1.0.07 write the same XMP and the same guideline identifiers.
  3. Extract it streamed, from the revision in force — the intarsys XRECHNUNG sample attaches its XML by an incremental update —, decoded under the reader's limits: OpenXml() returns a stream of the exact bytes, and XmlSha256 their hash.
  4. Check the container, as findings with facturx.* identifiers: the XMP schema present and described, its level agreeing with the XML's guideline identifier, the name agreeing with the flavor, the relationship admitted for the profile, the PDF/A claim present and of an admitted part, a single /AF entry per file, the payload CII. What only veraPDF can say about the PDF/A claim is left to M20; what the reader sees for itself — no output intent, no /ID, a metadata stream without its type — is reported.

Validation​

EInvoiceValidator.Validate(xml, options) runs, in order, stopping only where the next step would be meaningless:

  1. Well-formedness, under the same hostile-input settings as XMP.
  2. Schema: the CII, ZUGFeRD 1 and UBL 2.1 XSDs, and each Factur-X profile's restricted XSD, embedded at pinned versions and applied with the BCL's XmlSchemaSet (XSD 1.0 suffices for them). Each schema's license is checked before it is embedded; one whose license forbids redistribution is fetched by the build from a pinned source, as the remote corpus is.
  3. Syntax binding rules of CEN/TS 16931-3-2 and 3-3 (UBL-SR-*, UBL-CR-*, CII-SR-*, CII-DT-*), on the XML.
  4. EN 16931 semantic rules — BR-*, BR-CO-*, BR-DEC-*, the VAT category rules (BR-S-*, BR-Z-*, BR-E-*, BR-AE-*, BR-K-*, BR-G-*, BR-O-*, and those of the Canary Islands' and Ceuta and Melilla's categories) and the code-list rules (BR-CL-*) — once, on a syntax-neutral semantic view of the invoice (its business terms and groups, each with its XML location), so that CII and UBL share one implementation.
  5. Profile rules: what each Factur-X and ZUGFeRD profile adds or restricts.
  6. National rules: BR-FR-* as the French reform's external specifications and XP Z12-012 state them, and BR-DE-* as KoSIT's XRechnung rules state them, each rule set pinned by version and applied when the caller asks, or when the guideline identifier says so (XRECHNUNG).
  7. Legal adequacy: a profile that is not a legal invoice in the caller's country, where the caller says the document is one (facturx.profile-not-an-invoice).

The report, EInvoiceValidationReport, holds findings of the form (RuleId, Flag, Location, BusinessTerm, Message): the official identifier verbatim (BR-CO-15, BR-FR-12, CII-SR-102) — it is what platforms, accountants and the official artifacts speak —, fatal or warning as the artifacts flag it, the XPath with the line and column, the business term (BT-112), and a message of ours. It says which version of each rule set and of each code list it applied. Code lists (ISO 4217, ISO 3166-1, ISO 6523, EAS, UNCL 1001, 4461, 5189, 5305, 7143, 7161, UN/ECE recommendations 20 and 21, VATEX, MIME) are generated at build time from the pinned CEN release, as M02's Arlington tables are (ADR 44).

Porting. Rules are ported from the rule statements that EN 16931-1, the syntax bindings, the French specifications and XRechnung publish, with the official Schematron as the referee, not the source: the CEN artifacts are published under the EUPL, whose terms are not attribution-only, and an ADR written in slice 8 records how each rule set's license was read and what that allows. XRechnung's rules are Apache-2.0. Every rule is tested by the differential harness — the same XML through our port and through the KoSIT validator running the official artifacts in a container — until the two fire exactly the same identifiers on every input.

The invoice model​

The model is composed, not written: ZUGFeRD-csharp (Stephan Stapel's s2industries.ZUGFeRD, under the Apache-2.0 license its repository states) is the candidate — an EN 16931 invoice descriptor that reads and writes CII for ZUGFeRD 1, 2.x and Factur-X, and XRechnung, with UBL output; what it covers today is checked against the criteria below, not assumed. Slice 10 adopts it in an ADR of its own, at the next free number, against those criteria, and composes rather than wraps it:

  • license and closure: Apache-2.0, its NOTICE carried; no transitive dependency beyond the BCL;
  • trimming and AOT: no warning from the analyzers, or the verbs that need it stay out of the AOT binary;
  • determinism: the same model gives the same bytes on every OS and culture — line endings, decimal separators, the XML declaration, namespace prefixes and their order are pinned by tests under fr-FR and on Linux and Windows;
  • coverage: CII for every profile and version in the table, UBL for EN 16931 and XRechnung, reading both; the business terms the French reform adds (the buyer's SIREN, the delivery address, the category of operation, the option for VAT on debits) where the French specifications place them. A gap is contributed upstream; a fork is a last resort the ADR must justify.

The public API takes and returns the composed model's InvoiceDescriptor directly: a wrapper would be a rewrite in disguise, and a second vocabulary for the same business terms. The satellite adds only what the model lacks — profile and version selection from the data table, validation before serialization, and the bridge to the rendition's view model. The ADR states the cost: the model's major versions become visible in ours.

Generating a Factur-X invoice​

FacturXDocument.Render(html, invoice, FacturXOptions, output)
FacturXOptions profile, version (the current one by default), PDF/A level and PDF/UA-1, relationship
(from the table by default), country rules, dates and document identifier from the caller,
PdfConformancePolicy, the XML (the model's by default, or the caller's own bytes)
  1. The XML is produced from the model — or taken as the caller's bytes, never re-serialized — and validated; a fatal finding refuses the render (FacturXValidationException, carrying the report) unless the caller insists, and then the report is returned beside the document.
  2. The HTML — which the caller renders from the same model with their own templates or an M12 PdfRenderTemplate — is laid out under the PDF/A-3 target (and PDF/UA-1 when asked).
  3. Visible consistency: M12 knows the text it laid out. The invoice number (BT-1), its date (BT-2), the total with VAT (BT-112) and the amount due (BT-115) are looked for in it, in the forms the document's language writes numbers and dates; one not found is reported (facturx.visible-value-not-found, warning, naming the business term). It never blocks: a template may legitimately format a value in a way the check does not know. The matcher — the business terms and the forms each language writes them in — takes its text from a seam, not from M12, so that M15 runs it unchanged over a received page's extracted text.
  4. The XML is attached through the associated files API with the table's name, the chosen relationship, the MIME type text/xml, a description, and the caller's date; the catalog's /AF lists it.
  5. The XMP gets pdfaid, the fx: properties and their extension schema exactly as the specification gives it, pdfuaid and its schema under the dual claim.

Embedding into an existing document (FacturXEmbedder.Embed) takes a PdfDocument that claims PDF/A-3 — or PDF/A-2 with RaiseClaimToPartThree — and the XML, and writes an incremental update by default, so that the original bytes are untouched; a signed input follows M04's classification (a certification refuses it, an approval signature is reported as a change after signing). A document without a PDF/A claim is refused: making it PDF/A is M21's.

Rendering a received invoice​

EInvoiceRenderer.Render(xml, options, output) turns a CII (any version the model reads), UBL or XRechnung XML into a readable document for approval or for a case file:

  • the XML is read into the model, and every business term it carries is shown, grouped as EN 16931 groups them (seller, buyer, payee, delivery, lines with their allowances and charges, VAT breakdown, totals, payment terms and means, notes, attachments referenced); an element the model does not know is listed in an annex, so that nothing in the XML is hidden by the rendition;
  • labels in French, German and English, written by us from the business terms' names; the language chosen by the caller, or the XML's;
  • the shipped template is semantic HTML, so the output is PDF/A-3a and PDF/UA-1 through M12 and M13; a caller may supply their own template over the same view model;
  • the original XML is attached byte for byte under its own name with AFRelationship Source;
  • a rendition is not an invoice: it carries no fx: schema, no Factur-X attachment name, and a visible statement, in the chosen language, that the attached XML is the invoice and this page its visualization; the validation report may be appended as a last page when the caller asks.

Hostile XML​

An XML from a received file is hostile (invariant 4): DTDs prohibited and no resolver — no entity expansion, no external fetch —; its size bounded by the reader's decoded-stream guard; parsing iterative; every count the rules iterate over (lines, allowances, VAT categories) bounded by the XML's own size; regular expressions only from the schemas we embed, never from the input; an XPath in a finding built from positions, never evaluated from input.

Diagnostics and findings​

In PdfDiagnosticCodes for the core, disjoint from validation rule identifiers (ADR 36):

CodeSeverityMeaning
xmp.malformed, xmp.wrapper-missing, xmp.rdf-repeatedWarningThe packet is not well formed, lacks its wrapper, or repeats rdf:RDF; what can be read is read
xmp.info-mismatchWarning/Info and XMP disagree on an equivalent property
xmp.extension-schema-mismatchWarningA described property does not match its description
pdfa.conflictWarningOne conflict of a PDF/A target, with its clause, collected before the policy applies
pdfa.conformance-claim-removedConformanceLossThe claim was not written, and why
pdfa.claim-raisedInformationA PDF/A-2 claim was raised to part 3 on the caller's request
pdfa.attachment-date-missingInformationAn attachment has no /ModDate, since the caller gave none

The satellite's container findings (facturx.*): facturx.attachment-missing, facturx.attachment-not-document-level, facturx.name-unexpected, facturx.relationship-unexpected, facturx.xmp-schema-missing, facturx.xmp-schema-undescribed, facturx.xmp-xml-level-mismatch, facturx.pdfa-claim-missing, facturx.pdfa-part-not-admitted, facturx.af-duplicate, facturx.payload-not-cii, facturx.not-an-invoice (Order-X), facturx.profile-not-an-invoice, facturx.xml-not-well-formed, facturx.visible-value-not-found. They share M02's identifier grammar, and live in the satellite's report until M20 makes the rule engine public (ADR 36).

Referees​

All in containers (ADR 27), their versions and the rule artifacts they run pinned by digest:

RefereeConfirms
veraPDF (--flavour 3b, 3u, 3a, ua1)Each PDF/A-3 level and PDF/UA-1, extension schemas included
Mustang (mustangproject's CLI, Apache-2.0, on a JRE)A hybrid as a Factur-X or ZUGFeRD invoice: XMP, attachment, profile, schema and Schematron
KoSIT validator (Apache-2.0), with KoSIT's XRechnung configuration and a scenario of ours applying the CEN EN 16931 artifacts to CII and UBLThe official Schematron's verdict, rule by rule — the differential harness's other side
factur-x (Akretion's Python library)An independent extraction and profile detection
qpdf and pikepdfAttachment bytes (--show-attachment) and listing; open_metadata()
ExifToolAn independent XMP parser
poppler (pdftotext, pdfinfo -struct-text)The rendition's text; the dual claim's structure

The command-line tool​

html2pdf --pdf-a 3b|3u|3a [--pdf-ua]; metadata FILE [--json] and metadata set FILE --ns URI --name N --value V, which writes the extension schema under a PDF/A claim; facturx info (flavor, profile, version, relationship, findings), facturx extract (the exact bytes), facturx validate (an XML or a PDF, the report as JSON), facturx embed, facturx render.

Slices​

Each slice ends on a green commit, with its codes documented and its measurements recorded in docs/status.md.

  1. The XMP read model. Delivers PdfXmpMetadata parsed from every form, the diagnostics, the byte-for-byte write of an untouched packet, and M03's internal reader replaced. Proved by unit tests per RDF form and per defect; an FsCheck property — any byte sequence parses to a model or to xmp.malformed, never an exception, in time linear in its length —; integration: on every committed document with a packet, the namespaces, names and values ExifTool and pikepdf read equal ours; every PDF/A document rewritten without a metadata change keeps its packet byte for byte. Leaves writing.
  2. XMP writing, /Info, custom and extension schemas. Delivers the builder, the deterministic serializer, custom /Info keys through pdfx:, XmpExtensionSchema read and generated, PdfDocumentMetadata, M03's writer replaced, and the metadata verb. Proved by unit tests; FsCheck (serialize then parse is the identity on the model); integration: a custom property added to each committed PDF/A document, then veraPDF upholds every claim it upheld (1b, 1a, 2b, 2a, 3b, 3u); ExifTool reads the property; the extension schemas of the ZUGFeRD release-candidate invoice, the 4s4u sample, PDFMaker 9's PDF/A-1b file and PDFlib's invoice are read as their producers described them. Leaves PDF/A.
  3. PDF/A-3b in the core. Delivers the targets, the output intent and the chosen sRGB profile, the file and metadata rules, the graphics-state, image, XObject, annotation, action and limit rules on PdfDocumentBuilder, the conflicts and the policy, and the 2.0 refusal. Proved by unit tests per row of the requirements table and per refusal; integration: veraPDF's 3b profile passes documents made with the builder — text in the OFL faces, JPEG and PNG images, links, transparency, a barcode — and each faulty option is refused with its clause. Leaves the HTML engine.
  4. PDF/A-3b and 3u through the HTML engine. Delivers the target in M12's options, color and image checks (CMYK JPEG, JPEG 2000 colr, 16-bit PNG), restricted @font-face faces, javascript: links, the level u check of every glyph. Proved by unit tests; integration: veraPDF's 3b and 3u profiles pass the reference documents; those documents enter the corpus and pass M01's, M02's and M03's acceptance in turn. Leaves attachments.
  5. Associated files under PDF/A-3. Delivers M06's API held to part 3's rules, one file specification shared by the name tree and /AF, streamed attachments, the version row, and RaiseClaimToPartThree. Proved by unit tests; integration: veraPDF's 3b profile passes the invoice with its source HTML (Source) and a CSV of its lines (Data), and a 100 MB attachment embedded with memory flat; qpdf --list-attachments shows each with its relationship; documents/archival/libreoffice-report-pdfa2b.pdf raised to part 3 with an attachment passes veraPDF's 3b profile; vendor/verapdf/pdfa3b-embedded-pass.pdf reads clean and the rule veraPDF reports on pdfa3b-embedded-fail.pdf, where it concerns the attachment, is one our checks name. Leaves level a.
  6. PDF/A-3a and the dual claim. Delivers level a on M13's structure and the pdfuaid extension schema. Proved by integration: veraPDF's 3a and ua1 profiles pass the reference documents under the dual claim; pdfinfo -struct-text prints the same tree as under PDF/UA-1 alone; the XMP's shape matches PDFlib's invoice's. Leaves the satellite.
  7. The satellite and reading hybrids. Delivers the package, the data table (each row verified and sourced), FacturXReader, identification, streamed extraction, the container findings, facturx info and extract, and the measurement of the .Html dependency for the packaging decision; the manifest's expect.facturx — flavor, profile, version (unknown where the file cannot tell), attachment, relationship and xmlSha256 — and its schema, written by build_corpus.py from Akretion's factur-x library, the XMP and qpdf --show-attachment, never from our reader. Proved by unit tests over the table and over hand-made containers; integration: for every hybrid in the corpus, the XML's SHA-256 equals that of the bytes qpdf --show-attachment writes, and the flavor and profile equal what Akretion's factur-x library and the XMP state. Leaves validation.
  8. Schemas and EN 16931 rules on CII. Delivers well-formedness and schema checks, the semantic view of a CII invoice, the syntax binding and EN 16931 rules, code lists generated at a pinned release, the report, the license ADR for the ported rule sets, and the differential harness. Proved by unit tests per rule (a passing and a failing instance each); integration: on every CII in the test set — the XMLs of the corpus's hybrids, KoSIT's test suite and the negative variants derived here, each standalone file a manifest entry of M07's xml format — our rule identifiers equal the KoSIT validator's, in both directions. Leaves UBL and the national rules.
  9. UBL, XRechnung, the French rules and the profiles. Delivers the UBL semantic view and bindings, BR-DE-*, BR-FR-*, each profile's rules, legal adequacy, and facturx validate. Proved by unit tests; integration: the differential harness on UBL and XRechnung inputs under KoSIT's XRechnung configuration, including the UBL payload of the iText 9 sample and the intarsys XRECHNUNG sample; the French rules against the official artifacts wherever the FNFE-MPE publishes them, and otherwise against worked examples recorded with their source. Leaves the model.
  10. The model. Delivers the model's ADR and the composition of ZUGFeRD-csharp: profile and version selection, CII and UBL serialization, the determinism settings, the gaps contributed upstream. Proved by unit tests under fr-FR and the invariant culture; an FsCheck generator of EN 16931-valid invoices whose CII and UBL both pass our validator and serialize identically twice; integration: the KoSIT validator accepts the generated set in CII and UBL under EN 16931, and XRechnung's configuration its XRECHNUNG members; the XML of every corpus hybrid read into the model and written again keeps every business term's value. Leaves generation.
  11. Generating and embedding Factur-X. Delivers FacturXDocument, the visible-consistency check, the XMP, FacturXEmbedder as an incremental update, facturx embed and html2pdf --facturx. Proved by unit tests; integration: Mustang validates an invoice we generate for each profile and each current version; veraPDF passes it at 3b, 3u, and at 3a with ua1; Akretion's library extracts XML byte-identical to what we embedded; the XML embedded into vendor/docentric/fop-factur-x-visualization.pdf leaves its original bytes untouched, keeps veraPDF's 3b verdict, and passes Mustang. Leaves the rendition.
  12. The rendition. Delivers the view model, the template and its labels, EInvoiceRenderer, the Source attachment, the statement, the optional report page, and facturx render. Proved by unit tests; integration: every XML of the corpus's hybrids renders to a document veraPDF passes at 3a and ua1, in which pdftotext finds every business term's value the XML carries, and whose attachment is the XML byte for byte; FacturXBenchmarks and PdfABenchmarks recorded; the documentation published.

Tests required​

Unit —

  • XMP: each RDF form; qualifiers; language alternatives with and without x-default; arrays of structures of arrays; a packet without wrapper, with two rdf:RDF, with a legacy prefix, not well formed; the byte-for-byte path; serializer determinism (namespace order, property order, padding); /Info in step in 1.7 and dropped in 2.0; custom keys through pdfx:; extension schemas generated for each value type and a custom type, read back, and a mismatch reported.
  • PDF/A-3: every row of the requirements table, its enforcement and its conflict; the policy (Refuse collects, RemoveClaim removes only the broken claim); the 2.0 refusal; UTF-8 strings encoded as UTF-16BE under the target; the limits counted (a 32,768-byte alternative text, q nested 29 deep, a name of 128 bytes).
  • Attachments: one file specification object for both references; MIME escaping; dates only from the caller; RaiseClaimToPartThree refused on a part-1 input (part 1 forbids attachments, and part 3 is not part 1 plus them).
  • Factur-X: each table row; identification of each flavor, of a UBL payload, of Order-X, of an unknown XML named factur-x.xml; the version Unknown where the file cannot tell; each container finding; extraction from the last revision.
  • Validation: each rule with a passing and a failing instance; the semantic view equal for the same invoice in CII and UBL; code lists at their pinned version; the report's locations; legal adequacy by country.
  • The model: each profile's serialization; culture and line-ending independence; the caller's XML bytes never re-serialized; validation before embedding.
  • The rendition: every business term shown; unknown elements listed; labels in each language; no fx: schema.
  • Hostile: an XML with a DTD, an external entity, a billion-laughs payload (refused before expansion); a 500 MB XML under the default limits (cut at the guard, limit.decoded-stream); a million invoice lines (bounded time, memory in proportion to the XML); an XMP packet nested 100,000 deep (iterative); an extension schema describing itself; attachment names with .., NUL and 10,000 characters.
  • Properties (FsCheck): XMP parse is total; serialize-then-parse is the identity; an invoice the generator builds validates in CII and UBL alike and serializes identically twice; the rule engine is total.

Integration — in containers (ADR 27): veraPDF on every PDF/A and PDF/UA document produced; Mustang on every Factur-X produced; the KoSIT validator on every XML produced and in the differential harness; Akretion's factur-x library, qpdf and pikepdf on every hybrid read or produced; ExifTool on every packet written; poppler on every rendition.

Acceptance conditions​

"The reference documents" are our renderings of sources/invoice-fr.html, report-fr.html and contract-fr.html (documents/*/adcodicem-*-fr.pdf, committed by M12 and tagged by M13), and of M13's accessibility reference. "The corpus's hybrids" are, committed: vendor/zugferd/mustang-zugferd2-en16931-invoice.pdf, vendor/zugferd/mustang-zugferd1-basic-invoice.pdf, vendor/zugferd/itext-pdfbox-weclapp-facturx-en16931-invoice.pdf, vendor/docentric/aspose-d365-facturx-extended-invoice.pdf, vendor/zugferd/gnuaccounting-mustang10-zugferd-rc-invoice.pdf, vendor/zugferd/pypdf2-facturx-python-false-pdfa3b.pdf; and remote: remote/zugferd-corpus/fnfe-factur-x-basicwl-fr.pdf, intarsys-zugferd2-en16931.pdf, intarsys-zugferd22-xrechnung-profile.pdf, symtrax-itextsharp-zugferd21-minimum-pdfa3a.pdf, itext9-facturx-ubl-payload.pdf, pypdf2-zugferd1-additional-data-cr-eol.pdf, konik-pdfbox-zugferd1-basic-from-word.pdf, and remote/mustang/dwc-weasyprint-facturx-extended-pdfa4f.pdf, dwc-weasyprint-facturx-extended-no-fx-xmp.pdf, facturx-python-order-x-comfort.pdf. Rows naming remote documents close only on a green Remote corpus run.

DocumentsBehaviorVerified by
The reference documents under PDF/A-3b, 3u and 3averaPDF reports no error at each levelPdfARefereeTests.Reference_documents_pass_pdf_a_3_at_every_level
The reference documents under PDF/A-3a with PDF/UA-1veraPDF reports no error under 3a and under ua1; the XMP carries pdfaid, pdfuaid and its extension schemaPdfARefereeTests.The_dual_claim_passes_both_profiles
Our PDF/A-3 outputs, committed to the corpusThey pass M01's, M02's and M03's acceptance, and M06 merging one with vendor/verapdf/pdfa3b-embedded-pass.pdf keeps the PDF/A-3b claim veraPDF upholdsCorpusPdfATests.Our_pdf_a_3_outputs_pass_earlier_acceptance
The invoice with its source HTML and a CSV of its lines attached; documents/archival/libreoffice-report-pdfa2b.pdf raised to part 3 with an attachmentveraPDF's 3b profile reports no error; qpdf --list-attachments lists each file with its relationship and MIME typePdfARefereeTests.Associated_files_keep_pdf_a_3
An invoice we generate from the model and invoice-fr.html for each profile — MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, and XRECHNUNG as ZUGFeRD — at each current version, in PDF/A-3b and in PDF/A-3a with PDF/UA-1Mustang reports it valid; veraPDF passes the claimed levels; Akretion's library extracts XML byte-identical to the XML we produced, and states the profile we choseFacturXRefereeTests.Generated_invoices_are_accepted
The corpus's hybridsThe XML's SHA-256 equals that of the bytes qpdf --show-attachment writes; flavor, profile, version (or Unknown), name and relationship equal those the manifest records for the documentCorpusFacturXTests.Hybrids_yield_their_xml_profile_and_version
The corpus's hybrids with a known defect: the release-candidate GnuAccounting invoice (XMP BASIC over a COMFORT XML), the DWC invoice without fx: XMP, the DWC PDF/A-4f invoice (factur-x.xml twice in /AF), the iText 9 invoice (a UBL payload), the Order-X order, the factur-x Python library's false PDF/A-3b (two file specifications, no output intent, no /ID, a metadata stream without its type), the intarsys XRECHNUNG invoice (its XML attached by an update, Source); and documents/invoice/qpdf-invoice-with-facturx-xml.pdf (a factur-x.xml whose ram: prefix is undeclared, no PDF/A claim, no /AF)Exactly the container findings each defect implies, and none on the sound hybrids — the Mustang, weclapp, Aspose, FNFE, intarsys EN 16931, Symtrax, 4s4u and Konik filesCorpusFacturXTests.Container_defects_are_reported_exactly
Every XML of the test set — the corpus's hybrids' XMLs, KoSIT's test suite in CII and UBL, and the negative variants derived hereOur validator fires exactly the rule identifiers the KoSIT validator fires with the official artifacts, no more and no fewerInvoiceRuleDifferentialTests.Our_rules_fire_exactly_where_the_official_artifacts_fire
Invoices built from the model for each profileThe KoSIT validator accepts their CII and their UBL under EN 16931, and XRechnung's configuration the XRECHNUNG onesFacturXRefereeTests.Model_invoices_pass_kosit_in_cii_and_ubl
The corpus's hybrids' XMLsRead into the model and written again, every business term keeps its value, or the term the model cannot hold is reportedCorpusFacturXTests.Corpus_xml_round_trips_through_the_model
vendor/docentric/fop-factur-x-visualization.pdf — a PDF/A-3b claim with no XML attachedOur XML embedded as an incremental update: the original bytes untouched, veraPDF's 3b verdict kept, Mustang reports the result validFacturXRefereeTests.Embedding_into_a_received_pdf_a_3_keeps_its_claim
The corpus's hybrids' XMLs, CII of every flavor, XRechnung, and the UBL payloadEach renders to a document veraPDF passes at 3a and ua1; pdftotext finds every business term's value the XML carries; the XML attached as Source byte for byte; no fx: schemaCorpusFacturXTests.Received_xml_renders_to_a_readable_accessible_pdf
Every committed document with an XMP packet, and the remote ones — attribute form, two rdf:RDF blocks, no x:xmpmeta, no xpacket, legacy xap prefix, pre-2002 syntax, a packet that is not well formed, a large packet with a thumbnail, XMP inside XFANamespaces, names and values equal ExifTool's where ExifTool reads the packet; the defects reported with their codes; the packet written back byte for byte when untouchedCorpusXmpTests.Every_packet_reads_as_exiftool_reads_it
Every committed PDF/A document, a custom property addedveraPDF upholds each claim it upheld beforePdfARefereeTests.Custom_metadata_keeps_every_pdf_a_claim
The ZUGFeRD release-candidate invoice, the 4s4u sample, PDFMaker 9's PDF/A-1b file, PDFlib's invoiceTheir extension schemas read as described: namespace, prefix, each property's type and categoryCorpusXmpTests.Extension_schemas_read_as_their_producers_wrote_them
M12's long report (not in the corpus, M12's need) at 1,000 pages under PDF/A-3a; an invoice with a 100 MB attachmentMemory flat as pages grow; the attachment streamed, memory independent of its sizeCorpusPdfATests.Pdf_a_generation_holds_its_budget, PdfABenchmarks
Every output aboveTwo runs give identical bytes, under the invariant culture and fr-FR, on Linux and on WindowsCorpusFacturXTests.Factur_x_output_is_deterministic
The same operations through the toolThe verbs produce the API's bytes and reportsCorpusToolTests.Facturx_and_metadata_verbs_match_the_api

Corpus​

What the corpus holds​

  • Hybrids from real producers: the six committed and ten remote files listed above — ZUGFeRD 1 from Mustang, Konik and 4s4u, the 2014 release-candidate namespace, ZUGFeRD 2.0 from Mustang and intarsys, Factur-X from weclapp through iText and PDFBox, from Dynamics 365 through Aspose, from the factur-x Python library through PyPDF2 and PyPDF4, from Symtrax through iTextSharp (a PDF/A-3a), from DWC through WeasyPrint (PDF/A-4f and PDF/A-3u), a UBL payload from iText 9, an XRECHNUNG XML attached by an update, an Order-X order — with profiles MINIMUM, BASIC WL, BASIC, EN 16931 and COMFORT, EXTENDED and XRECHNUNG, and relationships Data, Source, Alternative and Supplement (af-relationship-*); and our own derived stub, documents/invoice/qpdf-invoice-with-facturx-xml.pdf.
  • PDF/A-3 without an XML: vendor/docentric/fop-factur-x-visualization.pdf (Apache FOP, PDF/A-3b), and vendor/zugferd/fop26-xrechnung-visualization.pdf (an XRechnung visualization from the quba-viewer, through FOP 2.6, not PDF/A) — the nearest existing thing to our rendition.
  • PDF/A calibration: veraPDF's fixtures (vendor/verapdf/pdfa3b-embedded-pass.pdf, -fail.pdf, the 1b and 2b pairs, a PDF/A-4 pass), BFO's PDF/A-2b carrying a PDF/A file, the PDFlib PDF/A-2a and PDF/UA-1 invoice (the dual claim's XMP, xmp-pdfua-extension-schema), some twenty PDF/A-1a to 3u claims veraPDF upholds or rejects (conformanceValid), and our LibreOffice PDF/A-2b report.
  • XMP variety: seventy-odd packets (xmp-metadata) from Distiller, PDFMaker, InDesign, Illustrator, LibreOffice and OpenOffice.org, Word, Ghostscript, iText, PDFBox, WeasyPrint; the variants xmp-attribute-form, xmp-two-rdf-blocks, xmp-without-xmpmeta-wrapper, xmp-without-xpacket-wrapper, legacy-xmp-xap-namespace, pre-2002-xmp-syntax-in-old-revision, malformed-xmp, malformed-font-xmp, large-xmp-with-thumbnail, xmp-packet-inside-xfa, info-xmp-metadata-mismatch, and extension schemas (pdfa-extension-schema-in-xmp, xmp-two-extension-schemas, xmp-zugferd-rc-extension-schema).
  • Sources: sources/invoice-fr.html, the template of our generated invoices.

What it lacks​

NeedWhyPriorityLikely source
The Factur-X facts of each hybrid in the manifest — flavor, profile, version or Unknown, attachment name, relationship, the XML's SHA-256 — where today only the attachment's name is recordedThe acceptance compares with recorded expectations, which must come from the file and an independent tool, not from our reader1Generated here: build_corpus.py records them in expect.facturx, which slice 7 adds to the schema, from Akretion's factur-x library, the XMP and qpdf --show-attachment
Our own Factur-X invoices for each profile and current version, at PDF/A-3b and at 3a with PDF/UA-1, and our renditions, committed"What we produce must be as readable as what we consume": later milestones (M15, M19, M26) accept on them1Generated here, by this milestone, recorded in build_corpus.py
Standalone XML invoices — CII (D16B and later), UBL 2.1 invoices and credit notes, XRechnung 3 in both syntaxes, ZUGFeRD 1 — as inputs for validation and renditionThe differential harness and the UBL rendition need XML the corpus does not hold outside PDFs; they enter the manifest with M07's format: xml and its block — syntax, profile, KoSIT's verdict —, under the provenance rule every document follows1A public source (W16): KoSIT's XRechnung test suite (Apache-2.0), committed; the CEN examples and FeRD's and the FNFE-MPE's example packages, remote unless their terms are attribution-only
Negative XML variants, one per rule family — totals (BR-CO-*), VAT categories, code lists, decimals, BR-FR-*, BR-DE-*, each profile's restrictionA differential harness fed only valid invoices proves that both sides say nothing1Generated here: derived from committed XMLs (the Mustang, weclapp and Docentric hybrids' own, under Apache-2.0 and MIT) by recorded edits
Factur-X 1.08 and 1.09 (ZUGFeRD 2.4 and 2.5) samples, and an EXTENDED-CTC-FR sampleThe current versions, the profile the French reform adds, and the elements only they define; nothing in the corpus is newer than 2025's DWC files2A public source (W16): the FNFE-MPE's and FeRD's example packages, remote
A Factur-X that a supplier's ERP actually sent, anonymized, that we may commit (W07)Every real ERP hybrid found carried personal data or is remote; a committed one keeps the acceptance in the main CI job2A contribution
An XRechnung XML with KoSIT's own visualization of itThe rendition's coverage compared with the official visualization, field by field; fop26-xrechnung-visualization.pdf has no XML beside it2A public source (W16): KoSIT's visualization project's test material (Apache-2.0)
veraPDF's PDF/UA-1 verdicts in the manifest (M13's need)The dual claim's referee calibrated on PDFlib's invoice and the other claims2Generated here, with M13
A PDF/A-3a with PDF/UA-1 Factur-X from another producerThe dual claim is compared with PDFlib's A-2a invoice and Symtrax's A-3a, neither both at once3A contribution, or a vendor's sample in the remote corpus
A PDF/A-3 with non-XML associated files from another producer — a source office document, a CSVAssociated files other than invoices are checked on our output only3Generated here (LibreOffice's hybrid export, if it combines with PDF/A-3), or a contribution

Traps​

  • PDF/A-3 is a PDF 1.x format. A merge or a feature that raises the version to 2.0 — a UTF-8 text string, a 2.0-only key — silently voids the claim; under the target it is encoded the 1.7 way, or refused.
  • /AF is a PDF 2.0 key that part 3 brought into 1.7. Raising the version for it would break the very claim that requires it.
  • The output intent's subtype is /GTS_PDFA1 for every part. The 1 is not a part number.
  • A MIME type is a name: text/xml is written /text#2Fxml. Unescaped, the slash ends the name.
  • The XML is the invoice. Parsing and re-serializing it changes bytes a platform may hash; the caller's bytes are streamed as they are, and hashed before and after.
  • Two file specifications for one attachment — one in the name tree, another in /AF — make readers disagree about which is the invoice. One object, two references.
  • /ModDate from the clock makes two runs differ (invariant 6); from the caller or not at all.
  • The version of a received Factur-X is often unknowable: fx:Version has said "1.0" for every Factur-X release. Reporting a guess as a version misleads the caller who routes on it.
  • The fx: extension schema must be exact — value type Text, category external for each property — or veraPDF fails the PDF/A claim on a hybrid Mustang calls valid.
  • EN 16931 has a space in ConformanceLevel; ZUGFeRD 1's COMFORT is EN 16931's predecessor, not a synonym a comparison may treat as equal.
  • One cent. Line net amounts, allowances and totals round at two decimals at defined points (BR-CO-*, BR-DEC-*); a model that rounds once at the end books a total the page does not show — the weclapp sample's one-cent gap.
  • A decimal comma under fr-FR or de-DE in an amount, or CRLF from a Windows serializer, changes the XML; the tests change culture and run on both systems.
  • Code lists change twice a year. A stale list rejects valid invoices; a list newer than the rules breaks the differential. The report names the version it applied.
  • The official rule artifacts are not ours to copy. The CEN artifacts are EUPL-licensed; porting from the rule statements, with the Schematron as referee, keeps the licenses apart — and the ADR writes down how.
  • MINIMUM and BASIC WL are not invoices in Germany, and a French platform may refuse them: a hybrid can be valid Factur-X and not a valid invoice.
  • A rendition is not an invoice. A second PDF claiming Factur-X for someone else's XML is a second original; no fx: schema, no Factur-X name, a statement on the page.
  • A Factur-X on PDF/A-4f (the DWC sample) is valid PDF/A and, today, not a Factur-X base; ADR 40 says what would change that.
  • An XML attached by an incremental update exists only in the revision that added it; a reader of the first revision finds none.
  • Embedding into a signed invoice changes the file after signing: a certification forbids it (M04), and an approval signature will show it as a change.
  • PDF/A-1 forbids attachments and PDF/A-2 allows only PDF/A ones. Embedding an XML into either breaks the claim; only a part-2 claim can be raised to part 3, on request.
  • PDF/A's implementation limits: a string of 32,767 bytes, a name of 127, q nested 28 deep, 8,388,607 objects — a tagged ten-thousand-page report can approach the last.
  • The XMP packet is hostile, like everything else in a received file: a DTD in it is refused, never expanded.

Documentation​

  • docs/website/docs/concepts/pdf-a.md (new): the parts and levels, what the generator enforces and where, the policy, the dual claim, what is reported, and what M20 and M21 will add.
  • docs/website/docs/concepts/metadata.md (new): the XMP model, /Info in 1.7 and 2.0, custom schemas and extension schemas, the untouched-packet guarantee.
  • docs/website/docs/guides/custom-metadata.md (new): case numbers and matter references in XMP, under PDF/A.
  • docs/website/docs/guides/factur-x.md (new): generating an invoice from a template and the model, embedding into an existing PDF/A-3, reading a received hybrid, the container findings, the legal-invoice check.
  • docs/website/docs/guides/validating-e-invoices.md (new): the validation pipeline, the rule sets and their versions, the report, and the differential status of each rule set.
  • docs/website/docs/guides/rendering-received-e-invoices.md (new): the rendition, its template and its statement.
  • docs/website/docs/reference/factur-x-profiles.md (new): the data table, with each row's source and date.
  • docs/website/docs/reference/diagnostics.md: the xmp.* and pdfa.* codes; the satellite's findings.
  • docs/website/docs/reference/tool/: html2pdf --pdf-a, metadata, facturx.
  • docs/website/docs/introduction.md and docs/features/features.json: the facturx entry brought to its state.
  • docs/architecture.md: Metadata/ in the core, the satellite's layers and dependencies, the packaging decision of slice 7.
  • docs/adr/: the ADR on the composed model and the ADR on the ported rule sets' licenses.
  • docs/releasing.md: the new package and its API baseline (#42).
  • NOTICE: the sRGB profile's license, ZUGFeRD-csharp's Apache-2.0 notice, every embedded schema and code list with its source and terms.
  • docs/corpus.md: expect.facturx, the XML invoices under M07's xml format, and the new referees (Mustang, the KoSIT validator, Akretion's library, ExifTool).
  • docs/status.md: the measurements, the rule sets' versions and the differential's state.

Exit criteria​

  • The XMP model reads every form, writes untouched packets byte for byte and changed ones deterministically, and generates extension schemas; M03's internal XMP code is gone.
  • PDF/A-3b, 3u and 3a are generated from the builder and from HTML, every requirement enforced where the content is made, conflicts collected under PdfConformancePolicy.
  • The dual claim PDF/A-3a and PDF/UA-1 passes both veraPDF profiles.
  • Associated files keep PDF/A-3, streamed, with one file specification per attachment.
  • AdCodicem.Pdf.FacturX reads, validates, generates, embeds and renders, every profile and version row verified against its specification and sourced; the packaging decision on .Html is recorded.
  • The ported rule sets fire exactly where the official artifacts fire on the whole test set; the license ADR is written.
  • An ADR records the composed model; its determinism holds under every culture and on Linux and Windows.
  • #42 is fixed before the package ships, if M10 has not fixed it.
  • The priority-1 gaps above are filled; each remaining gap is recorded in docs/corpus-contributions.md.
  • The acceptance conditions above pass on the corpus, in CI, with no document skipped, and the remote rows on a green Remote corpus run recorded in status.md.
  • Unit tests cover each behavior, its degenerate cases and its hostile ones; the FsCheck properties hold.
  • Integration tests run veraPDF, Mustang, the KoSIT validator, Akretion's factur-x library, qpdf, pikepdf, ExifTool and poppler in containers.
  • PdfABenchmarks and FacturXBenchmarks measure generation, embedding, validation and rendition with MemoryDiagnoser; status.md records them.
  • The tool's verbs ship, documented, in the dotnet tool — and in the AOT binary where the model allows.
  • The documentation site publishes the pages listed above.
  • Every page of Documentation is written in its Diátaxis section, one mode per page (ADR 47).