Dokumentation
PDFs erstellen
PDFs in Vue
PDFs in Nuxt
PDFs in React
PDFs in Angular
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
| Prop | Was es tut |
|---|---|
name | der Schlüssel, unter dem der Wert gespeichert wird und den fillForm nutzt |
width, height | die Box, die es reserviert, in Punkt |
tooltip | Text beim Darüberfahren (/TU), und was ein Screenreader ansagt |
readOnly | sichtbar, aber nicht bearbeitbar |
required | muss ausgefüllt sein, bevor das Formular gesendet wird |
hidden | gar nicht sichtbar, weder am Bildschirm noch im Druck |
print | false zeigt es nur am Bildschirm |
border, background, borderWidth | sein Aussehen; ein Feld zeichnet standardmäßig keine Border |
color, fontSize | wie 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
| Feld | Prop | Was es tut |
|---|---|---|
TextField | value | der Wert, mit dem es beginnt |
multiline | mehrere Zeilen annehmen - mit passender height | |
password | die Zeichen verbergen | |
maxLength | alles Längere ablehnen, bei der Eingabe und beim Ausfüllen | |
align | "left" | "center" | "right" | |
Checkbox | checked | beginnt angehakt |
onValue | was gespeichert wird, wenn angehakt (Standard "Yes") | |
size | Seitenlänge der Box in Punkt | |
RadioGroup | value | welche Option zu Beginn gewählt ist, nach ihrem Wert |
size, gap | Durchmesser des Buttons, und der Abstand dazwischen | |
Dropdown | value, editable | die Auswahl zu Beginn; editable erlaubt freien Text |
ListBox | value, multiSelect | die Auswahl zu Beginn; mehrere gleichzeitig wählbar |
PushButton | action | "reset" | { open: url } | { submit: url } |
| Felder mit Label | label | der Text neben einer Checkbox, einer Radio-Option oder einem Button |
labelSize, labelColor, labelGap | wie 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:
| Eigenschaft | Was es ist |
|---|---|
name | der vollständige Name - die /T-Werte von der Wurzel abwärts, mit Punkten verbunden |
type | "Tx" Text · "Btn" Button · "Ch" Auswahl · "Sig" Signatur |
value, values | was es gerade enthält; values bei einer Auswahl mit Mehrfachwahl |
options | die Einträge eines Auswahlfelds, als { value, label } |
onValues | jeder Exportname, den eine Checkbox oder Radio-Option neben Off annehmen kann |
maxLen | /MaxLen, im Baum nach unten vererbt wie der Typ |
flags | die /Ff-Bits, bereits mit allem Geerbten zusammengeführt |
widgets | die Boxen, die es anzeigen - eine Radio-Gruppe hat eine pro Button |
needsAppearance | kein 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.