TaxOSS32ktracked

Kaltblut

Sonatype Central javadoc

If this project saved you some time or made your day a little easier, a star would mean a lot — it helps others find it too.

A Java toolkit for working with ZUGFeRD / Factur-X hybrid invoices: detect the flavor of any hybrid PDF, extract the embedded XML and supporting attachments, and validate the carrier-side specification rules — including PDF/A-3 conformance via veraPDF.

XML-side business rules (cardinalities, EN 16931 rules, code lists ...) are out of scope for this project; use phive-rules-zugferd for those.

Per-version requirements analysis used to design this library lives under docs/. See docs/comparison.md for a cross-version overview of every PDF carrier rule from ZUGFeRD 1.0 (2014) through Factur-X 1.09.2 / ZUGFeRD 2.5.2 (2026-08-04).

Supported Versions

The detection table covers every published release since 2014:

ZUGFeRD Factur-X XMP namespace URI Embedded XML name
1.0 n/a urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0# ZUGFeRD-invoice.xml
2.0.1 n/a urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0# zugferd-invoice.xml
2.1 1.0.05 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# factur-x.xml
2.1 1.0.05 urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0# (legacy) zugferd-invoice.xml
2.2 1.0.06 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# factur-x.xml / xrechnung.xml
2.2 1.0.06 urn:zugferd:pdfa:CrossIndustryDocument:invoice:1p0# (legacy) zugferd-invoice.xml
2.3 1.0.07 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# factur-x.xml / xrechnung.xml
2.3.2 1.07.2 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# factur-x.xml / xrechnung.xml
2.3.3 1.07.3 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# factur-x.xml / xrechnung.xml
2.4 1.08 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# factur-x.xml / xrechnung.xml
2.5 1.09 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# factur-x.xml / xrechnung.xml
2.5.2 1.09.2 urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0# factur-x.xml / xrechnung.xml

Profiles per version

EZugferdProfile mirrors the fx:ConformanceLevel values (zf:ConformanceLevel in ZUGFeRD 1.0) that each spec generation permits:

ZUGFeRD Factur-X MINIMUM BASIC WL BASIC COMFORT EN 16931 EXTENDED XRECHNUNG
1.0 n/a yes yes yes
2.0.1 n/a yes yes yes yes yes
2.1 1.0.05 yes yes yes yes yes
2.2 1.0.06 yes yes yes yes yes yes
2.3 1.0.07 yes yes yes yes yes yes
2.3.2 1.07.2 yes yes yes yes yes yes
2.3.3 1.07.3 yes yes yes yes yes yes
2.4 1.08 yes yes yes yes yes yes
2.5 1.09 yes yes yes yes yes yes
2.5.2 1.09.2 yes yes yes yes yes yes

Notes:

  • COMFORT is the ZUGFeRD 1.0 name of the EN 16931-compliant core profile; from 2.0.1 onward the XMP value for it is EN 16931. EZugferdProfile.getFromIDOrNull additionally tolerates the unspaced EN16931 spelling found in the wild.
  • MINIMUM and BASIC WL were introduced in 2.0.1 — ZUGFeRD 1.0 knows neither.
  • XRECHNUNG was introduced in 2.2 as a reference profile; it changes the embedded XML name from factur-x.xml to xrechnung.xml. The legacy zugferd.de XMP extension schemas (2.1 Supplement B and the deprecated variant retained in 2.2 → 2.5.2) never permit XRECHNUNG.
  • Country restrictions on top of the table, enforced by HybridValidator from EZugferdCountry: DE↔DE must not use MINIMUM (BR-HYBRID-DE-01) or BASIC WL (BR-HYBRID-DE-02), FR↔FR must not use XRECHNUNG (BR-HYBRID-FR-01). Numbered from 2.3.2 onward; earlier generations state the same substance in prose.
  • Detection is deliberately version-agnostic: HybridInspector.detectProfile maps whatever ConformanceLevel string is present onto EZugferdProfile, and BR-HYBRID-07 only checks membership in the code list — it does not reject a profile that postdates the detected flavor.

Source: docs/comparison.md §6/§7 (permitted ConformanceLevel values) and §13 (profiles per version).

Why "Kaltblut"?

Kaltblut is German for the family of heavy draft horse breeds — the strongest and steadiest of the Zugpferde. The name is a nod to ZUGFeRD, which German speakers hear as Zugpferd ("draft horse"). Kaltblut picks the most workmanlike of them: built to pull heavy loads calmly and reliably, much like this toolkit aims to handle ZUGFeRD invoices.

As a bonus, kaltblütig in everyday German also means "cool-headed" — a useful trait for any library dealing with tax-relevant invoice processing

What Kaltblut Does (and Does Not Do)

Tier Concern Status
1 Detection & metadata: flavor, profile, XMP fields, embedded-file name, /AFRelationship implemented
2 Extraction: invoice XML, named attachments, full attachment list implemented
3 Validation: BR-HYBRID-* business rules, PDF/A-3 (via veraPDF SPI) implemented
4 Creation / embedding XML to produce hybrid PDFs not in scope
XML business-rules validation (EN 16931, KoSIT XRechnung) use phive-rules-zugferd

Project Layout

This is a multi-module Maven project:

  • kaltblut-core — the library. Source abstraction, model, inspector, extractor, validator. Classes live under com.helger.kaltblut.core.*. Emits tracing spans through the vendor-neutral ph-telemetry facade — no OpenTelemetry dependency (see Telemetry).
  • kaltblut-testfiles — shared test fixtures (sample PDFs + classpath-resource locator).
  • kaltblut-verapdf — PDF/A-3 validation adapter that wires veraPDF to the IPdfA3ValidatorSPI SPI. Optional; pull it in only if you need PDF/A-3 conformance checks.
  • kaltblut-cli — the command-line client (picocli). Builds a standalone fat JAR.

Key Library Concepts

IHybridSource — the input abstraction

All public entry points take a source. Use one of the HybridSource factories:

import com.helger.kaltblut.core.source.HybridSource;
import com.helger.kaltblut.core.source.IHybridSource;

IHybridSource s1 = HybridSource.fromFile (new File ("invoice.pdf"));         // lazy + cached
IHybridSource s2 = HybridSource.fromPath (Path.of ("invoice.pdf"));          // lazy + cached
IHybridSource s3 = HybridSource.fromBytes (aPdfBytes);                       // wraps array
IHybridSource s4 = HybridSource.fromByteBuffer (aBuffer);                    // copies
IHybridSource s5 = HybridSource.fromUrl (new URL ("https://example.com/invoice.pdf"));      // http/https only, with timeouts
IHybridSource s6 = HybridSource.fromInputStream (aIS);                       // reads now, closes
IHybridSource s7 = HybridSource.fromClasspath ("samples/invoice.pdf");       // resource path

IHybridSource is byte-array-centric: the contract is byte[] getBytes() throws IOException, plus long getSize() and String getName() as diagnostic hints. PDFBox 3 needs random access and every Kaltblut operation eventually needs the complete PDF in memory, so distinguishing single-read from multi-read inputs added API surface without value. Implementations may read lazily on first call and cache the result; callers must not mutate the returned array.

HybridSource.fromUrl only accepts http and https URLs and applies sensible connect / read timeouts (defaults 10 s / 60 s); other schemes (file:, jar:, ftp:, ...) are refused to prevent accidental SSRF / local-file-read when forwarding caller-supplied URLs.

HybridLimits — byte / count ceilings

Every Tier-1/2/3 entry point accepts an optional HybridLimits (defaulting to HybridLimits.DEFAULTS) that caps:

  • the input PDF size (default 64 MiB),
  • per-attachment inflated size (default 32 MiB),
  • aggregate attachment size (default 128 MiB),
  • attachment count (default 100).

Use HybridLimits.UNLIMITED to disable, or build a custom instance with the immutable withMaxPdfBytes(...) / withMaxAttachmentBytes(...) / ... witherers. Reading past a limit throws IOException rather than letting the JVM OOM.

Model

The model classes in com.helger.kaltblut.core.model are immutable value objects:

  • EZugferdFlavor — namespace-URI fingerprint of the spec generation.
  • EZugferdProfileMINIMUM, BASIC_WL, BASIC, COMFORT, EN_16931, EXTENDED, XRECHNUNG (see Profiles per version for which spec generation permits which).
  • EAFRelationshipData, Source, Alternative, Supplement, Unspecified.
  • EZugferdCountryDE, FR, OTHER (drives country-specific BR-HYBRID rules).
  • HybridMetadata — single snapshot of XMP fields + /AF data.
  • HybridAttachment — name, MIME type, AFRelationship, ModDate, bytes, invoice-XML flag.

Usage

Tier 1: detection

import com.helger.kaltblut.core.inspect.HybridInspector;
import com.helger.kaltblut.core.model.EZugferdFlavor;
import com.helger.kaltblut.core.model.HybridMetadata;

IHybridSource aSource = HybridSource.fromFile (new File ("invoice.pdf"));

if (HybridInspector.isHybridInvoice (aSource))
{
  EZugferdFlavor eFlavor = HybridInspector.detectFlavor (aSource);
  HybridMetadata aMeta = HybridInspector.readMetadata (aSource);
  System.out.println ("Flavor:        " + aMeta.getFlavor ());
  System.out.println ("Profile:       " + aMeta.getProfile ());
  System.out.println ("Embedded file: " + aMeta.getEmbeddedFileName ());
  System.out.println ("AFRelationship: " + aMeta.getAFRelationship ());
}

Tier 2: extraction

import com.helger.kaltblut.core.extract.HybridExtractor;
import com.helger.kaltblut.core.model.HybridAttachment;

byte [] aXmlBytes = HybridExtractor.extractInvoiceXml (aSource);
List <HybridAttachment> aAttachments = HybridExtractor.listAttachments (aSource);
byte [] aExcel = HybridExtractor.extractAttachment (aSource, "list_of_measurement.xlsx");

Security note: the bytes returned by extractInvoiceXml / extractAttachment come from a potentially untrusted PDF. If you parse the XML yourself, configure your XML processor to disable external entities, DTDs, and XInclude (i.e. FEATURE_SECURE_PROCESSING=true plus disallow-doctype-decl=true), or use a library — such as phive-rules-zugferd — that does so by default. Otherwise a malicious invoice can XXE-read local files or trigger SSRF.

Tier 3: validation

import com.helger.kaltblut.core.model.EZugferdCountry;
import com.helger.kaltblut.core.validate.HybridFinding;
import com.helger.kaltblut.core.validate.HybridValidator;
import com.helger.kaltblut.core.validate.HybridValidationLayer;
import com.helger.kaltblut.core.validate.HybridValidationResult;

HybridValidator aValidator = new HybridValidator ();
aValidator.getSettings ()
          .setCountry (EZugferdCountry.DE)
          .setCheckPdfA3 (true)
          .setApplyDePdfADowngrade (true);

HybridValidationResult aResult = aValidator.validate (aSource);

// Per-layer reporting: one BR_HYBRID layer + (when enabled) one PDF_A3 layer, each
// carrying its own findings and wall-clock duration.
for (HybridValidationLayer aLayer : aResult.getAllLayers ())
{
  System.out.println (aLayer.getDisplayName () + " - " + aLayer.getDuration ().toMillis () + "ms");
  for (HybridFinding aF : aLayer.getAllFindings ())
    System.out.println ("  " + aF);
}

// Aggregate predicates still work across all layers.
if (!aResult.isValid ())
  System.err.println ("Document considered invalid");

PDF/A-3 validation runs via the IPdfA3ValidatorSPI SPI. Add kaltblut-verapdf to the classpath to enable veraPDF; without it validate() records a single INFORMATION finding noting that PDF/A-3 conformance was not checked.

Command line

Build the standalone fat JAR and run it:

mvn clean package
java -jar kaltblut-cli/target/kaltblut-cli-full.jar [subcommand] [options] <files...>

Subcommands:

Subcommand Description
inspect Print flavor, profile, XMP fields, embedded-file name, and /AFRelationship.
extract Write the embedded invoice XML to disk.
attachments List all embedded files (invoice XML + supporting documents).
validate Run BR-HYBRID-* business rules and PDF/A-3 validation. Exit code 0 if no ERROR findings.

Common options:

Option Subcommand Description Default
-o, --output-dir extract Directory to write XML files to .
-s, --suffix extract Output filename suffix -invoice
-c, --country validate DE, FR, or OTHER — drives country-specific rules OTHER
--no-pdfa validate Skip PDF/A-3 validation via the SPI off
--no-de-pdfa-downgrade validate Disable the BR-FX-DE-03 downgrade for DE↔DE invoices off
-h, --help all Show help
-V, --version all Show version

Examples:

# Detect the flavor of one or more PDFs
java -jar kaltblut-cli-full.jar inspect invoice.pdf another-invoice.pdf

# Extract the invoice XML to /tmp/out/
java -jar kaltblut-cli-full.jar extract -o /tmp/out invoice.pdf

# List all embedded files in a PDF
java -jar kaltblut-cli-full.jar attachments invoice.pdf

# Validate a DE↔DE invoice (PDF/A-3 errors downgraded per BR-FX-DE-03)
java -jar kaltblut-cli-full.jar validate -c DE invoice.pdf

# Validate without PDF/A-3 (fast path; only the BR-HYBRID-* rules run)
java -jar kaltblut-cli-full.jar validate --no-pdfa invoice.pdf

Building

Requires Java 17+ and Maven.

mvn clean package

The build produces (replacing x.y.z with the effective version):

  • kaltblut-core/target/kaltblut-core-x.y.z-SNAPSHOT.jar — core library JAR.
  • kaltblut-verapdf/target/kaltblut-verapdf-x.y.z-SNAPSHOT.jar — veraPDF adapter JAR.
  • kaltblut-cli/target/kaltblut-cli-x.y.z-SNAPSHOT.jar — CLI library JAR.
  • kaltblut-cli/target/kaltblut-cli-full.jar — standalone executable fat JAR (all dependencies bundled).

Maven Coordinates

<!-- Core library: detection + extraction + BR-HYBRID validation -->
<dependency>
  <groupId>com.helger.kaltblut</groupId>
  <artifactId>kaltblut-core</artifactId>
  <version>x.y.z</version>
</dependency>

<!-- Optional: veraPDF-backed PDF/A-3 validation -->
<dependency>
  <groupId>com.helger.kaltblut</groupId>
  <artifactId>kaltblut-verapdf</artifactId>
  <version>x.y.z</version>
</dependency>

kaltblut-core transitively pulls the com.helger.telemetry:ph-telemetry facade only; no OpenTelemetry ends up on your classpath unless you add a binding yourself (see Telemetry).

Telemetry (OpenTelemetry)

Kaltblut is instrumented for distributed tracing via the vendor-neutral ph-telemetry facade. The library modules depend only on the facade, not on OpenTelemetry: when no tracer is registered every span is a cheap no-op, so there is zero runtime cost and zero extra dependency for callers that do not want telemetry.

The public entry points emit spans that nest into a single trace (a caller-side waterfall):

Span Emitted by Kind
kaltblut.inspect HybridInspector.readMetadata INTERNAL
kaltblut.extract (operation attribute) HybridExtractor (all three funnels) INTERNAL
kaltblut.validate + kaltblut.validate.brhybrid HybridValidator.validate INTERNAL
kaltblut.source.read reading the raw PDF bytes CLIENT
kaltblut.pdf.open PDFBox parse (Loader.loadPDF) INTERNAL
kaltblut.xmp.parse XMP metadata parse INTERNAL
kaltblut.attachments.extract embedded-file extraction INTERNAL
kaltblut.pdfa3.validate + .parse / .check kaltblut-verapdf PDF/A-3 run CLIENT

Spans carry attributes such as kaltblut.source.size_bytes, kaltblut.flavor, kaltblut.profile, kaltblut.attachment.count, kaltblut.pdfa.flavour, kaltblut.pdfa.compliant, and kaltblut.findings.total.

Enabling a backend

To actually export spans, an application registers a ph-telemetry tracer SPI and installs an OpenTelemetry SDK. The kaltblut-cli module does exactly this — add com.helger.telemetry:ph-telemetry-otel plus the OpenTelemetry SDK/exporter to your own deployment module, register your OtelTelemetryTracerSPI subclass via META-INF/services/com.helger.telemetry.ITelemetryTracerSPI, and install the SDK once at startup (before the first span, since the binding caches the resolved tracer).

The CLI keeps telemetry opt-in: it installs the SDK only when -Dotel.enabled=true (or the OTEL_ENABLED=true environment variable) is set. All endpoint / exporter / sampling configuration comes from the standard OpenTelemetry environment variables:

# Export CLI traces to an OTLP collector (e.g. grafana/otel-lgtm on localhost)
OTEL_ENABLED=true \
OTEL_SERVICE_NAME=kaltblut \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
  java -jar kaltblut-cli-full.jar validate invoice.pdf

Extending

To plug in a different PDF/A-3 validator (or none at all), implement com.helger.kaltblut.core.validate.IPdfA3ValidatorSPI and register the class via META-INF/services/com.helger.kaltblut.core.validate.IPdfA3ValidatorSPI. The validator is discovered via ServiceLoader; only the first implementation found is used.

License

Apache License, Version 2.0.

News and Noteworthy

v0.9.6 - work in progress

  • README: added a "Profiles per version" mapping table (ZUGFeRD / Factur-X version → permitted fx:ConformanceLevel profiles), derived from docs/comparison.md.

v0.9.5 - 2026-08-09

  • Added support for ZUGFeRD v2.5.2 / Factur-X 1.09.2. No code changes were necessary: the carrier rules are unchanged vs v2.5, and the new BR-FX-DE-04 (Warning, DE) is an XML-content rule that cannot be checked at the PDF-carrier level. See docs/2.5.2.md for the analysis.

v0.9.4 - 2026-07-15

  • Security hardening of the untrusted-PDF parsing paths:
    • The XMP /Metadata stream is now read through the bounded reader and capped at HybridLimits.getMaxPdfBytes(), closing a decompression-bomb vector that previously bypassed the PDF-size limit.
    • The embedded-files name tree is traversed with a maximum recursion depth, so a cyclic or deeply nested tree raises a clean IOException instead of a StackOverflowError.
    • Behaviour change: when veraPDF throws while validating an untrusted PDF, the PDF/A-3 layer now emits an ERROR (fail-closed) instead of a WARNING, so such a document is reported as invalid rather than valid; the finding no longer echoes the raw exception message.
    • HybridAttachment now defensively copies the payload in its constructor, matching the @Immutable contract already honoured by getBytes().
    • Attacker-controlled strings (embedded-file names, XMP values) are stripped of control characters before being written to logs and to the CLI console, preventing log / terminal escape-sequence injection.
    • The CLI extract subcommand writes the output XML with NOFOLLOW_LINKS, refusing to write through a pre-planted symlink in the output directory.
    • Clarified the HybridSource.fromUrl Javadoc: the scheme check does not prevent host-level SSRF or re-validate HTTP redirects — host / IP allow-listing remains the caller's responsibility.

v0.9.3 - 2026-07-02

  • Added distributed-tracing instrumentation via the vendor-neutral ph-telemetry facade. kaltblut-core and kaltblut-verapdf emit spans around the usual hotspots (source read, PDFBox parse, XMP parse, attachment extraction, BR-HYBRID + PDF/A-3 validation) and degrade to no-ops when no tracer is registered — no OpenTelemetry dependency is added to the library. The kaltblut-cli module ships an opt-in OpenTelemetry binding (-Dotel.enabled=true / OTEL_ENABLED=true). See Telemetry.

v0.9.2 - 2026-06-10

  • Added support for ZUGFeRD v2.5

v0.9.1 - 2026-05-13

  • Validation: the result of HybridValidator.validate is now structured as a list of HybridValidationLayers (BR_HYBRID + optional PDF_A3, identified by EHybridValidationLayerKind) instead of a flat finding list. Each layer carries its own findings and wall-clock Duration. Aggregate predicates on HybridValidationResult continue to work across all layers.
  • Breaking: ValidationResult renamed to HybridValidationResult, and the new per-layer container is HybridValidationLayer.
  • Breaking: EHybridSeverity.FATAL renamed to ERROR. Predicate methods follow: isFatal()isError(), hasFatal()hasError(), hasFatalRule()hasErrorRule().
  • EHybridSeverity entries now carry the equivalent ph-commons EErrorLevel via getErrorLevel(), so consumers mapping findings into ph-commons error infrastructure no longer need a translation table.
  • EZugferdCountry now implements IHasID<String> with getID() and the static getFromIDOrNull(String) factory, matching the style of the other ph-commons-based enums.
  • CLI validate subcommand prints one line per layer with its kind, finding count, and duration, then the layer's findings indented underneath.

v0.9.0 - 2026-05-13

  • Detection: recognises all five XMP extension-schema namespaces seen across ZUGFeRD 1.0, 2.0.1, 2.1, 2.2, 2.3, 2.3.2, 2.3.3 and 2.4.
  • Extraction: invoice XML, named attachments, full attachment list including Modification Date and MIME type.
  • Validation: BR-HYBRID-01 through BR-HYBRID-15 (and the BR-HYBRID-DE-/-FR- country variants) plus PDF/A-3 conformance via the IPdfA3ValidatorSPI SPI implemented by kaltblut-verapdf using veraPDF (-jakarta artifact line, JAXB 4.x only).
  • Command-line client with subcommands inspect, extract, attachments, validate.

My personal Coding Styleguide | It is appreciated if you star the GitHub project if you like it.