Available Now: Explore our latest release with enhanced accessibility and powerful IDP features

How to Create ZUGFeRD-valid Invoices with Apryse

By Apryse | 2019 Jan 10

Sanity Image
Read time

4 min

The ZUGFeRD standard makes the sharing of payment information between entities large and small a lot simpler. However, building a program to automatically generate ZUGFeRD-compliant invoices is a little more involved. This post shows you how you can create valid ZUGFeRD files via Windows, Mac, or Linux apps by leveraging Apryse’s full-featured SDK.

Generate ZUGFeRD Invoices with Apryse

Copied to clipboard

Start by downloading a trial of Apryse's SDK, which includes the features you will need, such as adding file attachments to a PDF, converting to PDF/A format, and PDF/A verification; as well as offerings for digital signature creation, redaction and much more.

ZUGFeRD invoices bring together two parts:

  • XML file: serves as a machine-readable representation of the invoice (which should be named ZUGFeRD-invoice.xml).
  • PDF/A-3: serves as both the human-readable image and as a container for the XML.

When generating the human-readable invoice, you may wish to work in a higher level format like DOCX, XAML, or HTML before converting to PDF, which makes direct editing and structuring of the file more involved.

Apryse's SDK won’t restrict you to working in PDF since it is able to convert 20+ file formats via pdftron.PDF.Convert.ToPDF, with samples available in C#, C++, Java, Obj-C, PHP, Python, Ruby, and VB.

Embed an XML in a PDF

Copied to clipboard

The first step after creating your XML file is to embed the XML within the PDF invoice on a “one-to-one” basis. That means each ZUGFeRD invoice should consist of a single XML representation paired with a matching PDF representation. You can later add as many non-invoice file attachments as you wish.

This post provides sample code in C#⁠—with the full sample available for download here. Note that this sample assumes you have a file named ZUGFeRD-invoice.xml in the input_path TestFiles. (It will not generate an XML for you.)

Below is the section from the sample that reads the PDF document and an XML invoice from your chosen inputs, embeds the XML invoice into the PDF as an attachment, and saves the result as a new, in-memory PDF file (stored in the byte array). Importantly, when embedding the XML, the code attaches the XML to the PDF as a whole, rather than to a section of the document.

byte[] doc_bytes_invoice_added = null;
using (PDFDoc in_doc = new PDFDoc(input_path + "fish.pdf"))
{
	in_doc.InitSecurityHandler();
	NameTree files = NameTree.Create(in_doc, "EmbeddedFiles");
	FileSpec fs = FileSpec.Create(in_doc, input_path + "ZUGFeRD-invoice.xml", true);
	byte[] file1_name = System.Text.Encoding.UTF8.GetBytes("ZUGFeRD-invoice.xml");
	files.Put(file1_name, fs.GetSDFObj());
	fs.GetSDFObj().PutText("Desc", "ZUGFeRD Rechnung");
	fs.GetSDFObj().PutName("AFRelationship", "Alternative");
	fs.GetSDFObj().PutText("F", "ZUGFeRD-invoice.xml");
	fs.GetSDFObj().PutText("UF", "ZUGFeRD-invoice.xml");
	Obj collection = in_doc.GetRoot().FindObj("Collection");
	if (collection == null) collection = in_doc.GetRoot().PutDict("Collection");
	collection.PutName("View", "T");
	doc_bytes_invoice_added = in_doc.Save(SDFDoc.SaveOptions.e_incremental);
}

Convert a PDF to PDF/A-3

Copied to clipboard

Next, convert to PDF/A-3 to ensure there are no objects (e.g., non-embedded fonts) in your invoice that could prevent a PDF reader from correctly displaying the content. The PDF/A-3 standard is mandatory for ZUGFeRD invoices, as only PDF/A-3 permits you to embed non-PDF/A file types.

For the following sample, we’ve also chosen to convert to PDF/A conformance level b. This is generally a good path to follow; selecting level b will make it easier for invoices to pass auto-conversion. You can always change the syntax to convert to another conformance level (i.e., level a or level u) if you are restricted by a policy or want to enhance your invoices’ accessibility features, such as ensuring text that can be reliably searched and copied. Just know that this may entail doing extra scripting to mark PDF content for logical structure and read order, and to add character mappings to Unicode if your PDFs do not contain this information already.

byte[] doc_bytes_pdfa = null;
using (PDFACompliance pdf_a = new PDFACompliance(true, doc_bytes_invoice_added, null, PDFACompliance.Conformance.e_Level3B, null, 10, false))
{
	doc_bytes_pdfa = pdf_a.SaveAs(false);
}

Embed XMP Metadata

Copied to clipboard

Finally, add XMP metadata to describe the embedded invoice as a ZUGFeRD invoice by executing the following code. Afterwards, it will save to disk a ZUGFeRD-compliant PDF.

using (PDFDoc in_doc = new PDFDoc(doc_bytes_pdfa, doc_bytes_pdfa.Length))
{
	in_doc.InitSecurityHandler();
	Obj xmp_stm = in_doc.GetRoot().FindObj("Metadata");
	Filter stm = xmp_stm.GetDecodedStream();
	FilterReader reader = new FilterReader(stm);
	byte[] buf = new byte[stm.Size()];
	int nb = reader.Read(buf);
	String xmp_str = System.Text.ASCIIEncoding.ASCII.GetString(buf);
	String zugferd_xmp = @"<rdf:Description xmlns:zf=""urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0#"" rdf:about="""">
		<zf:ConformanceLevel>BASIC</zf:ConformanceLevel>
		<zf:DocumentFileName>ZUGFeRD-invoice.xml</zf:DocumentFileName>
		<zf:DocumentType>INVOICE</zf:DocumentType>
		<zf:Version>1.0</zf:Version>
	</rdf:Description>
	<rdf:Description xmlns:pdfaExtension=""http://www.aiim.org/pdfa/ns/extension/"" xmlns:pdfaSchema=""http://www.aiim.org/pdfa/ns/schema#"" xmlns:pdfaProperty=""http://www.aiim.org/pdfa/ns/property#"" rdf:about="""">
		<pdfaExtension:schemas>
			<rdf:Bag>
				<rdf:li rdf:parseType=""Resource"">
					<pdfaSchema:schema>ZUGFeRD PDFA Extension Schema</pdfaSchema:schema>
					<pdfaSchema:namespaceURI>urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
					<pdfaSchema:prefix>zf</pdfaSchema:prefix>
					<pdfaSchema:property>
						<rdf:Seq>
							<rdf:li rdf:parseType=""Resource"">
								<pdfaProperty:name>DocumentFileName</pdfaProperty:name>
								<pdfaProperty:valueType>Text</pdfaProperty:valueType>
								<pdfaProperty:category>external</pdfaProperty:category>
								<pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
							</rdf:li>
							<rdf:li rdf:parseType=""Resource"">
								<pdfaProperty:name>DocumentType</pdfaProperty:name>
								<pdfaProperty:valueType>Text</pdfaProperty:valueType>
								<pdfaProperty:category>external</pdfaProperty:category>
								<pdfaProperty:description>INVOICE</pdfaProperty:description>
							</rdf:li>
							<rdf:li rdf:parseType=""Resource"">
								<pdfaProperty:name>Version</pdfaProperty:name>
								<pdfaProperty:valueType>Text</pdfaProperty:valueType>
								<pdfaProperty:category>external</pdfaProperty:category>
								<pdfaProperty:description>The actual version of the ZUGFeRD XML schema</pdfaProperty:description>
							</rdf:li>
							<rdf:li rdf:parseType=""Resource"">
								<pdfaProperty:name>ConformanceLevel</pdfaProperty:name>
								<pdfaProperty:valueType>Text</pdfaProperty:valueType>
								<pdfaProperty:category>external</pdfaProperty:category>
								<pdfaProperty:description>The conformance level of the embedded ZUGFeRD data</pdfaProperty:description>
							</rdf:li>
						</rdf:Seq>
					</pdfaSchema:property>
				</rdf:li>
			</rdf:Bag>
		</pdfaExtension:schemas>
	</rdf:Description>";
	xmp_str = xmp_str.Insert(xmp_str.IndexOf("<rdf:Description "), zugferd_xmp);
	xmp_stm.SetStreamData(System.Text.Encoding.ASCII.GetBytes(xmp_str));
	in_doc.Save(output_path + "zugferd_invoice.pdf", SDFDoc.SaveOptions.e_linearized);
}

Use Other PDF Invoicing Features

Copied to clipboard

That’s it! You should now have a working prototype able to automatically generate ZUGFeRD-valid invoices using Apryse's SDK.

Later, you may want to add other invoicing capabilities to your solution: check out Apryse’s full-featured SDK library with a free trial.

If you have any questions about Apryse’s PDF SDK, please feel free to get in touch!

Sanity Image

Apryse

Share this post

email
linkedIn
twitter