Dokumentation
PDFs erstellen
PDFs in Vue
PDFs in Nuxt
PDFs in React
PDFs in Angular
Seiten
Ein Document enthält eine oder mehrere Pages. Jede Seite legt ihre eigene Größe, Orientation und
Margins fest, und ist ihr Inhalt höher als die Seite, verteilt jasy ihn von selbst auf so viele Seiten, wie
er braucht.
Page({ size: "A4", orientation: "portrait", margin: 56 }, [Text("One page.")]);
| Prop | Typ | Was es tut |
|---|---|---|
size | PageSize oder ein Name wie "A4", "letter" | das Seitenformat (Standard A4) |
orientation | "portrait" | "landscape" | dreht das Format (Standard Portrait) |
margin | Insets | der Abstand des Inhalts zum Rand (Standard 56pt rundum) |
header | ein Element | oben auf jeder Seite gezeichnet |
footer | ein Element | unten auf jeder Seite gezeichnet |
Eine Page nimmt außerdem die Optionen von Column (gap, justify, align),
denn ihre Kinder werden in einer Spalte gestapelt.
Eine eigene Größe
Nicht jedes Dokument ist A-irgendwas. Ein Etikett, ein Ticket oder ein Regalschild bekommt seine Maße
direkt - in Punkt oder mit dem Helfer mm in Millimetern:
import { mm } from "@jasy/pdf";
Page({ size: mm(50, 65), margin: 4 }, [Text("A 50 x 65 mm label")]);
Page({ size: { width: 200, height: 120 }, margin: 8 }, [Text("200 x 120 points")]);
Alles andere folgt daraus: Page-Box, Content-Box und die Umrechnung der Koordinaten verwenden die Größe, die Sie angegeben haben, Header, Footer und Pagination verhalten sich also genau wie bei einem benannten Format.
Viele Seiten
Geben Sie Document eine Liste von Seiten. Jede kann anders sein, Größen und Orientations lassen sich
frei mischen.
Document([
Page({ size: "A4" }, [Text("Cover")]),
Page({ size: "A5", orientation: "landscape" }, [Text("A landscape insert")]),
]);
Header, Footer und Pagination
header und footer werden einmal angeordnet und auf jeder Seite neu gedruckt, über die der Inhalt
läuft, eine lange Tabelle oder ein langer Stapel Absätze behält also durchgehend Header und
Footer. Sie schreiben den Inhalt einmal; jasy paginiert.
import { writeFileSync } from "node:fs";
import { Document, Page, Column, Row, Text, renderToBytes } from "@jasy/pdf";
const header = Row({ justify: "between" }, [
Text("Acme Inc.", { bold: true, color: "#1450aa" }),
Text("Report"),
]);
const footer = Row({ justify: "between" }, [
Text("Confidential", { size: 9, color: "gray" }),
Text("2026", { size: 9, color: "gray" }),
]);
async function build() {
const body = Array.from({ length: 60 }, (_, i) =>
Text(`Paragraph ${i + 1} - long body text that flows across pages.`, { size: 12 }),
);
const doc = Document([
Page({ size: "A4", margin: 56, header, footer }, [Column({ gap: 6 }, body)]),
]);
writeFileSync("report.pdf", await renderToBytes(doc));
}
build();
Die 60 Absätze sind höher als eine Seite, das Ergebnis hat also mehrere Seiten, jede mit demselben Header und Footer.
Page Breaks
jasy paginiert von selbst, aber manchmal wollen Sie bestimmen, wo ein Page Break fällt - einen Abschnitt auf
einer neuen Seite beginnen oder verhindern, dass ein Block an der Seitengrenze geteilt wird. Das entspricht
den CSS-Eigenschaften break-before / break-after / break-inside.
Eine neue Seite erzwingen
PageBreak() schickt alles, was danach kommt, auf eine neue Seite, egal wie viel Platz noch ist.
import { Document, Page, Column, Text, PageBreak } from "@jasy/pdf";
Column([Text("Chapter one ends here."), PageBreak(), Text("Chapter two starts on a new page.")]);
Break vor oder nach einem Element
Dasselbe als Prop - setzen Sie breakBefore oder breakAfter an eine beliebige Box, Column oder
Row (CSS break-before / break-after: page):
Column([
Text("Summary"),
Box({ breakBefore: true, padding: 12 }, [Text("Appendix - always starts its own page")]),
]);
Ein breakBefore ganz oben auf einer Seite wird ignoriert, Sie bekommen also nie eine verirrte leere
Seite.
Einen Block zusammenhalten
keepTogether verhindert, dass eine Gruppe an einer Seitengrenze geteilt wird (CSS break-inside: avoid). Passt die Gruppe nicht in den verbleibenden Platz, aber auf eine neue Seite, wandert sie als
Ganzes dorthin; ist sie höher als eine ganze Seite, wird sie trotzdem geteilt, damit die Pagination
immer zu Ende kommt. Verschachteln funktioniert - ein inneres keepTogether hält auch dann noch, wenn ein
äußeres nachgeben muss.
import { keepTogether } from "@jasy/pdf";
keepTogether([
Text("Invoice total", { bold: true }),
Divider(),
Text("Subtotal 100.00"),
Text("Tax 19.00"),
Text("Total 119.00", { bold: true }),
]);
// or as a prop: Box({ keepTogether: true, padding: 12 }, [ /* ... */ ])
Alle vier gibt es auch in @jasy/vue und @jasy/nuxt: <PageBreak />, <KeepTogether> und die Props
break-before / break-after / keep-together. In @jasy/react heißen sie <PageBreak />,
<KeepTogether> und breakBefore / breakAfter / keepTogether.
Orphans und Widows
Ein Orphan ist die erste Zeile eines Absatzes, die allein unten auf einer Seite stehen bleibt; eine Widow ist die letzte Zeile, die allein oben auf die nächste geschoben wird. jasy beachtet beides, mit den Standards von CSS, jeweils 2:
Text(longParagraph, { orphans: 3, widows: 3 });
Text(longParagraph, { orphans: 1, widows: 1 }); // switch the protection off
Beide sind eine ganze Zahl von Zeilen, mindestens 1 - alles andere wird mit Namen abgelehnt. Anders als in CSS werden sie nicht vererbt: Eine Umbruchregel gilt pro Absatz. (Browser unterstützen das uneinheitlich - Chrome beachtet sie bei Paged Media, Firefox hat sie nie umgesetzt -, und genau deshalb muss eine PDF-Engine es selbst tun.)
Seitenzahlen
PageNumber() und PageCount() drucken die aktuelle Seite und die Gesamtzahl des Dokuments. Setzen Sie
sie, wohin Sie wollen - der Footer ist der übliche Ort, aber nichts hindert Sie, sie in den Inhalt, in
eine Tabellenzelle oder fünfmal auf eine Seite zu setzen.
Page(
{ footer: Row({ justify: "center" }, [Text("Page "), PageNumber(), Text(" of "), PageCount()]) },
[Column(body)],
);
Beide nehmen die üblichen Optionen von Text plus ein offset, das zur Zahl addiert
wird. Nützlich, wenn ein Deckblatt nicht mitzählen soll:
PageNumber({ offset: -1, size: 9, color: "gray" });
Nummerierung innerhalb eines Abschnitts
Eine lange Page, die über mehrere Blätter läuft, kann sich selbst ebenso nummerieren wie das Dokument.
SubPageNumber() und SubPageCount() zählen innerhalb dieser einen logischen Seite, eine Rechnung mit
Anlage kann also „Anlage, Seite 1 von 2“ neben „Blatt 3 von 4“ in den Footer setzen:
Row({ justify: "between" }, [
Row({}, [Text("Page "), PageNumber(), Text(" of "), PageCount()]),
Row({}, [Text("Sheet "), SubPageNumber(), Text(" of "), SubPageCount()]),
]);
Eigene bauen
Hinter beiden steckt ein Primitive: PageBuilder. Er reicht Ihnen die Daten der Seite und zeichnet, was Sie
zurückgeben, Formatierung und Bedingungen sind also einfaches JavaScript.
import { Document, Page, Column, Row, Text, PageBuilder } from "@jasy/pdf";
const doc = Document([
Page(
{
// A full title on page 1, a quiet continuation line afterwards.
header: PageBuilder(({ pageNumber }) =>
pageNumber === 1
? Text("Acme Inc. - Annual Report", { size: 16, bold: true })
: Text("Annual Report (continued)", { size: 9, color: "gray" }),
),
footer: PageBuilder(({ pageNumber, pageCount }) =>
Text(`Page ${pageNumber} of ${pageCount}`, { size: 9, align: "center" }),
),
},
[Column(body)],
),
]);
Die Funktion bekommt pageNumber (ab 1, gezählt werden physische Seiten, eine überlaufende Page
trägt also mehrere bei), pageCount (die Gesamtzahl des Dokuments) und pageSize (die MediaBox in
Punkt). In @jasy/react nimmt <PageBuilder> diese Funktion als Kind - siehe
Komponenten.
Damit pageCount stimmt, bricht jasy das ganze Dokument um, bevor es irgendetwas zeichnet: Die
Gesamtzahl muss feststehen, bevor Seite 1 gemalt wird. Diese Reihenfolge bringt zwei Dinge mit sich, die
man kennen sollte, und beide folgen aus demselben Henne-Ei-Problem - der Inhalt bestimmt die Zahl, die der
Inhalt anzeigt.
Ein bedingter Header darf auf späteren Seiten schrumpfen, nie wachsen. Der Bereich für den Inhalt wird am ersten Aufbau gemessen, ein höherer Header auf Seite 3 würde also den Inhalt zusammendrücken, für den schon Platz bemessen war. Schrumpfen macht nur Platz frei, das schadet nicht.
Im fließenden Inhalt wird die Box reserviert, bevor die Gesamtzahl bekannt ist, ein deutlich breiterer
endgültiger String ("9 of 10", wo "1 of 1" gemessen wurde) kann also etwas darüber hinaus zeichnen.
Halten Sie dynamischen Inhalt im Fließtext ungefähr gleich breit. In Header und Footer stimmen die Zahlen
immer genau.