Dokumentation
PDFs erstellen
PDFs in Vue
PDFs in Nuxt
PDFs in React
PDFs in Angular
Text
Text bringt Wörter aufs Papier. Übergeben Sie einen String und ein paar Optionen, und die Engine bricht
ihn auf die verfügbare Breite um, an Wortgrenzen und mit den echten Schriftmetriken. Sie messen nie etwas
selbst.
Wann bricht Text um?
Ein Text bricht auf die Breite um, die er bekommt - die Inhaltsbreite der Seite in einer Page oder
Column, oder die Breite einer Box({ width }), eines Expanded oder einer Tabellenzelle, wenn er darin
steht. Ganz ohne Breitenbegrenzung (als einfaches Kind einer Row) bleibt er einzeilig, denn es gibt
nichts, woran er umbrechen könnte.
Gestaltung
Das zweite Argument setzt den Stil. Alles ist optional und hat einen sinnvollen Standard.
Text("Big and bold", { size: 26, bold: true, color: "#1450aa" });
Text("A quiet note", { size: 12, italic: true, color: "gray" });
| Prop | Typ | Was es tut |
|---|---|---|
size | number | Schriftgröße in Punkt (Standard 12) |
bold | boolean | fett |
italic | boolean | kursiv |
color | ColorInput | Textfarbe (jede der Farbformen) |
font | string | Schriftfamilie (Standard "Helvetica"; siehe Schriften) |
align | "left" | "center" | "right" | "justify" | Ausrichtung im Textblock (Standard: der Anfang - links, in rtl rechts) |
lineHeight | number | Faktor für die Line-Height; nicht gesetzt = die natürliche Line-Height der Schrift |
underline | boolean | unterstreichen, an der Position und in der Stärke, die die Schrift angibt |
strikethrough | boolean | eine Linie durch den Text ziehen, auf halber x-Höhe |
skipInk | boolean | die Unterstreichung um Descender herumführen (braucht eine eingebettete Schrift) |
letterSpacing | number | zusätzlicher Abstand nach jedem Zeichen, in Punkt (negativ verengt) |
wordSpacing | number | zusätzlicher Vorschub an jedem Leerzeichen, in Punkt (CSS word-spacing) |
textIndent | number | wie weit die ERSTE Zeile eingerückt beginnt, in Punkt |
textTransform | "uppercase" | "lowercase" | "capitalize" | Groß-/Kleinschreibung ändern (CSS text-transform) |
ligatures | boolean | die Ligaturen fi / fl / ffi der Schrift (Standard an) |
breakWord | boolean | ein Wort, das breiter als seine Box ist, an beliebiger Stelle teilen, ohne Bindestrich |
hyphenate | (word) => string[] | an gültigen Stellen trennen und den Bindestrich zeichnen - die Patterns bringen Sie mit |
direction | "ltr" | "rtl" | Schreibrichtung; mit rtl beginnt die Zeile rechts |
maxLines | number | die Zahl der Zeilen begrenzen (Standard: unbegrenzt) |
overflow | "clip" | "ellipsis" | was nach maxLines passiert (Standard "clip") |
orphans | number | Mindestzahl an Zeilen, die vor einem Page Break stehen bleiben (Standard 2) |
widows | number | Mindestzahl an Zeilen, die auf die nächste Seite wandern (Standard 2) |
Ausrichtung
align setzt den Text innerhalb seines eigenen Blocks, unabhängig vom umgebenden Layout.
Text("Centered", { align: "center" });
Text("Right aligned", { align: "right" });
Text(longParagraph, { align: "justify" });
justify verteilt den freien Platz einer Zeile auf ihre Leerzeichen, sodass beide Kanten bündig
abschließen. Die letzte Zeile eines Absatzes behält ihre natürlichen Abstände, wie im Druck und in CSS.
Die Engine darf eine Zeile auch leicht stauchen - um bis zu ein Viertel Leerzeichen -, wenn dadurch
ein weiteres Wort darauf passt.
Line-Height
lineHeight ist ein Faktor auf den Zeilenabstand. Bleibt er ungesetzt, bekommt jede Zeile die eigene
natürliche Line-Height der Schrift - dasselbe, was CSS line-height: normal nennt. Text liest sich so
von Anfang an angenehm, und Großbuchstaben sitzen optisch mittig in ihrer Zeile. Eine Zahl überschreibt
das: 1 ist ein em (eng, Großbuchstaben berühren die Ränder womöglich gerade), 1.3 bis 1.5
öffnen Fließtext weiter. Wie groß der Wert auch ist, der zusätzliche Platz wird gleichmäßig über und unter
jeder Zeile verteilt.
Text("Comfortable by default - the font's natural line height.");
Text("Airier still, set once.", { lineHeight: 1.5 });
Underline, Strikethrough und Letter-Spacing
Underline, Strikethrough und Letter-Spacing nehmen ihre Maße aus der Schrift selbst, nie aus einer fest eingebauten Schätzung.
Text("Underlined", { underline: true });
Text("Struck through", { strikethrough: true });
Text("S P A C E D H E A D E R", { letterSpacing: 3, bold: true });
Text("tighter", { letterSpacing: -0.5 });
Eine Unterstreichung sitzt an der Position und in der Stärke, die die Schrift angibt, ein Strich pro
umbrochener Zeile, und eine Durchstreichung kreuzt auf halber x-Höhe - dort, wo ein Browser sie setzt.
letterSpacing fügt nach jedem Zeichen einen festen Abstand ein (ein negativer Wert verengt); er zählt
beim Umbruch und bei der Ausrichtung mit, Text mit Letter-Spacing bricht und richtet sich also trotzdem
korrekt aus. All das funktioniert auch an einem einzelnen span und vererbt sich
von Document / DefaultTextStyle.
skipInk führt die Unterstreichung wie ein Browser um Descender herum - dafür braucht es eine
eingebettete Schrift (die Standard-PDF-Schriften haben ihre Umrisse im Viewer, nicht im Dokument), und
wer es mit einer Standardschrift verlangt, bekommt einen Fehler statt eines stillen Nichtstuns.
Kerning und Ligaturen
Beides ist standardmäßig an, denn der Gestalter der Schrift hat es gezeichnet, und niemand sollte die Fachbegriffe kennen müssen, um sauber gesetzten Text zu bekommen.
Kerning wendet die Kerning-Paare der Schrift an und rückt Kombinationen wie AV, To und Wa
enger zusammen, damit Überschriften so wirken wie im Web und nicht in lose Blöcke zerfallen. Das gilt für
die Standardschriften und für eingebettete (die alte kern-Tabelle ebenso wie modernes OpenType GPOS).
Ligaturen verschmelzen fi, fl und ffi zu dem einen Glyph, den die Schrift dafür mitbringt,
gelesen aus ihrer GSUB-Tabelle. Nur bei eingebetteten Schriften - die 14 Standardschriften haben keine
solche Tabelle.
Text("office affluent film"); // ffi and fl become one glyph each
Text("office affluent film", { ligatures: false }); // opt out for this text
await renderToBytes(doc, { kerning: false }); // or turn kerning off document-wide
ligatures ist ein vererbbarer Textstil, er lässt sich also am Document, an einem Teilbaum mit
DefaultTextStyle oder an einem einzelnen Text setzen.
Ein Wort, das nicht passt
Standardmäßig läuft ein Wort, das breiter als seine Box ist, über, genau wie in CSS. Darauf bauen zwei Stufen auf, und keine ist an, solange Sie nicht danach fragen:
Text(email, { breakWord: true }); // split where the box ends, no hyphen
Text(german, { hyphenate }); // split at valid points, with a hyphen
breakWord ist die Untergrenze: Eine E-Mail-Adresse, eine IBAN oder eine Rechnungsnummer hat nirgends
einen gültigen Hyphenation-Punkt, und genau das verhindert, dass ein Pflichtfeld auf einer Rechnung über sein
eigenes Label gezeichnet wird.
hyphenate ist ein Hook, keine mitgelieferten Daten. Geben Sie ihm eine Funktion, die die Teile eines
Wortes zurückgibt, und jasy zeichnet den Bindestrich an der Trennung. Jede Bibliothek mit Hyphenation-Patterns
taugt dafür:
import { hyphenateSync } from "hyphen/de";
const hyphenate = (word: string) => hyphenateSync(word).split("\u00AD");
Text("Rechtsschutzversicherungsgesellschaften", { hyphenate, breakWord: true });
@jasy/pdf liefert keine Hyphenation-Patterns mit: Deutsch allein sind 732 KB, und ohne Kenntnis der Sprache zu
trennen ist schlechter, als gar nicht zu trennen. Der Standard ist eine Haltung, keine Lücke - wir trennen
erst, wenn Sie die Sprache nennen.
Right-to-Left und Arabisch
Setzen Sie direction: "rtl", und die Zeile beginnt rechts. Gemischter Text wird in jedem Fall nach
Unicode UAX #9 umgeordnet, Hebräisch in einem englischen Absatz kommt also auch ohne die Option richtig
heraus - die Option entscheidet, wo die ZEILE beginnt.
Text("שלום world", { direction: "rtl" });
Text("مرحبا بالعالم", { font: "Amiri", direction: "rtl" });
Arabische Buchstaben verbinden sich, gelesen aus der eigenen GSUB-Tabelle der Schrift, und Lam-Alif
verschmilzt zu einem Glyph. Dafür braucht es eine eingebettete arabische Schrift; kopierter Text kommt als
echtes Arabisch zurück, nicht als Presentation Forms.
Zeilenumbrüche
Ein \n im String ist ein harter Umbruch: Er bricht die Zeile auch dort um, wo noch Platz ist, denn er
ist eine Anweisung und kein Umbruch aus Platzgründen. \r\n und ein einzelnes \r bedeuten dasselbe, ein
Tabulator wird zum Leerzeichen, und unsichtbare Steuerzeichen werden entfernt - außer denen, die etwas
bewirken, wie der Zero-Width Joiner, den das arabische Shaping braucht.
Textstile vererben
Jede Option oben hat einen eingebauten Standard, aber size oder color wollen Sie selten in jeder Zeile
wiederholen. Setzen Sie sie einmal und lassen Sie sie vererben - genau wie in CSS oder mit Flutters
DefaultTextStyle.
Standards für das ganze Dokument
Übergeben Sie die Textoptionen an Document, und jeder Text erbt sie, solange er keine eigenen setzt.
So bekommt ein ganzes Dokument am saubersten eine einheitliche Schrift, Größe, Farbe und einen
einheitlichen Rhythmus.
Document({ font: "Times-Roman", size: 11, color: "#1f2a37", lineHeight: 1.5 }, [
Page({ size: "A4" }, [
Text("Inherits the serif, 11pt, dark ink and 1.5 line-height."),
Text("Sets nothing either - identical to the line above."),
Text("Overrides just the color; size, font and line-height still inherit.", {
color: "#c0392b",
}),
]),
]);
Eine Schrift für das, was die erste nicht kann
font nimmt auch eine Liste. Jedes Zeichen wird von der ersten Familie darin gezeichnet, die einen
Glyph dafür hat, gemischte Schriftsysteme funktionieren also, ohne den Text von Hand aufzuteilen:
Text("Hello 你好 مرحبا", { font: ["Inter", "NotoSansCJK", "Amiri"] });
Ein Zeichen, das keine Familie der Liste zeichnen kann, wird entfernt und gemeldet, statt als
.notdef-Kästchen ausgegeben zu werden - PDF/A verbietet, auf eines zu verweisen.
renderToBytes(doc, { onMissingGlyphs }) sagt Ihnen genau, welche Zeichen fehlen, damit ein verlorenes
Häkchen eine Rechnung nie unbemerkt aufhält.
Ein Standard für einen Abschnitt - DefaultTextStyle
Um statt des ganzen Dokuments einen Abschnitt neu vorzubelegen, umschließen Sie ihn mit
DefaultTextStyle. Seine Kinder erben die neuen Werte, aufgesetzt auf das, was schon von weiter oben
vererbt wurde.
DefaultTextStyle({ size: 9, color: "gray" }, [
Text("Small and gray - a footnote, a caption, a contact block."),
Text("Same here, nothing set on it."),
Text("Still 9pt, but overrides the color.", { color: "#1450aa" }),
]);
Um eine ganze Seite zu gestalten, machen Sie es zum Kind der Seite:
Page([ DefaultTextStyle({ … }, [ …content ]) ]).
Wie ein Wert aufgelöst wird
Für jede Eigenschaft gewinnt die spezifischste Quelle - und nur für diese Eigenschaft; die übrigen erben weiter (eine Zusammenführung pro Eigenschaft, wie in Flutter):
ausdrücklich am Text > nächster DefaultTextStyle > Standards des Document > eingebauter Standard
Nur Text-Eigenschaften werden vererbt - font, size, color, bold, italic, align,
lineHeight, dieselbe Menge, die CSS vererbt. Box- und Layout-Eigenschaften (Padding, Border,
Breite, Hintergrund) nie.
Langen Text kürzen
Standardmäßig bricht ein Text auf so viele Zeilen um, wie er braucht. Begrenzen Sie das mit maxLines,
und wählen Sie mit overflow, was mit dem Überschuss geschieht: "clip" schneidet hart ab, "ellipsis"
beendet die letzte Zeile mit .... Kürzen braucht eine begrenzte Breite - eine Column, eine
Box({ width }), ein Expanded oder eine Tabellenzelle.
Box({ width: 220 }, [
Text("Premium Wireless Noise-Cancelling Over-Ear Headphones with 40 Hour Battery Life", {
maxLines: 2,
overflow: "ellipsis",
}),
]);
maxLines: 1 begrenzt auf eine einzige Zeile - praktisch für einen langen Produktnamen in einer schmalen
Tabellenzelle.
Gemischte Stile in einer Zeile
Um den Stil mitten in einer Zeile zu wechseln, übergeben Sie statt eines Strings ein Array aus
span(...)-Stücken. Jeder span überschreibt nur die Felder, die er setzt, und erbt den Rest von den
Optionen des Text.
import { Text, span } from "@jasy/pdf";
Text(
[
span("This line mixes "),
span("bold", { bold: true }),
span(", "),
span("italic", { italic: true }),
span(" and "),
span("color", { color: "#1450aa", bold: true }),
span(" in one run."),
],
{ size: 14 },
);
span nimmt jeden Stil, der zu einem Textstück gehört geändert in beta.5 - Größe, Schrift,
Schriftstärke, Farbe, Linien und skipInk, letterSpacing, wordSpacing, textTransform, ligatures,
breakWord, hyphenate. Ein am Span gesetzter Stil gewinnt gegen den Text; ein nicht gesetzter wird
geerbt. Was zum ganzen Block gehört, bleibt am Text: align, lineHeight, maxLines, direction und
textIndent. Vor beta.5 nahm ein Span direction und textIndent an, ohne etwas damit zu tun, und
verlor mehrere der Stile für Textstücke. Zwei eigene bringt er mit:
Text([span("Total 1.190,00 EUR"), span("1", { verticalAlign: "super", size: 7 })]);
Text([span("Read the "), span("terms", { href: "https://example.com/terms" }), span(".")]);
verticalAlign: "super" | "sub" hebt ein Fußnotenzeichen an oder senkt einen Index ab - es verschiebt nur
die Grundlinie, geben Sie also auch eine kleinere size an, wenn es wie <sup> im Browser aussehen soll.
Und href / to verlinken nur dieses Stück statt des ganzen Textes.
Ein Wort, das über zwei Spans reicht, bricht mit breakWord dort, wo Chrome es bricht: Die Zeile wird
zuerst gefüllt, und ein Umbruch nach einem Zeichen ist erlaubt, wenn der Span dieses Zeichens ihn erlaubt.
hyphenate wird bei einem solchen Wort weiterhin für das Stück jedes Spans gefragt, nicht für das ganze
Wort (#113).
Absätze
Paragraph ist Text unter anderem Namen. Er verhält sich identisch, drückt aber die Absicht aus, wenn
Sie einen Block Fließtext meinen und kein Label und keine Überschrift.
Paragraph(
"Long text wraps automatically to fit its width, breaking on word boundaries using the real " +
"font metrics, so you never have to measure anything yourself.",
{ size: 12 },
);
Alles in einer Datei
Kopieren Sie das, führen Sie es mit npx tsx text-demo.ts aus, und öffnen Sie text.pdf.
import { writeFileSync } from "node:fs";
import { Document, Page, Column, Text, Paragraph, span, Divider, renderToBytes } from "@jasy/pdf";
async function build() {
const doc = Document([
Page({ size: "A4", margin: 56 }, [
Column({ gap: 12 }, [
Text("Big and bold", { size: 26, bold: true, color: "#1450aa" }),
Text("Italic and gray", { size: 14, italic: true, color: "gray" }),
Text("Centered", { size: 14, align: "center" }),
Text("Right aligned", { size: 14, align: "right" }),
Divider({ color: "steelblue" }),
Text(
[
span("This line mixes "),
span("bold", { bold: true }),
span(", "),
span("italic", { italic: true }),
span(" and "),
span("color", { color: "#1450aa", bold: true }),
span(" in one run."),
],
{ size: 14 },
),
Divider({ color: "steelblue" }),
Paragraph(
"Paragraph is Text with a name that reads as intent. Long text wraps automatically to fit " +
"its width, breaking on word boundaries using the real font metrics, so you never measure " +
"anything yourself.",
{ size: 12 },
),
]),
]),
]);
writeFileSync("text.pdf", await renderToBytes(doc));
}
build();