Documentation

Output options

renderToBytes(doc, options) turns a document into bytes. Everything on this page is optional - renderToBytes(doc) on its own already produces a valid PDF.

import { renderToBytes } from "@jasy/pdf";

const bytes = await renderToBytes(doc, {
  title: "Q3 report",
  accessible: true,
});

Files inside the PDF

A PDF can carry other files. That is how a ZUGFeRD invoice hides its XML inside the page a human reads, and you can use it for anything else: the CSV a table was built from, the contract a quote refers to, the source of a chart.

const bytes = await renderToBytes(doc, {
  attachments: [
    {
      name: "figures.csv",
      data: new TextEncoder().encode("month,revenue\n2026-06,42000\n"),
      mimeType: "text/csv",
      relationship: "Source",
      description: "The numbers behind the chart on page 2",
    },
  ],
});
FieldMeaning
namethe file name a reader shows, e.g. "figures.csv"
datathe bytes (Uint8Array)
mimeTypee.g. "text/csv"; default "application/octet-stream"
relationship/AFRelationship: "Data", "Source", "Alternative", "Supplement"
descriptionfree text shown next to the file

relationship says what the file IS to the document rather than how it is stored, and a validator checks it: ZUGFeRD requires "Data" for its factur-x.xml, because the XML is the invoice, not a supplement to it. Attachments are written as associated files, which is what PDF/A-3 and PDF 2.0 require - a plain /EmbeddedFiles entry would not satisfy either.

Building a PDF/A yourself

@jasy/e-invoice does all of this for you, and for an invoice you should let it. These options exist because the same machinery is useful for any archival PDF - a contract, a report that has to survive a decade:

const bytes = await renderToBytes(doc, {
  pdfVersion: "1.7", // PDF/A-3 needs it; default "1.4"
  documentId: true, // a trailer /ID, which PDF/A requires
  outputIntent: sRGBProfileBytes, // an ICC profile, so colour is defined
  xmp: xmpPacket, // the XMP that DECLARES the conformance level
  standardFonts: false, // no Helvetica: PDF/A wants every font embedded
  fonts: { Helvetica: myEmbeddedFace },
});

Two of those are easy to get wrong:

  • standardFonts: false is the point, not a detail. PDF/A forbids relying on the reader's fonts, and the standard 14 are exactly that. Turning them off means every font name you use must arrive through fonts - which is why @jasy/e-invoice ships the Liberation faces under the names Helvetica, Times and Courier.
  • xmp is what a validator reads. The conformance level lives in the XMP packet, not in the structure. Getting the structure right and the packet wrong produces a file that claims nothing.

documentId writes a deterministic /ID: it is a hash of the objects, not a random number and not the clock. That is what lets the same document render byte-identically twice, so an archived file stays re-derivable years later.

Typography and size

OptionDefaultWhat it does
fonts-embedded faces, keyed by the name used in Text({ font })
kerningtruespace glyph pairs from the font's own data; false is the older output
compresstrueFlateDecode the streams; false gives a greppable, readable PDF

compress: false is a debugging tool worth knowing about: the content stream becomes plain text, so you can open the PDF in an editor and read the operators jasy wrote.

Telling you what happened

const bytes = await renderToBytes(doc, {
  onOverflow: "warn",
  onMissingGlyphs: (chars) => report(`not drawable: ${chars.join(" ")}`),
});
  • onOverflow - what to do when a single block is taller than a whole page and cannot be broken. "error" (default) throws, "warn" logs, "ignore" is silent. It is clipped either way: the alternative is a pagination loop that never ends.
  • onMissingGlyphs - called once with any characters no font could draw. They are removed rather than written as the font's .notdef box, which PDF/A forbids. Deliberately a callback and not a policy: a missing tick mark must never stop an invoice from being produced, but your application should be able to say which character went missing.

Metadata and access

OptionWhat it does
titlethe document title - used by readers, and required for a conformant tagged PDF
langdocument language, e.g. "de-DE" (default "en-US")
accessibleemit a tagged structure tree - see Accessibility
encryptAES-256 password protection - see Encryption

encrypt and PDF/A are mutually exclusive, and jasy says so rather than producing a file that fails validation later: PDF/A forbids encryption, so encrypting a ZUGFeRD invoice throws.

jasypdf

Declarative PDFs in pure TypeScript. ZUGFeRD & XRechnung compliant, with no headless browser and no Java.

Resources

© 2026 Florian Heuberger · MIT License

Built with Nuxt · self-hosted fonts · no trackers