Attach ZUGFeRD and Factur-X XML, and extract it again

A developer guide for creating compliant e-invoice PDFs and extracting their embedded XML data using the PDFluent SDK.

Add PDFluent to Cargo.toml

XML extraction works with the base crate. The einvoice feature adds profile detection and validation helpers.

toml
# Cargo.toml
[dependencies]
pdfluent = { version = "0.9", features = ["einvoice"] }

Create a ZUGFeRD or Factur-X e-invoice PDF in Rust

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.

rust
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(())
}
  1. Open the rendered invoice

    Start from the PDF a human reads. The XML is attached to it; it does not replace it.

    rust
    let mut doc = PdfDocument::open("invoice-template.pdf")?;
  2. Read the invoice XML

    attach_zugferd_xml takes the XML as bytes. PDFluent attaches it; it does not generate or validate the invoice content.

    rust
    let xml = std::fs::read("factur-x.xml")?;
  3. Attach it with the matching profile

    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.

    rust
    use pdfluent::ZugferdProfile;
    
    doc.attach_zugferd_xml(&xml, ZugferdProfile::EN16931)?;
  4. Convert to PDF/A-3

    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.

    rust
    use pdfluent::PdfAProfile;
    
    let archival = doc.convert_to_pdfa(PdfAProfile::A3b)?;
  5. Save the e-invoice

    Write the converted document. The XML travels inside it as an associated file named factur-x.xml.

    rust
    archival.save("invoice-einvoice.pdf")?;
  • PDFluent attaches and extracts invoice XML. It does not build the XML, and it does not check the invoice against EN 16931 business rules -- produce and validate the XML with a dedicated e-invoicing library first.
  • The attachment is written as an associated file named factur-x.xml with an /AFRelationship of Alternative, which is what ZUGFeRD and Factur-X readers look for.
  • The profile argument records which ZUGFeRD identifier goes in the XMP. It does not change or validate the XML you supply, so a mismatch between the two is not detected here.
  • Attaching does not imply PDF/A. Call convert_to_pdfa(PdfAProfile::A3b) yourself; A1b and A2b do not permit embedded files.
  • convert_to_pdfa returns a new document. Save the returned value, not the original.

Extract ZUGFeRD or Factur-X XML from a PDF in Rust

Read the structured invoice XML embedded inside a ZUGFeRD or Factur-X PDF. Parse it for automated accounting import.

rust
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(())
}
  1. Open the PDF

    Reading the attachment does not modify the document, so an immutable handle is enough.

    rust
    let doc = PdfDocument::open("invoice.pdf")?;
  2. Ask for the invoice XML

    zugferd_xml returns Ok(None) for a PDF that carries no invoice attachment. That is the normal answer, not an error.

    rust
    let xml = doc.zugferd_xml()?;
  3. Handle both outcomes

    Match on the Option so a plain PDF is reported rather than treated as a failure.

    rust
    match doc.zugferd_xml()? {
        Some(xml) => println!("found {} bytes of invoice XML", xml.len()),
        None => println!("No e-invoice XML found in this PDF."),
    }
  4. Write the XML out

    The return value is the raw bytes of the attachment, ready to hand to an XML parser or write to disk.

    rust
    if let Some(xml) = doc.zugferd_xml()? {
        std::fs::write("extracted-invoice.xml", &xml)?;
    }
  • zugferd_xml returns the raw bytes of the embedded file. Parse them with an XML library to read invoice values; PDFluent does not model the invoice contents.
  • Ok(None) means the PDF carries no invoice attachment. Only an unreadable or damaged document produces an error.
  • The profile is recorded in the document XMP rather than returned alongside the bytes, so read it from the XML or the metadata if you need it.
  • Extraction looks for the associated file the ZUGFeRD and Factur-X specifications define. A PDF that merely has an XML file stapled to it by other means is not guaranteed to be found.