A developer guide for creating compliant e-invoice PDFs and extracting their embedded XML data using the PDFluent SDK.
XML extraction works with the base crate. The einvoice feature adds profile detection and validation helpers.
# Cargo.toml
[dependencies]
pdfluent = { version = "0.9", features = ["einvoice"] }ZUGFeRD and Factur-X are hybrid e-invoice formats. The PDF is human-readable and machine-readable at the same time. The XML payload is embedded as an attachment.
use pdfluent::{PdfAProfile, PdfDocument, ZugferdProfile};
fn main() -> pdfluent::Result<()> {
let mut doc = PdfDocument::open("invoice-template.pdf")?;
let xml = std::fs::read("factur-x.xml")?;
doc.attach_zugferd_xml(&xml, ZugferdProfile::EN16931)?;
// PDF/A-3 is the conformance level that permits embedded files.
let archival = doc.convert_to_pdfa(PdfAProfile::A3b)?;
archival.save("invoice-einvoice.pdf")?;
Ok(())
}Start from the PDF a human reads. The XML is attached to it; it does not replace it.
let mut doc = PdfDocument::open("invoice-template.pdf")?;attach_zugferd_xml takes the XML as bytes. PDFluent attaches it; it does not generate or validate the invoice content.
let xml = std::fs::read("factur-x.xml")?;The profile writes the ZUGFeRD identifier into the document XMP. Pick the one the XML was built to: Minimum, BasicWL, Basic, EN16931, Extended or XRechnung.
use pdfluent::ZugferdProfile;
doc.attach_zugferd_xml(&xml, ZugferdProfile::EN16931)?;Attaching does not change the conformance level. PDF/A-3 is the level that allows embedded files, and convert_to_pdfa returns a new document rather than changing this one in place.
use pdfluent::PdfAProfile;
let archival = doc.convert_to_pdfa(PdfAProfile::A3b)?;Write the converted document. The XML travels inside it as an associated file named factur-x.xml.
archival.save("invoice-einvoice.pdf")?;Read the structured invoice XML embedded inside a ZUGFeRD or Factur-X PDF. Parse it for automated accounting import.
use pdfluent::PdfDocument;
fn main() -> pdfluent::Result<()> {
let doc = PdfDocument::open("invoice.pdf")?;
match doc.zugferd_xml()? {
Some(xml) => {
println!("found {} bytes of invoice XML", xml.len());
std::fs::write("extracted-invoice.xml", &xml)?;
}
None => println!("No e-invoice XML found in this PDF."),
}
Ok(())
}Reading the attachment does not modify the document, so an immutable handle is enough.
let doc = PdfDocument::open("invoice.pdf")?;zugferd_xml returns Ok(None) for a PDF that carries no invoice attachment. That is the normal answer, not an error.
let xml = doc.zugferd_xml()?;Match on the Option so a plain PDF is reported rather than treated as a failure.
match doc.zugferd_xml()? {
Some(xml) => println!("found {} bytes of invoice XML", xml.len()),
None => println!("No e-invoice XML found in this PDF."),
}The return value is the raw bytes of the attachment, ready to hand to an XML parser or write to disk.
if let Some(xml) = doc.zugferd_xml()? {
std::fs::write("extracted-invoice.xml", &xml)?;
}