Dokumentation
PDFs erstellen
PDFs in Vue
PDFs in Nuxt
PDFs in React
PDFs in Angular
Komponenten
Jede Komponente entspricht einem Element der Engine, und jede Option der Engine ist ein typisierter Prop -
<Text :size :color bold> vervollständigt sich und wird typgeprüft, und ein falscher Wert (etwa ein fit,
das es nicht gibt) wird in Ihrem Editor rot. Ein Test hält die Props an der Engine fest, eine Option kann
also nicht im Core erscheinen, ohne hier anzukommen. Boolesche Props haben eine Kurzform: <Text bold> ist
<Text :bold="true">.
Alle Komponenten:
Document · Page · Column · Row · Box · Padding · Expanded · Spacer · Divider ·
Image · Svg · Canvas · Text · Paragraph · Span · Table / TableRow / TableCell ·
Positioned · DefaultTextStyle · Link · Anchor · Bookmark · Rotated · RotatedBox ·
PageBreak · KeepTogether · PageNumber · PageCount · SubPageNumber · SubPageCount ·
PageBuilder · SplitView · Flow · Region · ColumnBreak · TextField · Checkbox ·
RadioGroup · Dropdown · ListBox · PushButton · SignatureField.
Das sind 43 Komponenten, und das Nuxt-Modul importiert genau dieselben 43 automatisch.
Struktur
Document
Die Wurzel jeder PDF-Komponente. Ihre Text-Props (font, size, color, lineHeight, align, bold,
italic) setzen die Standards für das ganze Dokument, die alles darin erbt.
| Prop | Typ |
|---|---|
size, color, font, bold, italic | Standard-Textstil |
align, lineHeight | Standard-Absatzstil |
meta | { title?, author? } |
fonts | eigene Fonts, siehe Daten und Assets |
Page
Eine physische Seite. Ordnet ihre Kinder als Column an, gap, justify und align funktionieren also auch
hier.
| Prop | Typ |
|---|---|
size | "A4", "Letter", ... oder eine eigene Größe |
orientation | "portrait" | "landscape" |
margin | eine Zahl, oder Insets pro Seite |
gap | Abstand zwischen den Kindern |
justify | "start" | "center" | "end" | "between" | "around" |
align | "start" | "center" | "end" | "stretch" |
<Page> nimmt außerdem die Slots #header und #footer. Sie werden auf jeder physischen Seite gezeichnet,
über die der Inhalt läuft, und für jede Seite neu gebaut - so weiß <PageNumber>, wo es ist:
<template>
<Document>
<Page :size="'A4'" :margin="48">
<template #header>
<Text :size="9" color="#94a3b8">ACME GmbH - Invoice</Text>
</template>
<Text>... long content that paginates across pages ...</Text>
<template #footer>
<Row :justify="'between'">
<Text :size="9" color="#94a3b8">acme.example</Text>
<Text :size="9" color="#94a3b8">Page footer</Text>
</Row>
</template>
</Page>
</Document>
</template>
Layout
Column und Row
Stapeln die Kinder senkrecht (Column) oder waagerecht (Row). Beide nehmen die Optionen der Stacks der
Engine: gap, justify, align, wrap, reverse, align-content, eine width und height in Punkt
oder "%", und die Page-Break-Props unten.
<template>
<Row :gap="12" :justify="'between'" :align="'center'">
<Text bold>Item</Text>
<Text>Value</Text>
</Row>
</template>
Jedes Kind kann mit align-self aus der Ausrichtung seiner Zeile ausscheren, sich mit order verschieben
und min-width, max-width, min-height, max-height und aspect-ratio nehmen - siehe
Layout.
Box
Ein Container mit Größe und Gestaltung: Hintergrund, Border, Padding, abgerundete Ecken.
| Prop | Typ |
|---|---|
bg | eine Farbe, oder ein Gradient |
border | eine Farbe |
borderTop / borderRight / borderBottom / borderLeft, borderWidth | Border pro Seite |
padding | eine Zahl, oder Insets pro Seite |
width, height | Punkt, oder ein Prozentwert wie "50%" |
radius | eine Zahl, oder Radien pro Ecke |
relative | macht sie zum Frame für die Positionierung |
overflow | "hidden" schneidet die Kinder auf die Box zu |
Padding, Spacer, Expanded, Divider
<Padding :insets="16">- umgibt ein Kind mit Abstand (eine Zahl oder Insets pro Seite).<Spacer :flex="1">- dehnbarer leerer Raum, der Geschwister auseinanderschiebt.<Expanded :flex="1">- ein Flex-Kind, das den restlichen Platz auf der Main-Axis füllt.<Divider color="#e2e8f0" :thickness="1" />- eine Linie.
Text
Text, Paragraph und Span
Text und Paragraph zeichnen Text; Span gestaltet ein Stück darin.
Jeder Textstil der Engine ist hier ein Prop - ein Test hält die Menge an der Engine fest, ein Stil kann also nicht im Core erscheinen, ohne in Vue anzukommen.
| Prop | Typ |
|---|---|
size, font, color | das Grundlegende; font nimmt auch eine Fallback-Liste |
bold, italic | boolesche Kurzformen |
align | "left" | "center" | "right" | "justify" |
lineHeight | Faktor für die Line-Height |
letterSpacing, wordSpacing | in Punkt; negativ verengt |
textIndent | Einzug der ersten Zeile, in Punkt |
textTransform | "uppercase" | "lowercase" | "capitalize" | "none" |
underline, strikethrough, skipInk | Linien; skipInk führt sie um Descender herum |
direction | "ltr" | "rtl" - Hebräisch und Arabisch in richtiger Reihenfolge |
ligatures | false schaltet fi/fl/ffi ab, die standardmäßig an sind |
breakWord, hyphenate | was mit einem Wort geschieht, das zu lang für seine Box ist |
orphans, widows | Mindestzahl an Zeilen, die an einem Page Break zusammenbleiben |
maxLines, overflow | overflow ist "clip" | "ellipsis" |
role | "h1".."h6" | "p" für ein getaggtes, barrierefreies PDF |
href | dieses Stück mit einer externen URL verlinken |
to | dieses Stück mit einem <Anchor> verlinken |
<Span> nimmt die Stile eines Textstücks geändert in beta.5 - size, font, color,
bold, italic, die Linien, letterSpacing, wordSpacing, textTransform, ligatures, breakWord,
hyphenate - plus href / to und verticalAlign ("super" | "sub"), das die Baseline eines Stücks
verschiebt, ohne seine Größe zu ändern. Was zum ganzen Absatz gehört, bleibt an <Text>: align,
lineHeight, textIndent, direction, maxLines, overflow, orphans, widows und role.
<template>
<Paragraph>
Plain text, then a <Span bold color="#1450aa">styled run</Span> in the middle.
</Paragraph>
</template>
bold und italic sind standardmäßig nicht gesetzt, ein <Text> in einem fetten <DefaultTextStyle>
bleibt also fett, solange Sie es nicht überschreiben. <DefaultTextStyle> belegt den Textstil für einen
ganzen Teilbaum neu vor, das Gegenstück pro Abschnitt zu den Standards von <Document>.
Mit href (oder to) an einem <Span> verlinken Sie nur dieses Stück, und der Satz fließt drumherum
weiter:
<template>
<Paragraph>
Built with <Span href="https://jasy.dev" bold color="#1450aa">jasy</Span>, and this part is not
a link.
</Paragraph>
</template>
Image
<template>
<Image
:src="bytes"
:width="120"
:height="120"
:fit="'cover'"
:align="'top'"
:radius="8"
alt="Logo"
/>
</template>
src nimmt Bild-Bytes (ein Uint8Array) oder eine URL; wie Sie sie im Browser laden, steht unter
Daten und Assets. fit ist "fill" (der Standard), "contain", "cover"
oder "none". align seit beta.5 sagt, wo das Bild in seiner Box sitzt - "center" (der
Standard), "top", "bottom left" und so weiter. Ohne Größe hat das Bild seine eigene Größe, passend
verkleinert; die Props sind die der Factory Image.
Eine SVG-Quelle wird von selbst erkannt und bleibt ein Vektor - ohne fit, align oder radius, denn
sie wird immer passend und zentriert gezeichnet, das ist der eigene Standard von SVG.
Svg
<template>
<Svg :src="logoMarkup" :width="120" alt="Acme" />
</template>
src nimmt SVG-Markup, die Bytes der Datei oder - auf dem Server - einen Pfad. Ohne Größe zeichnet es in
seiner eigenen Größe; legen Sie eine Achse fest, folgt die andere der viewBox. Mit alt wird es in einem
getaggten PDF zu einer Figure. Was das Subset abdeckt, steht unter SVG und Canvas.
Canvas
Für ein Diagramm, eine Sparkline oder ein Siegel reicht <Canvas> Ihrer Funktion einen typisierten Pen und
die Größe, die das Layout ermittelt hat. paint ist ein gewöhnlicher Funktions-Prop:
<script setup lang="ts">
import type { CanvasPainter } from "@jasy/pdf";
const months = [12, 19, 14, 27, 22, 31];
function sparkline(c: CanvasPainter, { width, height }: { width: number; height: number }) {
const max = Math.max(...months);
c.move(0, height - (months[0] / max) * height);
months
.slice(1)
.forEach((v, i) =>
c.line(((i + 1) / (months.length - 1)) * width, height - (v / max) * height),
);
c.stroke("#1450aa", { width: 1.5, cap: "round" });
}
</script>
<template>
<Canvas :height="46" :paint="sparkline" />
</template>
Dieselbe Funktion funktioniert in jeder Breite, eine Definition dient also einer breiten Karte und einer schmalen Seitenleiste.
Table
<Table :columns> enthält <TableRow>s aus <TableCell>s. Markieren Sie eine Zeile mit header, um sie
auf jeder Seite zu wiederholen, über die die Tabelle läuft. Columns sind Breiten: eine Zahl ist eine feste
Breite in Punkt, ein String wie "1fr" ein flexibler Anteil.
<template>
<Table :columns="['1fr', 120]" cell-border="#e2e8f0" :cell-padding="9">
<TableRow header>
<TableCell><Text bold>Description</Text></TableCell>
<TableCell><Text bold>Amount</Text></TableCell>
</TableRow>
<TableRow v-for="line in lines" :key="line.id">
<TableCell
><Text>{{ line.name }}</Text></TableCell
>
<TableCell
><Text>{{ line.total }} EUR</Text></TableCell
>
</TableRow>
</Table>
</template>
Table nimmt außerdem gap / rowGap / colGap, cellPadding, cellBorder und eine Farbe für rule.
Positioned
Platzierung außerhalb des Flows, verankert am nächsten <Box relative> (oder an der Seite). Geben Sie ihm
Kanten (top / right / bottom / left) oder einen Anchor (h / v) plus einen Versatz x / y.
:z-index seit beta.5 legt die Zeichenreihenfolge fest - siehe
Positionierung.
<template>
<Box relative :height="200">
<Positioned :top="8" :right="8">
<Text :size="9" color="#94a3b8">draft</Text>
</Positioned>
</Box>
</template>
Navigation
<Link> macht sein Kind anklickbar. href öffnet eine URL, to springt zu einem <Anchor> im selben
Dokument. Genau eines von beiden.
<template>
<Link href="https://jasy.dev">
<Box bg="#f3dc29" :padding="12"><Text>The whole box is the hit area</Text></Box>
</Link>
</template>
<Anchor name> markiert ein Sprungziel, und <Bookmark title> fügt einen Eintrag in den Outline-Baum ein,
den Ihr Viewer in seiner Seitenleiste zeigt. level hängt ihn unter das nächste vorangehende kleinere
Level.
| Komponente | Props |
|---|---|
<Link> | href | to |
<Anchor> | name (Pflicht) |
<Bookmark> | title (Pflicht), level |
Eine Kapitelüberschrift will meist beides: einen Eintrag in der Seitenleiste und ein Sprungziel. Verschachteln Sie sie.
<template>
<Bookmark title="2. Installation" :level="1">
<Anchor name="install">
<Text :size="22" bold>2. Installation</Text>
</Anchor>
</Bookmark>
</template>
Wie Links, Anchors und Bookmarks darunter funktionieren, steht unter Navigation.
Rotated und RotatedBox
<Rotated :angle> dreht sein Kind um einen beliebigen Winkel um seine Mitte, beim Zeichnen: Die Layout-Box
bleibt ungedreht, Geschwister ordnen sich also nicht neu an. Genau das wollen Sie für einen Stempel oder
ein Watermark.
<RotatedBox :turns> macht stattdessen Quarter-Turns, die das Layout berücksichtigt. Eine Drehung um 90
oder 270 Grad tauscht Breite und Höhe, ein senkrechtes Label reserviert also genau den Streifen, den es
einnimmt, und seine Nachbarn fließen drumherum. turns zählt Schritte von 90 Grad im Uhrzeigersinn.
<template>
<Row :gap="12">
<RotatedBox :turns="3"><Text bold>SECTION A</Text></RotatedBox>
<Column><Text>Line one</Text><Text>Line two</Text></Column>
</Row>
<Rotated :angle="-18">
<Box bg="#ffe9e9" border="#d33" :padding="10"
><Text :size="26" bold color="#d33">PAID</Text></Box
>
</Rotated>
</template>
Page Breaks
<PageBreak /> unterbricht den Flow: Alles danach beginnt auf einer neuen Seite. <KeepTogether> tut das
Gegenteil - es lässt seine Kinder nicht teilen und schiebt die ganze Gruppe auf die nächste Seite, wenn sie
auf dieser nicht passt. Beide gibt es auch als Props (break-before, break-after, keep-together) an
<Box>, <Column> und <Row>, das ist meist die aufgeräumtere Form.
<template>
<Page>
<Text :size="18" bold>Terms</Text>
<Text>...</Text>
<PageBreak />
<Text :size="18" bold>Appendix</Text>
<KeepTogether>
<Box border="#d0d4da" :radius="8" :padding="14">
<Text bold>Signed</Text>
<Text :size="9" color="gray">This box is never torn across two pages.</Text>
</Box>
</KeepTogether>
</Page>
</template>
Ein Break, der nichts bewirken kann - in einer <Row> oder irgendwo, wo nicht paginiert wird -, wird
ignoriert, statt halb befolgt zu werden, und sagt das einmal auf der Konsole. Eine Gruppe, die höher als
eine ganze Seite ist, wird doch geteilt, denn die Alternative wäre ein Dokument, das nie fertig wird.
Spalten seit beta.5
<SplitView> setzt seinen Inhalt in Zeitungsspalten, die auf der nächsten Seite weitergehen und sich auf
der letzten ausgleichen. <ColumnBreak /> beginnt die nächste Spalte.
<template>
<Page :gap="12">
<Text :size="28" bold color="#1450aa">The Valley Courier</Text>
<SplitView :columns="3" :colGap="16" :rowGap="8">
<Text v-for="p in paragraphs" :key="p.id">{{ p.text }}</Text>
</SplitView>
<Text italic>This line follows the columns, right where they end.</Text>
</Page>
</template>
Für ein eigenes Layout nimmt <Flow> es als Slot #page: eine Seite des Layouts, mit einer <Region />
überall dort, wo der Inhalt hin soll. Der Slot bekommt { index, pageNumber }, die erste Seite kann also
anders aussehen als die übrigen.
<template>
<Page>
<Flow :gap="8">
<template #page="{ index }">
<Column :gap="12">
<Text v-if="index === 0" :size="22" bold color="#1450aa">Annual Report</Text>
<Text v-else :size="9" italic color="gray">Annual Report, continued</Text>
<Row :gap="16">
<Expanded><Region /></Expanded>
<Box bg="#f3f5f9" :padding="8"><Region :width="150" /></Box>
</Row>
</Column>
</template>
<Text v-for="p in report" :key="p.id">{{ p.text }}</Text>
<ColumnBreak />
<Text bold>Notes</Text>
<Text v-for="n in notes" :key="n.id" :size="8">{{ n.text }}</Text>
</Flow>
</Page>
</template>
Der Slot #page wird für jede Seite gerendert, während das PDF angeordnet wird, und sieht, was die
Komponenten um ihn herum per provide bereitstellen. Rendern Sie ein Dokument mit <Flow> über
renderToPdf. Die Optionen und die benannten Fehler stehen auf der Seite
Spalten.
Formularfelder
Ein Formularfeld ist ein Layout-Element wie jedes andere: Es reserviert eine Box, fließt mit seinen Nachbarn und erzeugt ein Widget, in das man im Reader wirklich tippen kann. Sieben Arten:
| Komponente | Wofür |
|---|---|
TextField | eine Zeile oder, mit multiline, mehrere; password, max-length |
Checkbox | ein einzelnes Häkchen; on-value ist das, was gespeichert wird |
RadioGroup | eines von mehreren, für Sie angeordnet; options + value |
Dropdown | eine geschlossene Liste, oder editable für freien Text |
ListBox | eine sichtbare Liste; multi-select für mehrere gleichzeitig |
PushButton | eine Aktion - "reset", { open: url } oder { submit: url } |
SignatureField | ein leeres Feld, in dem der Leser unterschreibt |
Jedes Feld nimmt name (der Schlüssel, unter dem der Wert gespeichert wird), required, hidden, print
und die Appearance-Props border, background und color. Die einzelnen Widgets ergänzen tooltip,
read-only und border-width; die mit Box (TextField, Dropdown, ListBox, PushButton,
SignatureField) nehmen width und height, eine Checkbox nimmt size. Eine RadioGroup besteht aus
mehreren Buttons und hat deshalb keines dieser drei: Sie nimmt size, gap und die Label-Gestaltung ihrer
Optionen.
<template>
<Column :gap="10">
<TextField name="fullName" :width="220" :height="22" border="#aab3c2" background="#fbfcfe" />
<RadioGroup
name="plan"
:gap="6"
:options="[
{ value: 'basic', label: 'Basic' },
{ value: 'pro', label: 'Pro' },
]"
/>
<Dropdown name="country" :width="110" :height="22" :options="['DE', 'AT', 'CH']" />
<Checkbox name="agree" label="I have read the statutes" />
<PushButton name="clear" label="Reset form" action="reset" :width="92" :height="26" />
</Column>
</template>
Geben Sie einem Feld eine border, wenn es eine haben soll - ein Textfeld zeichnet standardmäßig nichts,
genau wie ein Eingabefeld im Browser mit border: none.
Ein Formular zurückzulesen und auszufüllen ist die andere Hälfte und liegt im Core: Formulare.
Seitenzahlen
<PageNumber> und <PageCount> drucken die aktuelle Seite und die Gesamtzahl des Dokuments. Setzen Sie sie
irgendwohin - ein #footer ist der übliche Ort, aber der Inhalt funktioniert auch. Sie nehmen die
Gestaltungs-Props von <Text> plus offset, das zur Zahl addiert wird (-1, wenn ein Deckblatt nicht
mitzählen soll).
<template>
<Document>
<Page>
<template #footer>
<Row justify="center" :gap="3">
<Text :size="9">Page</Text>
<PageNumber :size="9" bold />
<Text :size="9">of</Text>
<PageCount :size="9" bold />
</Row>
</template>
<Column><!-- body --></Column>
</Page>
</Document>
</template>
Damit die Gesamtzahl stimmt, paginiert jasy das ganze Dokument, bevor es irgendetwas zeichnet.
<SubPageNumber> und <SubPageCount> seit beta.2 zählen innerhalb einer <Page>, für ein Dokument
aus Abschnitten.
Für alles andere, was von der Seite abhängt, nimmt <PageBuilder> seit beta.2 es als seinen
Default Scoped Slot und baut es für jede Seite, auf der es landet. Ein Slot, der nichts rendert, zeigt auf
dieser Seite nichts:
<template>
<Page>
<template #header>
<PageBuilder #default="{ pageNumber }">
<Text v-if="pageNumber > 1" :size="9">Annual Report, continued</Text>
</PageBuilder>
</template>
<template #footer>
<PageBuilder #default="{ pageNumber, pageCount }">
<Text :size="9" align="center">Page {{ pageNumber }} of {{ pageCount }}</Text>
</PageBuilder>
</template>
<!-- body -->
</Page>
</template>
Der Slot bekommt pageNumber, pageCount, pageSize, subPageNumber und subPageTotalPages, und er
sieht, was die Komponenten um ihn herum per provide bereitstellen. Was er baut, wird als Ganzes auf seine
Seite gesetzt.
Eine eigene Komponente
Die Menge oben ist nicht abgeschlossen. registerElement(type, factory) bringt dem Renderer einen neuen
Knotentyp bei, und ab dann ist er eine Komponente wie jede andere - in einem Vue-Template, in einer
Nuxt-Seite oder in einem einfachen Descriptor-Baum:
import { registerElement, Box, Text } from "@jasy/pdf";
registerElement("callout", (props, children) =>
Box({ bg: "#eef1f5", border: "#1450aa", radius: 6, padding: 12 }, [
Text(String(children[0] ?? ""), { color: "#1450aa" }),
]),
);
Darunter liegt buildDocument(descriptor): ein Baum { type, props, children }, aufgelöst über dieselben
Factories, die die Komponenten verwenden. Genau so ist @jasy/vue gebaut - seine Host-Knoten SIND diese
Descriptors -, und deshalb funktioniert ein eigener Typ in jedem Binding zugleich, und deshalb kann ein
Browser einem Node-Service ein Dokument als einfaches JSON übergeben.
Die Primitives von react-pdf sind eine geschlossene Menge; diese hier nicht.