Skip to content

Proposal: @node-zugferd/pdf — programmatic PDF generation with a built-in German (§ 14 UStG compliant) invoice template #119

Description

@Fejir62

Motivation

node-zugferd currently covers two of the three steps needed to produce a complete e-invoice:

  1. ✅ Generate Factur-X/ZUGFeRD XML from typed data (invoice.toXML())
  2. Produce the visual PDF itself
  3. ✅ Embed the XML and apply PDF/A-3b conformance (invoice.embedInPdf(pdf))

For step 2, users must either bring their own PDF or set up @node-zugferd/api, which requires an HTTP server, a self-written template, and Puppeteer (a full Chromium download — slow cold starts and problematic on serverless platforms).

I'd like to contribute a new plugin package that closes this gap with a single programmatic call — no server, no Chromium — including a built-in, customizable default template whose primary goal is a legally compliant German invoice (all mandatory content of § 14 Abs. 4 UStG, DIN 5008-inspired layout).

Proposed API

import { zugferd } from "node-zugferd";
import { EN16931 } from "node-zugferd/profile/en16931";
import { pdf } from "@node-zugferd/pdf";
import { defaultTemplate } from "@node-zugferd/pdf/templates";

export const invoicer = zugferd({
  profile: EN16931,
  plugins: [
    pdf({
      template: defaultTemplate({
        logo: fs.readFileSync("./logo.png"),
        accentColor: "#1e3a5f",
        locale: "de",
        footer: {
          columns: [
            "Lieferant GmbH\nLieferantenstraße 20\n80333 München",
            "Geschäftsführer: Hans Muster\nAmtsgericht München HRB 12345",
            "IBAN: DE02 1203 0000 0000 2020 51",
          ],
        },
      }),
    }),
  ],
});

// data → rendered PDF → embedded XML → PDF/A-3b, in one call:
const pdfA = await invoicer.createPdf(data, { metadata: { title: "Rechnung 471102" } });

The plugin registers createPdf through the existing ZugferdPlugin mechanism, so it fits the current architecture without core changes.

How it works

data
 └─▶ template(ctx)                    → pdfmake TDocumentDefinitions
      └─▶ pdfmake printer             → PDF buffer (fonts embedded)
           └─▶ document.create(data).embedInPdf(buffer, opts)
                → Uint8Array (validated XML embedded, PDF/A-3b metadata applied)

Rendering is done with pdfmake (pure JS, MIT): declarative document definitions, automatic page breaks for long line-item tables, no native modules.

Key design points

  • Germany-first default template: renders all § 14 Abs. 4 UStG mandatory content (seller/buyer, VAT ID / tax number, invoice number, dates, line items, per-rate VAT breakdown incl. exemption reasons, totals, payment terms). Rendered exclusively from the same data that produces the XML, so the visual PDF can never contradict the embedded XML.
  • Three customization tiers: simple options (logo, colors, de/en labels, footer) → override individual sections (header, line table, totals, …) → replace the template entirely (any function returning a pdfmake docDefinition).
  • PDF/A-safe by construction: fonts are always embedded (pdfmake's non-embedded standard-14 fonts path is not used), so embedInPdf's PDF/A-3b post-processing stays valid.
  • Profile support: the default template requires BASIC or higher (matching the README's guidance that MINIMUM/BASIC WL aren't valid invoices under German law); custom templates are unrestricted.
  • Quality: Vitest tests next to sources (formatters, template content against the existing test fixtures, full-pipeline integration), best-effort veraPDF conformance check, docs page under docs/content/docs/plugins/ mirroring the API plugin docs.
  • Non-goal: replacing @node-zugferd/api — HTML/CSS templates via Puppeteer remain the right choice for pixel-perfect corporate designs. This plugin optimizes for zero-setup DX.

Open questions

  1. Package name: @node-zugferd/pdf or @node-zugferd/template?
  2. Is pdfmake acceptable as a dependency (pure JS, MIT)? The alternative — hand-rolled layout on top of the existing pdf-lib dependency — avoids the dep but means a lot more code (line breaking, table pagination, …).
  3. Handler name createPdf ok?
  4. Bundled default font: reuse Roboto (already inside pdfmake) or Liberation Sans?
  5. Longer term, should the template contract live somewhere shared so @node-zugferd/api could consume the same templates?

Offer

I'd like to implement this myself and submit it as a PR (package, tests, and docs included). I have a detailed design document ready and I'm happy to adjust any of the above to your preferences before starting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions