PDF/A-3 Embedding
Embed XML invoices into PDF/A-3 documents to create ZUGFeRD / Factur-X hybrid invoices. The embedded PDF contains both a human-readable invoice and the machine-readable XML data.
Prerequisites
Ghostscript must be installed on your system:
# Debian / Ubuntu
sudo apt-get install ghostscript
# Arch Linux
sudo pacman -S ghostscript
# macOS
brew install ghostscript
# Verify
gs --versionThen download the required PDF artifacts:
bin/setup-schemasThis downloads zugferd.ps and the sRGB ICC color profile into vendor/zugferd/.
Basic Usage
require "zugpferd"
require "zugpferd/pdf" # explicit opt-in for PDF support
# Build and serialize an invoice (CII is typical for ZUGFeRD)
invoice = Zugpferd::Model::Invoice.new(
number: "RE-2024-001",
issue_date: Date.new(2024, 6, 15),
currency_code: "EUR"
)
# ... configure seller, buyer, line items, totals ...
xml = Zugpferd::CII::Writer.new.write(invoice)
# Embed into an existing PDF
embedder = Zugpferd::PDF::Embedder.new
embedder.embed(
pdf_path: "rechnung.pdf",
xml: xml,
output_path: "rechnung_zugferd.pdf"
)The output is a PDF/A-3 compliant file with the XML attached as factur-x.xml.
Versions and Conformance Levels
The version parameter controls which ZUGFeRD/Factur-X version metadata is written:
| Version | XML Filename | Profiles |
|---|---|---|
"2p1" (default) | factur-x.xml | MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG |
"2p0" | zugferd-invoice.xml | MINIMUM, BASIC WL, BASIC, EN 16931, EXTENDED, XRECHNUNG |
"1p0" | ZUGFeRD-invoice.xml | BASIC, COMFORT, EXTENDED |
Conformance level must match the CIUS
If your invoice uses the XRechnung CIUS (i.e. customization_id contains urn:xeinkauf.de:kosit:xrechnung_3.0), you must set conformance_level: "XRECHNUNG". A mismatch will cause validation warnings in downstream systems.
# XRechnung invoice → conformance_level must be "XRECHNUNG"
embedder.embed(
pdf_path: "input.pdf",
xml: xml,
output_path: "output.pdf",
version: "2p1",
conformance_level: "XRECHNUNG"
)
# Plain EN 16931 invoice (no national CIUS) → "EN 16931"
embedder.embed(
pdf_path: "input.pdf",
xml: xml,
output_path: "output.pdf",
version: "2p1",
conformance_level: "EN 16931"
)
# ZUGFeRD 1.0
embedder.embed(
pdf_path: "input.pdf",
xml: xml,
output_path: "output.pdf",
version: "1p0",
conformance_level: "EXTENDED"
)Error Handling
begin
embedder.embed(pdf_path: "input.pdf", xml: xml, output_path: "output.pdf")
rescue Zugpferd::PDF::Embedder::GhostscriptNotFound
# Ghostscript is not installed or not in PATH
rescue Zugpferd::PDF::Embedder::EmbedError => e
# Ghostscript failed — e.message contains stderr output
rescue ArgumentError => e
# Invalid version, conformance level, or missing input file
endHow It Works
Under the hood, the embedder:
- Writes the XML to a temporary file
- Invokes Ghostscript with
-dPDFA=3and the vendoredzugferd.psPostScript program - Ghostscript converts the input PDF to PDF/A-3, embeds the XML with correct AF arrays, AFRelationship, XMP metadata, and ICC color profile
- Cleans up temporary files
The vendored zugferd.ps and default_rgb.icc files are sourced from the Ghostscript project, ensuring consistent behavior independent of the installed Ghostscript version.
Validating the Output
For production use, validate generated PDFs with veraPDF (PDF/A-3 structure) or Mustangproject (full ZUGFeRD validation). Both are available as Docker containers — see Docker Setup.
Docker Setup
# docker-compose.yml
services:
verapdf:
image: verapdf/rest:latest
ports:
- "8080:8080"
mustang:
build: docker/mustang# Start veraPDF
docker compose up -d verapdf
# Build Mustangproject image
docker compose build mustangveraPDF (PDF/A-3 compliance)
require "zugpferd/validation/pdf_validator"
validator = Zugpferd::Validation::PdfValidator.new
result = validator.validate("output.pdf", profile: "3b")
if result.compliant
puts "PDF/A-3b compliant"
else
result.failures.each { |f| puts f[:description] }
endMustangproject (full ZUGFeRD validation)
require "zugpferd/validation/mustang_validator"
validator = Zugpferd::Validation::MustangValidator.new
result = validator.validate("output.pdf")
if result.valid
puts "ZUGFeRD valid"
else
puts result.output
end