Dokumentation

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" });
PropTypWas es tut
sizenumberSchriftgröße in Punkt (Standard 12)
boldbooleanfett
italicbooleankursiv
colorColorInputTextfarbe (jede der Farbformen)
fontstringSchriftfamilie (Standard "Helvetica"; siehe Schriften)
align"left" | "center" | "right" | "justify"Ausrichtung im Textblock (Standard: der Anfang - links, in rtl rechts)
lineHeightnumberFaktor für die Line-Height; nicht gesetzt = die natürliche Line-Height der Schrift
underlinebooleanunterstreichen, an der Position und in der Stärke, die die Schrift angibt
strikethroughbooleaneine Linie durch den Text ziehen, auf halber x-Höhe
skipInkbooleandie Unterstreichung um Descender herumführen (braucht eine eingebettete Schrift)
letterSpacingnumberzusätzlicher Abstand nach jedem Zeichen, in Punkt (negativ verengt)
wordSpacingnumberzusätzlicher Vorschub an jedem Leerzeichen, in Punkt (CSS word-spacing)
textIndentnumberwie weit die ERSTE Zeile eingerückt beginnt, in Punkt
textTransform"uppercase" | "lowercase" | "capitalize"Groß-/Kleinschreibung ändern (CSS text-transform)
ligaturesbooleandie Ligaturen fi / fl / ffi der Schrift (Standard an)
breakWordbooleanein 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
maxLinesnumberdie Zahl der Zeilen begrenzen (Standard: unbegrenzt)
overflow"clip" | "ellipsis"was nach maxLines passiert (Standard "clip")
orphansnumberMindestzahl an Zeilen, die vor einem Page Break stehen bleiben (Standard 2)
widowsnumberMindestzahl 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();