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,
/Infokept 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'sPdfRenderOptions.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'sPdfConformancePolicy; - the dual claim PDF/A-3a and PDF/UA-1 on M13's structure, with the extension schema
pdfuaidneeds; - associated files under PDF/A-3: M06's attachments API held to part 3's rules —
/AF,/AFRelationship,/Fand/UF, a MIME type, dates the caller supplies — with attachments streamed; - the
AdCodicem.Pdf.FacturXsatellite:- 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;
- embedding and extraction aware of every Factur-X and ZUGFeRD profile and version, kept as data: the
attachment's name, its relationship, the
- the tool gains
html2pdf --pdf-a,metadata, andfacturx 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
| Part | Where | Why |
|---|---|---|
| The XMP model and extension schemas | Core, 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 constraints | Core, beside PdfGenerationOptions | The writer, the content builder and the font code enforce them; no dependency |
| The sRGB output-intent profile | Core, an embedded resource | The core alone must be able to write a PDF/A document |
| Associated files under PDF/A-3 | Core, M06's API | |
| Factur-X, EN 16931 validation, the model, generation, the rendition | AdCodicem.Pdf.FacturX | XML 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:valuewith qualifiers, severalrdf:Descriptions for one subject. What the corpus shows besides is read and reported: a packet withoutx:xmpmeta(xmp.wrapper-missing), without thexpacketprocessing instructions, twordf:RDFblocks 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
xpacketheader without thebytesandencodingattributes 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/Infokeys too, carried in thepdfx: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:
| Area | Requirement (ISO 19005-2, which part 3 inherits, and ISO 19005-3) | Where it is enforced | When it cannot be met |
|---|---|---|---|
| File | A %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 target | Encryption with the target: ArgumentException when the options are built |
| Metadata | An 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 intent | One /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 color | CMYK or gray device color, or a CMYK JPEG, with no matching profile or DefaultCMYK: a conflict |
| Fonts | Embedded, legally embeddable, widths consistent with the program, no reference to .notdef (§6.2.11) | M08 under the target | A 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 ToUnicode | A glyph with no mapping: a conflict |
| Images | Interpolate 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.5 | A JPEG 2000 passed through whose colr box breaks the rule: a conflict |
| Graphics state | No TR; TR2 only /Default; no HTP; halftones of type 1 or 5; one of the four rendering intents (§6.2.5) | M08's PdfGraphicsState | — |
| Transparency | Allowed; the standard blend modes; a page group color space consistent with the output intent (§6.2.10) | M08, M12 | — |
| XObjects | No 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 | — |
| Actions | No 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 links | A javascript: link: never written (ADR 37), reported |
| Optional content | Every configuration named, no /AS (§6.9) | M11's layers | A 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) | M13 | M13's conflicts |
| Embedded files | Any 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 API | An attachment without a MIME type: a conflict |
| Limits | A 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 count | A limit passed: a conflict |
- Conflicts follow M09's
PdfConformancePolicy:Refuse(the default) throwsPdfConformanceExceptiononce every conflict of the render is collected, each named with its clause;RemoveClaimwrites the document withoutpdfaidand reportspdfa.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
EmbeddedFilesname tree and the/AFarray — the factur-x Python library's sample in the corpus has two that disagree; /Fand/UF(UTF-16BE when not ASCII),/Desc,/AFRelationshipfrom the caller (defaultUnspecified, which PDF/A-3 accepts and Factur-X does not — the satellite always sets it);- the embedded file stream's
/Subtypeas a MIME type written as a name, its/escaped (/text#2Fxml),/Paramswith/Sizeand/ModDate— the date the caller's, never the clock's; left out, and reported once, when the caller gives none./CheckSumis 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/Paramsan indirect dictionary written after the data, like/Length, since/Sizeis 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.
| Flavor | Versions | Attachment | XMP schema | Profiles (ConformanceLevel) |
|---|---|---|---|---|
| ZUGFeRD 1 | 1.0 | ZUGFeRD-invoice.xml | urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0# | BASIC, COMFORT, EXTENDED |
| ZUGFeRD 2.0 | 2.0, 2.0.1 | zugferd-invoice.xml | urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0# | MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED |
| Factur-X, and ZUGFeRD from 2.1 | Factur-X 1.0.x to 1.09.x; ZUGFeRD 2.1 to 2.5.x | factur-x.xml; xrechnung.xml for XRECHNUNG | urn: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.0 | order-x.xml | the generic urn:factur-x:pdfa:CrossIndustryDocument:1p0#, DocumentType ORDER | BASIC, 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:
- Find the attachment: the catalog's
/AFfirst, then theEmbeddedFilesname tree, by the names of the table, case-insensitively, then by the namefx:DocumentFileNamegives; 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 asSupplement— are all listed, the invoice chosen by name and relationship. - 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, UBLInvoiceorCreditNote, 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 isUnknownbeyond that: Factur-X 1.0.05 and 1.0.07 write the same XMP and the same guideline identifiers. - 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, andXmlSha256their hash. - 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/AFentry 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:
- Well-formedness, under the same hostile-input settings as XMP.
- 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. - Syntax binding rules of CEN/TS 16931-3-2 and 3-3 (
UBL-SR-*,UBL-CR-*,CII-SR-*,CII-DT-*), on the XML. - 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. - Profile rules: what each Factur-X and ZUGFeRD profile adds or restricts.
- National rules:
BR-FR-*as the French reform's external specifications and XP Z12-012 state them, andBR-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). - 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
NOTICEcarried; 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-FRand 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)
- 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. - 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). - 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. - 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/AFlists it. - The XMP gets
pdfaid, thefx:properties and their extension schema exactly as the specification gives it,pdfuaidand 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
AFRelationshipSource; - 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):
| Code | Severity | Meaning |
|---|---|---|
xmp.malformed, xmp.wrapper-missing, xmp.rdf-repeated | Warning | The packet is not well formed, lacks its wrapper, or repeats rdf:RDF; what can be read is read |
xmp.info-mismatch | Warning | /Info and XMP disagree on an equivalent property |
xmp.extension-schema-mismatch | Warning | A described property does not match its description |
pdfa.conflict | Warning | One conflict of a PDF/A target, with its clause, collected before the policy applies |
pdfa.conformance-claim-removed | ConformanceLoss | The claim was not written, and why |
pdfa.claim-raised | Information | A PDF/A-2 claim was raised to part 3 on the caller's request |
pdfa.attachment-date-missing | Information | An 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:
| Referee | Confirms |
|---|---|
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 UBL | The 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 pikepdf | Attachment bytes (--show-attachment) and listing; open_metadata() |
| ExifTool | An 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.
- The XMP read model. Delivers
PdfXmpMetadataparsed 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 toxmp.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. - XMP writing,
/Info, custom and extension schemas. Delivers the builder, the deterministic serializer, custom/Infokeys throughpdfx:,XmpExtensionSchemaread and generated,PdfDocumentMetadata, M03's writer replaced, and themetadataverb. 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. - 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. - 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-facefaces,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. - 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, andRaiseClaimToPartThree. 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-attachmentsshows each with its relationship;documents/archival/libreoffice-report-pdfa2b.pdfraised to part 3 with an attachment passes veraPDF's 3b profile;vendor/verapdf/pdfa3b-embedded-pass.pdfreads clean and the rule veraPDF reports onpdfa3b-embedded-fail.pdf, where it concerns the attachment, is one our checks name. Leaves level a. - PDF/A-3a and the dual claim. Delivers level a on M13's structure and the
pdfuaidextension schema. Proved by integration: veraPDF's 3a and ua1 profiles pass the reference documents under the dual claim;pdfinfo -struct-textprints the same tree as under PDF/UA-1 alone; the XMP's shape matches PDFlib's invoice's. Leaves the satellite. - The satellite and reading hybrids. Delivers the package, the data table (each row verified and sourced),
FacturXReader, identification, streamed extraction, the container findings,facturx infoandextract, and the measurement of the.Htmldependency for the packaging decision; the manifest'sexpect.facturx—flavor,profile,version(unknownwhere the file cannot tell),attachment,relationshipandxmlSha256— and its schema, written bybuild_corpus.pyfrom Akretion's factur-x library, the XMP andqpdf --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 bytesqpdf --show-attachmentwrites, and the flavor and profile equal what Akretion's factur-x library and the XMP state. Leaves validation. - 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
xmlformat — our rule identifiers equal the KoSIT validator's, in both directions. Leaves UBL and the national rules. - 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, andfacturx 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. - 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 underfr-FRand 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. - Generating and embedding Factur-X. Delivers
FacturXDocument, the visible-consistency check, the XMP,FacturXEmbedderas an incremental update,facturx embedandhtml2pdf --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 intovendor/docentric/fop-factur-x-visualization.pdfleaves its original bytes untouched, keeps veraPDF's 3b verdict, and passes Mustang. Leaves the rendition. - The rendition. Delivers the view model, the template and its labels,
EInvoiceRenderer, theSourceattachment, the statement, the optional report page, andfacturx render. Proved by unit tests; integration: every XML of the corpus's hybrids renders to a document veraPDF passes at 3a and ua1, in whichpdftotextfinds every business term's value the XML carries, and whose attachment is the XML byte for byte;FacturXBenchmarksandPdfABenchmarksrecorded; 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 twordf:RDF, with a legacy prefix, not well formed; the byte-for-byte path; serializer determinism (namespace order, property order, padding);/Infoin step in 1.7 and dropped in 2.0; custom keys throughpdfx:; 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 (
Refusecollects,RemoveClaimremoves 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,qnested 29 deep, a name of 128 bytes). - Attachments: one file specification object for both references; MIME escaping; dates only from the caller;
RaiseClaimToPartThreerefused 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 versionUnknownwhere 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.
| Documents | Behavior | Verified by |
|---|---|---|
| The reference documents under PDF/A-3b, 3u and 3a | veraPDF reports no error at each level | PdfARefereeTests.Reference_documents_pass_pdf_a_3_at_every_level |
| The reference documents under PDF/A-3a with PDF/UA-1 | veraPDF reports no error under 3a and under ua1; the XMP carries pdfaid, pdfuaid and its extension schema | PdfARefereeTests.The_dual_claim_passes_both_profiles |
| Our PDF/A-3 outputs, committed to the corpus | They 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 upholds | CorpusPdfATests.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 attachment | veraPDF's 3b profile reports no error; qpdf --list-attachments lists each file with its relationship and MIME type | PdfARefereeTests.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-1 | Mustang 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 chose | FacturXRefereeTests.Generated_invoices_are_accepted |
| The corpus's hybrids | The 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 document | CorpusFacturXTests.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 files | CorpusFacturXTests.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 here | Our validator fires exactly the rule identifiers the KoSIT validator fires with the official artifacts, no more and no fewer | InvoiceRuleDifferentialTests.Our_rules_fire_exactly_where_the_official_artifacts_fire |
| Invoices built from the model for each profile | The KoSIT validator accepts their CII and their UBL under EN 16931, and XRechnung's configuration the XRECHNUNG ones | FacturXRefereeTests.Model_invoices_pass_kosit_in_cii_and_ubl |
| The corpus's hybrids' XMLs | Read into the model and written again, every business term keeps its value, or the term the model cannot hold is reported | CorpusFacturXTests.Corpus_xml_round_trips_through_the_model |
vendor/docentric/fop-factur-x-visualization.pdf — a PDF/A-3b claim with no XML attached | Our XML embedded as an incremental update: the original bytes untouched, veraPDF's 3b verdict kept, Mustang reports the result valid | FacturXRefereeTests.Embedding_into_a_received_pdf_a_3_keeps_its_claim |
| The corpus's hybrids' XMLs, CII of every flavor, XRechnung, and the UBL payload | Each 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: schema | CorpusFacturXTests.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 XFA | Namespaces, 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 untouched | CorpusXmpTests.Every_packet_reads_as_exiftool_reads_it |
| Every committed PDF/A document, a custom property added | veraPDF upholds each claim it upheld before | PdfARefereeTests.Custom_metadata_keeps_every_pdf_a_claim |
| The ZUGFeRD release-candidate invoice, the 4s4u sample, PDFMaker 9's PDF/A-1b file, PDFlib's invoice | Their extension schemas read as described: namespace, prefix, each property's type and category | CorpusXmpTests.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 attachment | Memory flat as pages grow; the attachment streamed, memory independent of its size | CorpusPdfATests.Pdf_a_generation_holds_its_budget, PdfABenchmarks |
| Every output above | Two runs give identical bytes, under the invariant culture and fr-FR, on Linux and on Windows | CorpusFacturXTests.Factur_x_output_is_deterministic |
| The same operations through the tool | The verbs produce the API's bytes and reports | CorpusToolTests.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,AlternativeandSupplement(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), andvendor/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 variantsxmp-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
| Need | Why | Priority | Likely 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 recorded | The acceptance compares with recorded expectations, which must come from the file and an independent tool, not from our reader | 1 | Generated 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 them | 1 | Generated 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 rendition | The 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 follows | 1 | A 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 restriction | A differential harness fed only valid invoices proves that both sides say nothing | 1 | Generated 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 sample | The current versions, the profile the French reform adds, and the elements only they define; nothing in the corpus is newer than 2025's DWC files | 2 | A 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 job | 2 | A contribution |
| An XRechnung XML with KoSIT's own visualization of it | The rendition's coverage compared with the official visualization, field by field; fop26-xrechnung-visualization.pdf has no XML beside it | 2 | A 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 claims | 2 | Generated here, with M13 |
| A PDF/A-3a with PDF/UA-1 Factur-X from another producer | The dual claim is compared with PDFlib's A-2a invoice and Symtrax's A-3a, neither both at once | 3 | A 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 CSV | Associated files other than invoices are checked on our output only | 3 | Generated 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.
/AFis 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_PDFA1for every part. The 1 is not a part number. - A MIME type is a name:
text/xmlis 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. /ModDatefrom 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:Versionhas 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 typeText, categoryexternalfor each property — or veraPDF fails the PDF/A claim on a hybrid Mustang calls valid. EN 16931has a space inConformanceLevel; 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-FRorde-DEin an amount, orCRLFfrom 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,
qnested 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,/Infoin 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: thexmp.*andpdfa.*codes; the satellite's findings.docs/website/docs/reference/tool/:html2pdf --pdf-a,metadata,facturx.docs/website/docs/introduction.mdanddocs/features/features.json: thefacturxentry 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'sxmlformat, 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.FacturXreads, validates, generates, embeds and renders, every profile and version row verified against its specification and sourced; the packaging decision on.Htmlis 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 corpusrun recorded instatus.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.
-
PdfABenchmarksandFacturXBenchmarksmeasure generation, embedding, validation and rendition withMemoryDiagnoser;status.mdrecords 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).