PDF Generation and E-Invoicing with FlyingSaucer 10

Torsten Uhlmann

Mon, 19 Jan 2026


Role: You are a senior software engineer writing a personal retrospective (devlog) about building an “Invoicer” application.

Tone:

Reflective & Honest: Focus on the “why” and the struggle. Do not sound like a marketing brochure.

No Fluff: Avoid words like “game-changer,” “revolutionary,” “seamless,” or “delve.”

Technical but Narrative: Explain the tech through the story of discovering it, not lecturing about it.

Write between 1800 and 2200 words.

The Story Arc (Context):

PDF/A-1B creation

  • Mustang e-invoice library for embedding XML requires PDF/A-1B containers to create combined PDF/XML that would be validated as PDF/A-3U (Mustang can ignore container errors, but the resulting document would not validate, even though it would contain the XML)

PDF/A-3U is the PDF standard for long time archival of documents. As such it has to look identical

things needed

  • flying saucer 10, openpdf 3
  • embed fonts into PDF
  • embed ICC color profile
  • set PDF standard to 1.4
  • do not use XRef streams
  • write proper meta data

Analysis how its implemented

Based on my research of the codebase, here’s a comprehensive breakdown of the steps required to make Flying Saucer generate PDF/A-1b compliant documents and feed them into the Mustang library for creating archival PDFs. I’ll document the exact process as implemented in the current codebase.

Code Changes for PDF/A-1b Generation with Flying Saucer

1. Configure PDF Writer for PDF/A-1b Conformance

The first step is to configure the ITextRenderer from Flying Saucer to produce PDF/A-1b compliant output:

val renderer = ITextRenderer()
// Set PDF/A-1b conformance level
renderer.pdfxConformance = PdfWriter.PDFA1B
// Disable compression (required for PDF/A compliance)
renderer.setCompressionEnabled(false)

2. Embed All Fonts Used in the Document

PDF/A-1b requires all fonts to be embedded in the PDF. The codebase extracts IBM Plex Sans font variants from resources and registers them with Flying Saucer’s font resolver:

private fun embedFonts(renderer: ITextRenderer) {
  val fontsDir = ensureFontsExtracted()
  val fontResolver = renderer.fontResolver

  val fontFiles = fontsDir.listFiles { file ->
    file.isFile && file.extension == "ttf"
  } ?: emptyArray()

  for (fontFile in fontFiles) {
    fontResolver.addFont(fontFile.absolutePath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED)
  }
}

The fonts extracted include:

  • IBMPlexSans-Regular.ttf
  • IBMPlexSans-Bold.ttf
  • IBMPlexSans-Italic.ttf
  • IBMPlexSans-BoldItalic.ttf
  • And additional variants (ExtraLight, Light, Medium, SemiBold, Thin, etc.)

3. Set Output Intents with ICC Color Profile

Implement a custom PDFCreationListener that sets the required output intents using an sRGB ICC color profile:

class PdfA1bCreationListener : DefaultPDFCreationListener() {
  private fun configurePreWrite(writer: PdfWriter) {
    val colorProfileFile = ensureColorProfileExtracted()
    if (colorProfileFile != null && colorProfileFile.exists()) {
      val iccProfile = java.awt.color.ICC_Profile.getInstance(colorProfileFile.inputStream())
      writer.setOutputIntents(
        "sRGB IEC61966-2.1", // outputConditionIdentifier
        "", // outputCondition (empty for sRGB)
        "http://www.color.org", // registryName
        "sRGB IEC61966-2.1", // info
        iccProfile // colorProfile
      )
    }
  }

  private fun configurePreClose(writer: PdfWriter) {
    writer.createXmpMetadata()
  }

  override fun preWrite(renderer: ITextRenderer, pageCount: Int) {
    renderer.writer?.let { configurePreWrite(it) }
    super.preWrite(renderer, pageCount)
  }

  override fun onClose(renderer: ITextRenderer) {
    renderer.writer?.let { configurePreClose(it) }
    super.onClose(renderer)
  }
}

The color profile used is sRGB2014.icc extracted from /color/sRGB2014.icc in the application resources.

4. Attach the Listener to the Renderer

Set the custom listener on the renderer before generating the PDF:

val pdfCreationListener = PdfA1bCreationListener()
renderer.setListener(pdfCreationListener)

5. Handle Image Transparency

PDF/A-1b doesn’t support images with alpha channels. The codebase includes image validation that flattens transparent images to a white background:

// Validate and flatten images for PDF/A-1b compatibility
val processedImage = validateImageForPdfA1b(imageBytes)

6. Generate the PDF

With all configurations in place, generate the PDF:

renderer.setDocument(doc, null)
renderer.layout()
renderer.createPDF(outputStream)

Creating Archival PDFs with Mustang Library

1. Initialize ZUGFeRD Exporter

Use ZUGFeRDExporterFromA1 from the Mustang library:

val exporter = ZUGFeRDExporterFromA1()

// Configure metadata
exporter.setProducer("Your Application Name")
exporter.setCreator("Your Application Name")

// Allow non-PDF/A input (though we provide PDF/A-1b)
exporter.ignorePDFAErrors()

2. Load the PDF/A-1b Document

exporter.load(pdfContent)

3. Set the Target Profile

For German e-invoicing compliance, use the XRechnung profile:

exporter.setProfile(Profiles.getByName("XRechnung"))

4. Set Transaction Data

Create a Mustang Invoice object with all the invoice data and set it:

val mustangInvoice = createMustangInvoice(invoiceData)
exporter.setTransaction(mustangInvoice)

5. Export the Archival PDF

exporter.export(outputFile.absolutePath)

Exact Specification for the Archival PDF

The resulting archival PDF conforms to ZUGFeRD/Factur-X based on EN 16931 (the European e-invoicing standard). Specifically:

  • Format: PDF/A-3 (which allows file attachments)
  • Embedded Data: XML invoice data in CII (Cross Industry Invoice) format
  • Profile: XRechnung (German implementation of EN 16931)
  • Compliance: Meets German GoBD (Grundsätze ordnungsmäßiger Buchführung) requirements for electronic archiving

The XRechnung profile ensures compliance with:

  • European e-invoicing directive requirements
  • PEPPOL BIS 3.0 specifications
  • German public sector procurement standards

This hybrid format provides both human-readable PDF content and machine-readable XML data in a single, tamper-evident file suitable for long-term archival and automated processing.