Dokumentation

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.

PropTyp
size, color, font, bold, italicStandard-Textstil
align, lineHeightStandard-Absatzstil
meta{ title?, author? }
fontseigene Fonts, siehe Daten und Assets

Page

Eine physische Seite. Ordnet ihre Kinder als Column an, gap, justify und align funktionieren also auch hier.

PropTyp
size"A4", "Letter", ... oder eine eigene Größe
orientation"portrait" | "landscape"
margineine Zahl, oder Insets pro Seite
gapAbstand 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.

PropTyp
bgeine Farbe, oder ein Gradient
bordereine Farbe
borderTop / borderRight / borderBottom / borderLeft, borderWidthBorder pro Seite
paddingeine Zahl, oder Insets pro Seite
width, heightPunkt, oder ein Prozentwert wie "50%"
radiuseine Zahl, oder Radien pro Ecke
relativemacht 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.

PropTyp
size, font, colordas Grundlegende; font nimmt auch eine Fallback-Liste
bold, italicboolesche Kurzformen
align"left" | "center" | "right" | "justify"
lineHeightFaktor für die Line-Height
letterSpacing, wordSpacingin Punkt; negativ verengt
textIndentEinzug der ersten Zeile, in Punkt
textTransform"uppercase" | "lowercase" | "capitalize" | "none"
underline, strikethrough, skipInkLinien; skipInk führt sie um Descender herum
direction"ltr" | "rtl" - Hebräisch und Arabisch in richtiger Reihenfolge
ligaturesfalse schaltet fi/fl/ffi ab, die standardmäßig an sind
breakWord, hyphenatewas mit einem Wort geschieht, das zu lang für seine Box ist
orphans, widowsMindestzahl an Zeilen, die an einem Page Break zusammenbleiben
maxLines, overflowoverflow ist "clip" | "ellipsis"
role"h1".."h6" | "p" für ein getaggtes, barrierefreies PDF
hrefdieses Stück mit einer externen URL verlinken
todieses 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>

<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.

KomponenteProps
<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:

KomponenteWofür
TextFieldeine Zeile oder, mit multiline, mehrere; password, max-length
Checkboxein einzelnes Häkchen; on-value ist das, was gespeichert wird
RadioGroupeines von mehreren, für Sie angeordnet; options + value
Dropdowneine geschlossene Liste, oder editable für freien Text
ListBoxeine sichtbare Liste; multi-select für mehrere gleichzeitig
PushButtoneine Aktion - "reset", { open: url } oder { submit: url }
SignatureFieldein 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.