Dokumentation

Ausgabeoptionen

renderToBytes(doc, options) macht aus einem Dokument Bytes. Alles auf dieser Seite ist optional - renderToBytes(doc) allein erzeugt schon ein gültiges PDF.

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

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

Dateien im PDF

Ein PDF kann andere Dateien mitführen. So versteckt eine ZUGFeRD-Rechnung ihr XML in der Seite, die ein Mensch liest, und Sie können es für alles andere nutzen: die CSV, aus der eine Tabelle gebaut wurde, den Vertrag, auf den sich ein Angebot bezieht, die Quelle eines Diagramms.

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",
    },
  ],
});
FeldBedeutung
nameder Dateiname, den ein Reader anzeigt, z. B. "figures.csv"
datadie Bytes (Uint8Array)
mimeTypez. B. "text/csv"; Standard "application/octet-stream"
relationship/AFRelationship: "Data", "Source", "Alternative", "Supplement"
descriptionfreier Text, der neben der Datei angezeigt wird

relationship sagt, was die Datei für das Dokument IST, nicht wie sie gespeichert ist, und ein Validator prüft es: ZUGFeRD verlangt "Data" für seine factur-x.xml, denn das XML ist die Rechnung und keine Ergänzung dazu. Anhänge werden als Associated Files geschrieben, so wie es PDF/A-3 und PDF 2.0 verlangen - ein einfacher Eintrag in /EmbeddedFiles würde keinem von beiden genügen.

Zurücklesen seit beta.4

readAttachments listet jede Datei, die ein PDF mitführt - von jedem Erzeuger, nicht nur von jasy:

import { PdfDocument, readAttachments } from "@jasy/pdf/edit";

const doc = await PdfDocument.open(bytes, { password }); // password only if it is encrypted
for (const file of readAttachments(doc)) {
  console.log(file.name, file.mimeType, file.relationship, file.locations);
  const data = file.bytes(); // or file.text() for XML, CSV, ...
}

Es findet eine Datei überall, wo ein PDF eine ablegen kann - in der Anhangsliste des Dokuments, in den Associated Files von PDF/A-3 und in File Attachment Annotations auf einer Seite -, und locations sagt, wo jede hängt. Eine Datei, die an zwei Stellen aufgeführt ist, ist ein Eintrag; zwei Dateien mit demselben Namen sind zwei, was mit einem doppelten Namen geschieht, entscheiden also Sie. bytes() gibt genau zurück, was eingebettet wurde: entschlüsselt, jeder Standardfilter dekodiert und gegen die eigene Prüfsumme der Datei geprüft. Daten, die nicht dazu passen, oder ein Filter, der sich nicht dekodieren lässt, werfen einen AttachmentError, statt etwas Ähnliches zurückzugeben.

Ein PDF/A selbst bauen

@jasy/e-invoice erledigt all das für Sie, und bei einer Rechnung sollten Sie es ihm überlassen. Diese Optionen gibt es, weil dieselbe Mechanik für jedes Archiv-PDF nützlich ist - einen Vertrag, einen Bericht, der ein Jahrzehnt überstehen muss:

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 color is defined
  xmp: xmpPacket, // the XMP that DECLARES the conformance level
  standardFonts: false, // no Helvetica: PDF/A wants every font embedded
  fonts: { Helvetica: myEmbeddedFace },
});

Zwei davon macht man leicht falsch:

  • standardFonts: false ist der Kern, kein Detail. PDF/A verbietet, sich auf die Schriften des Readers zu verlassen, und die 14 Standardschriften sind genau das. Schalten Sie sie ab, muss jeder Schriftname, den Sie verwenden, über fonts kommen - deshalb liefert @jasy/e-invoice die Liberation-Schriften unter den Namen Helvetica, Times und Courier aus.
  • xmp ist das, was ein Validator liest. Das Conformance Level steht im XMP-Packet, nicht in der Struktur. Eine richtige Struktur mit falschem Packet ergibt eine Datei, die nichts behauptet.

documentId schreibt eine deterministische /ID: Sie ist ein Hash der Objekte, keine Zufallszahl und nicht die Uhrzeit. Nur so rendert dasselbe Dokument zweimal Byte für Byte gleich, und eine archivierte Datei bleibt auch Jahre später neu herleitbar.

Typografie und Größe

OptionStandardWas es tut
fonts-eingebettete Schriften, nach dem Namen, der in Text({ font }) steht
kerningtrueGlyph-Paare nach den eigenen Daten der Schrift ausgleichen; false ist die ältere Ausgabe
compresstruedie Streams mit FlateDecode komprimieren; false ergibt ein lesbares, durchsuchbares PDF

compress: false ist ein Werkzeug zur Fehlersuche, das man kennen sollte: Der Content Stream wird zu Klartext, Sie können das PDF also in einem Editor öffnen und die Operatoren lesen, die jasy geschrieben hat.

Vorab laden seit beta.7

Die Metriken der Standardschriften sind nicht im Bundle, mit dem Ihre App startet. Sie laden mit dem ersten Rendern (158 KB minifiziert, 12 KB mit gzip), und jedes weitere Rendern findet sie geladen vor. Um diese Wartezeit vom ersten PDF zu nehmen, laden Sie sie früher - sobald die App untätig ist, oder wenn der Zeiger einen Download-Button erreicht:

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

requestIdleCallback(() => void preloadStandardFonts());

Der Aufruf darf beliebig oft kommen, und ein fehlgeschlagenes Laden wird beim nächsten Aufruf wiederholt.

Was passiert ist

const bytes = await renderToBytes(doc, {
  onOverflow: "warn",
  onMissingGlyphs: (chars) => report(`not drawable: ${chars.join(" ")}`),
  onDiagnostic: (d) => report(`${d.code} in ${d.element} "${d.text}" on page ${d.page}`),
});
  • onOverflow - was geschehen soll, wenn ein einzelner Block höher als eine ganze Seite ist und sich nicht teilen lässt. "error" (Standard) wirft, "warn" protokolliert, "ignore" schweigt. Abgeschnitten wird in jedem Fall: Die Alternative wäre eine Pagination, die nie endet.
  • onMissingGlyphs - wird einmal mit allen Zeichen aufgerufen, die keine Schrift zeichnen konnte. Sie werden entfernt, statt als .notdef-Kästchen der Schrift geschrieben zu werden, was PDF/A verbietet. Absichtlich ein Callback und keine Regel: Ein fehlendes Häkchen darf nie verhindern, dass eine Rechnung entsteht, aber Ihre Anwendung soll sagen können, welches Zeichen fehlte.
  • onDiagnostic seit beta.4 - ein Kanal für alles, was jasy über ein Dokument zu sagen hat, das es trotzdem gerendert hat. Jeder Befund hat einen stabilen code, das Element so, wie Sie es geschrieben haben ("Box", "Text"), seinen ersten Text und die Seite:
    codeBedeutung
    page-break-ignoredein PageBreak in einer Row oder einem PageBuilder, die nie auf Seiten umbrechen
    column-break-ignored seit beta.5ein ColumnBreak außerhalb der Spalten eines Flow
    break-ignoredbreakBefore / breakAfter an etwas in einer Row oder einer Tabellenzeile
    keep-together-spliteine keepTogether-Gruppe enthält einen erzwungenen Umbruch; der Umbruch gewinnt
    overflow-clippedein Block, höher als eine Seite, der sich nicht teilen lässt (mit onOverflow nicht "error")
    overflow-wide seit beta.5ein Wort, das sich nicht umbrechen lässt, oder eine Row, deren Kinder mehr Platz brauchen, über den Rand gezeichnet
    missing-glyphsZeichen, die keine Schrift zeichnen konnte, pro Text
    image-unsized seit beta.5ein Bild, dessen eigene Größe beim Layout unbekannt war - eine URL, die erst in einem PageBuilder entsteht

    Ohne Handler ändert sich nichts: Was vorher gewarnt hat, geht weiterhin auf die Konsole.

Metadaten und Zugriff

OptionWas es tut
titleder Titel des Dokuments - von Readern verwendet, und Pflicht für ein konformes getaggtes PDF
langdie Sprache des Dokuments, z. B. "de-DE" (Standard "en-US")
accessibleeinen getaggten Structure Tree erzeugen - siehe Barrierefreiheit
encryptPasswortschutz mit AES-256 - siehe Verschlüsselung

encrypt und PDF/A schließen sich gegenseitig aus, und jasy sagt das, statt eine Datei zu erzeugen, die später an der Validierung scheitert: PDF/A verbietet Verschlüsselung, das Verschlüsseln einer ZUGFeRD-Rechnung wirft also einen Fehler.