Dokumentation

Formulare

jasy kann beide Hälften der PDF-Formulare: Es erzeugt ausfüllbare Felder als gewöhnliche Layout-Elemente, und es liest und füllt ein Formular, das von woanders kommt - ein PDF einer Behörde, das Bestellformular eines Lieferanten -, ohne den Rest der Datei anzufassen.

Felder erzeugen

Ein Feld ist ein Layout-Element wie jedes andere. Es reserviert eine Box und erzeugt eine Widget-Annotation, es steht also in einer Column, bricht auf Seiten um und nimmt dieselben Größen-Props wie eine Box.

import {
  Document,
  Page,
  Column,
  Row,
  Text,
  TextField,
  Checkbox,
  RadioGroup,
  Dropdown,
  ListBox,
  PushButton,
  SignatureField,
  renderToBytes,
} from "@jasy/pdf";

Document([
  Page({ size: "A4", margin: 48, gap: 12 }, [
    Text("Registration", { size: 20, bold: true }),

    TextField({ name: "fullName", width: 260, height: 22 }),
    TextField({ name: "notes", width: 260, height: 60, multiline: true, maxLength: 500 }),

    Checkbox({ name: "agree", label: "I agree to the terms" }),
    RadioGroup({ name: "plan", gap: 8 }, [
      { value: "basic", label: "Basic" },
      { value: "pro", label: "Pro" },
    ]),
    Dropdown({ name: "country", width: 120, height: 22 }, ["DE", "AT", "CH"]),
    ListBox({ name: "topics", width: 160, height: 48 }, ["invoices", "reports"]),

    PushButton({ name: "reset", label: "Reset", action: "reset", width: 80, height: 24 }),
    SignatureField({ name: "signature", width: 200, height: 48 }),
  ]),
]);

Sieben Arten: TextField, Checkbox, Radio / RadioGroup, Dropdown (Alias Select), ListBox, PushButton und SignatureField. Die Felder mit Auswahl nehmen sie als zweites Argument, so wie Column seine Kinder - eine Radio-Option als { value, label }, einen Eintrag für Dropdown oder Liste als einfachen String.

Ein PushButton tut eines von drei Dingen: action: "reset" leert das Formular, { open: url } folgt einem Link, { submit: url } sendet die Werte - Letzteres nur in Acrobat, denn weder poppler noch pdf.js setzen /SubmitForm um.

Die Props, die jedes Feld nimmt

PropWas es tut
nameder Schlüssel, unter dem der Wert gespeichert wird und den fillForm nutzt
width, heightdie Box, die es reserviert, in Punkt
tooltipText beim Darüberfahren (/TU), und was ein Screenreader ansagt
readOnlysichtbar, aber nicht bearbeitbar
requiredmuss ausgefüllt sein, bevor das Formular gesendet wird
hiddengar nicht sichtbar, weder am Bildschirm noch im Druck
printfalse zeigt es nur am Bildschirm
border, background, borderWidthsein Aussehen; ein Feld zeichnet standardmäßig keine Border
color, fontSizewie der Wert in der Box gezeichnet wird

Ein Feld ohne border ist unsichtbar, bis es angeklickt wird, genau wie ein Eingabefeld im Browser mit border: none. Das ist ein vernünftiger Standard, wenn Sie Ihre eigenen Kästen darunter zeichnen, und sonst eine Überraschung - geben Sie ihm also einen:

TextField({ name: "fullName", width: 260, height: 22, border: "#aab3c2", background: "#fbfcfe" });

Und die, die nur manche nehmen

FeldPropWas es tut
TextFieldvalueder Wert, mit dem es beginnt
multilinemehrere Zeilen annehmen - mit passender height
passworddie Zeichen verbergen
maxLengthalles Längere ablehnen, bei der Eingabe und beim Ausfüllen
align"left" | "center" | "right"
Checkboxcheckedbeginnt angehakt
onValuewas gespeichert wird, wenn angehakt (Standard "Yes")
sizeSeitenlänge der Box in Punkt
RadioGroupvaluewelche Option zu Beginn gewählt ist, nach ihrem Wert
size, gapDurchmesser des Buttons, und der Abstand dazwischen
Dropdownvalue, editabledie Auswahl zu Beginn; editable erlaubt freien Text
ListBoxvalue, multiSelectdie Auswahl zu Beginn; mehrere gleichzeitig wählbar
PushButtonaction"reset" | { open: url } | { submit: url }
Felder mit Labellabelder Text neben einer Checkbox, einer Radio-Option oder einem Button
labelSize, labelColor, labelGapwie dieses Label gesetzt ist und wie weit sie entfernt sitzt

Eine Radio-Option nimmt außerdem selected, wenn Sie die Buttons selbst bauen, statt RadioGroup eine Liste zu geben.

jasy erzeugt für jedes Feld eine Appearance, der Wert ist also sichtbar, bevor ein Reader ihn neu zeichnet. Mit renderToBytes(doc, { fieldAppearances: false }) schalten Sie das ab, wenn der Viewer lieber alles selbst zeichnen soll. Text in Feldern wird absichtlich ohne Kerning gesetzt: Viewer kernen Feldtext nicht, und eine erzeugte Appearance muss dem entsprechen, was der Viewer darüber neu zeichnet.

Ein fremdes Formular ausfüllen

Der Reader hat einen eigenen Entry Point, ein Projekt, das nur PDFs erzeugt, bündelt ihn also nie mit:

import { readAcroForm, fillForm, flattenForm, PdfDocument } from "@jasy/pdf/edit";

// What is in there? Each field comes back with its name, type, current value, options and flags.
const form = readAcroForm(await PdfDocument.open(bytes));
form.fields.map((f) => `${f.name} (${f.type})`); // ["fullName (Tx)", "agree (Btn)", ...]

Jedes Feld, das zurückkommt:

EigenschaftWas es ist
nameder vollständige Name - die /T-Werte von der Wurzel abwärts, mit Punkten verbunden
type"Tx" Text · "Btn" Button · "Ch" Auswahl · "Sig" Signatur
value, valueswas es gerade enthält; values bei einer Auswahl mit Mehrfachwahl
optionsdie Einträge eines Auswahlfelds, als { value, label }
onValuesjeder Exportname, den eine Checkbox oder Radio-Option neben Off annehmen kann
maxLen/MaxLen, im Baum nach unten vererbt wie der Typ
flagsdie /Ff-Bits, bereits mit allem Geerbten zusammengeführt
widgetsdie Boxen, die es anzeigen - eine Radio-Gruppe hat eine pro Button
needsAppearancekein Widget hat eine gezeichnete Appearance, beim Ausfüllen muss also eine erzeugt werden

Achten Sie auf den Namen: Die Option beim Erzeugen heißt maxLength, das zurückgelesene Feld meldet maxLen, denn diese Seite folgt dem PDF-Schlüssel /MaxLen und nicht der Factory.

const { bytes: filled, warnings } = await fillForm(bytes, {
  fullName: "Ada Lovelace",
  agree: true,
  country: "DE",
});

fillForm und flattenForm geben beide ein Ergebnisobjekt zurück - bytes plus das, was angefasst wurde (filled, flattened), und alle warnings - statt roher Bytes, damit auf dem Rückweg nichts über den Vorgang verloren geht.

Sie übergeben Werte, keine Änderungen an Objekten. Das Formular ist ein Vertrag, und jede Art, ihn zu brechen, ist ein benannter Fehler statt eines stillen Nichtstuns: ein unbekannter Feldname, ein Wert vom falschen Typ für die Feldart, ein String über das MaxLen des Felds hinaus, eine Auswahl, die nicht in der Liste steht, ein schreibgeschütztes Feld, ein Signaturfeld, ein Push-Button.

Flattening

flattenForm stempelt die Appearance jedes Felds auf die Seite und entfernt das Widget, die Werte lassen sich also nicht mehr bearbeiten. fillForm(bytes, values, { flatten: true }) erledigt beides in einem Durchgang:

const { bytes: frozen } = await fillForm(bytes, { fullName: "Ada" }, { flatten: true });

// or on its own, optionally for named fields only
const { bytes: partly } = await flattenForm(filled, { fields: ["fullName"] });

Die Originaldatei bleibt ein Präfix

Gespeichert wird als Incremental Update: Die Originalbytes bleiben unverändert, die geänderten Objekte werden angehängt, und die neue Cross-Reference-Tabelle verweist mit /Prev zurück. Das ergibt eine Eigenschaft, die Sie mit einem Diff prüfen können - die Datei, die Sie übergeben haben, ist wörtlich ein Präfix der Datei, die Sie zurückbekommen. Nichts wurde hinter Ihrem Rücken neu geschrieben, und jede Signatur über die Originalbytes deckt sie weiterhin ab.

Ein verschlüsseltes PDF funktioniert auch: fillForm(bytes, values, { password }) entschlüsselt, füllt und verschlüsselt neu, einschließlich der Strings, die es übernimmt.

Fehler, die Sie abfangen können, statt Strings, die Sie vergleichen müssen

Jede Ablehnung hat einen Typ, ein Aufrufer kann also eine von der anderen unterscheiden, ohne Meldungen zu lesen:

import {
  fillForm,
  FillError,
  FlattenError,
  PdfEncryptedError,
  PdfStreamTooLargeError,
} from "@jasy/pdf/edit";

try {
  await fillForm(bytes, { nope: "x" });
} catch (e) {
  if (e instanceof FillError) {
    // 'no such field: "nope". This form has: "fullName", "agree"'
  }
}

PdfEncryptedError sagt, welches der drei Dinge schiefging - kein Passwort, falsches Passwort oder eine Revision, die jasy nicht umsetzt. PdfStreamTooLargeError ist der Schutz vor Zip-Bomben: Ein komprimierter Stream, der sich auf mehr als 64 MB entpacken würde, wird abgelehnt statt in den Speicher entpackt. Pro Aufruf heben oder senken Sie die Grenze mit maxStreamSize, den Standard lesen Sie aus DEFAULT_MAX_STREAM_SIZE:

await fillForm(bytes, values, { maxStreamSize: 8 * 1024 * 1024 });

Der Schutz ist kein beiläufiger Rat: Die naheliegende Umsetzung ist falsch, denn fflate wirft bei einem zu langen Entpacken keinen Fehler - es schneidet still ab, und das würde einen Absturz gegen leise Datenverfälschung tauschen. jasy streamt die Eingabe und prüft die Summe, während sie wächst.

Lesen ist breit, Schreiben ist schmal

jasy öffnet AES-256 (R6, R5), AES-128 (R4) und RC4 (R2/R3), denn solche Dateien gibt es, und vielleicht müssen Sie eine ausfüllen. Geschrieben wird nur AES-256 R6 - der neueste Standard. Jemandem einen Schutz in die Hand zu geben, der bekanntermaßen gebrochen ist, ist keine Funktion, also verweigert der alte Handler das Verschlüsseln.

Gut zu wissen beim Vergleichen: PDFKit und react-pdf verwenden beide standardmäßig 40-Bit-RC4, wenn Sie nach einem Passwort fragen, und keines von beiden kann überhaupt R6 erzeugen.

Der Fehler, aus dem dieses Design entstand

Ein ausgefülltes Feld zeigte nichts, bis man hineinklickte. Die naheliegende Erklärung - „der Viewer zeichnet träge neu“ - war falsch, und eine zweite Umsetzung bewies es: Die ausgefüllte Datei von react-pdf verhielt sich richtig. Der Unterschied war, dass deren Datei keine Appearance hatte, der Viewer also zeichnen musste, während unsere eine veraltete aus der Erzeugung trug. Wir hatten einen neuen Wert geschrieben und das alte Bild stehen lassen: ein Dokument, das sich selbst widerspricht.

Beim Ausfüllen wird die Appearance eines Text- oder Auswahlfelds deshalb verworfen; die eines Buttons bleibt, denn sie enthält seine Zustände an und aus, und nur /AS wechselt.