Technical Article

Interaktive PDF-Formulare in Delphi: Aktionen und JavaScript

Ein PDF-Formularfeld für sich genommen ist nur ein Feld, das einen Wert enthält. Was ein Formular wie eine kleine Anwendung verhalten lässt, ist die daran angehängte Aktion: ein Klick, der einen Abschnitt ausblendet, gespeicherte Werte aus einer Datei abruft, zur letzten Seite springt oder ein Skript ausführt, das eine Spalte summiert. Nichts davon befindet sich im Feld selbst. Es befindet sich in einem Aktionsverzeichnis, und ISO 32000-1 organisiert die gesamte Familie in §12.6. Dieser Artikel führt durch die Aktionen, auf die ein Delphi-Programm am häufigsten zugreift, und zeigt, wie PDFlibPas jede einzelne mit einem Feld oder einem Link verbindet.

Das mentale Modell, das man im Kopf behalten sollte, ist, dass ein Feld und eine Aktion separate Objekte sind, die durch eine Referenz verbunden sind. Eine Widget-Annotation oder eine Link-Annotation trägt eine Aktion in ihrem /A-Eintrag. Die Aktion benennt das Feld, auf dem sie operiert, nach dem Titel, nicht nach dem Index, sodass der Titel, den Sie einem Feld geben, der Bezeichner ist, den jede spätere Aktion verwendet, um es zu finden. Sobald diese Trennung klar ist, sieht die API nicht mehr wie eine Ansammlung willkürlicher Aufrufe aus, sondern wie ein einziges Muster, das auf vier Arten von Verben angewendet wird.

Benannte Aktionen: Navigation ohne Seitenzahl

Die einfachsten Aktionen benötigen überhaupt keine Parameter. ISO 32000-1 §12.6.4.11, Tabelle 194, definiert benannte Aktionen: Der Viewer interpretiert zur Laufzeit einen symbolischen Namen, anstatt einem gespeicherten Ziel zu folgen. Vier Namen werden universell unterstützt, und es sind genau die, die ein Leser in einer Symbolleiste erwartet: NextPage, PrevPage, FirstPage und LastPage. Da das Ziel relativ zu der Seite ist, die der Viewer gerade anzeigt, funktioniert eine so erstellte Weiter-Schaltfläche auf jeder Seite, ohne dass Sie ein Ziel berechnen müssen.

In PDFlibPas wird eine benannte Aktion an ein Hotspot-Rechteck auf der aktuellen Seite angehängt. Das vierte und fünfte Integer-Argument wählen das Verb und das Erscheinungsbild aus.

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1);   // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1);    // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1);   // jump to last page

Es gibt kein Ziel, das synchron gehalten werden muss, was der eigentliche Punkt ist. Eine benannte Aktion übersteht das Einfügen und Löschen von Seiten, da sie gar keine Seite benennt. Vergleichen Sie das mit einem expliziten Go-to-Link, der einen Zielseitenindex speichert, den Sie neu nummerieren müssen, sobald das Dokument wächst.

Die Hide-Aktion und ihre Array-Tücke

Die Hide-Aktion, ISO 32000-1 §12.6.4.10, Tabelle 196, schaltet die Sichtbarkeit eines oder mehrerer Felder um. Dies ist der sauberste Weg, um Einblend- und Ausblendverhalten ohne Scripting zu erstellen, und es ist genau das, was Sie für einen "Details anzeigen"-Link oder für zwei sich gegenseitig ausschließende Panels wünschen, bei denen das Einblenden des einen das andere verbirgt. Die Aktion trägt ein Ziel in ihrem /T-Eintrag und einen Boolean /H, der die Richtung bestimmt: Ausblenden bei True, Anzeigen bei False.

Die Tücke liegt ganz darin, wie dieses Ziel kodiert ist, und das ist die Art von Detail, die dazu führt, dass ein Formular auf Ihrem Computer funktioniert, aber bei einem Kunden fehlschlägt. Wenn die Aktion ein einzelnes Feld benennt, /T wird als eine einzelne Textzeichenfolge geschrieben. Wenn sie mehrere benennt, wird /T als ein Array von Textzeichenfolgen geschrieben. Ältere Viewer behandeln ein Array mit einem Element nicht genauso wie eine einfache Zeichenfolge, sodass sich die Kodierung nach der Anzahl richten muss: Ein einzelner Name muss als Zeichenfolge und nicht als Array der Länge eins ausgegeben werden, damit möglichst viele Reader ihn interpretieren können. PDFlibPas trifft diese Entscheidung für Sie. Sie übergeben Feldnamen durch Kommas, Semikolons oder Zeilenumbrüche getrennt, und der Writer gibt eine einzelne Zeichenfolge für einen Namen und ein Array für zwei oder mehr aus.

// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

Da die Aktion auf keine externe Ressource verweist, bleibt sie mit PDF/A kompatibel. Die Namen, die Sie übergeben, sind vollqualifizierte Feldtitel, weshalb ein untergeordnetes Feld innerhalb einer Gruppe über seinen vollständigen, durch Punkte getrennten Pfad adressiert werden muss und nicht nur über seinen einfachen Elementnamen.

ImportData: Vorausfüllen aus FDF

Während die Hide-Aktion neu anordnet, was bereits auf der Seite vorhanden ist, bringt die Import-Data-Aktion Werte von außen ein. ISO 32000-1 §12.6.4.8, Tabelle 198, definiert sie als eine Aktion, die das AcroForm aus einer Forms Data Format-Datei (FDF) auf der Festplatte füllt. Dies ist die Aktion hinter Steuerelementen wie "Beispieldaten neu laden" oder "Auf Standardwerte zurücksetzen", bei denen eine FDF-Datei neben der PDF-Datei liegt und die kanonischen Feldwerte enthält. Der Aufruf spiegelt die anderen wider, indem er das Hotspot-Rechteck, den Pfad zur FDF-Datei und eine Appearance-Bitmaske übernimmt: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Die Datei muss beim Erstellen der PDF-Datei nicht existieren, sie muss jedoch vorhanden sein, wenn der Benutzer klickt, und alle Backslashes im Pfad werden für Sie in die PDF-kanonische Slash-Form umgeschrieben.

Eine Einschränkung sollte deutlich ausgesprochen werden, da sie häufig überrascht. Eine Import-Data-Aktion verweist auf eine externe Datei und ist daher in PDF/A nicht zulässig. Wenn sich das Dokument im PDF/A-Modus befindet, gibt der Aufruf Null zurück und fügt nichts hinzu, anstatt eine Datei zu erzeugen, die die Validierung nicht besteht. Wenn Ihre Pipeline auf Archivierungsausgabe abzielt, muss das Vorausfüllen zum Zeitpunkt der Generierung erfolgen, indem die Feldwerte direkt geschrieben werden, und darf nicht auf einen Klick verschoben werden.

JavaScript: Globale Pakete und skriptbasierte Aktionen

Für Logik, die über Anzeigen, Ausblenden und Importieren hinausgeht, greift die Aktionsfamilie auf JavaScript auf Dokumentenebene zurück. Es gibt zwei verschiedene Orte, an denen ein Skript abgelegt werden kann, und dieser Unterschied ist wichtig. Ein JavaScript-Paket auf Dokumentenebene wird einmal für die gesamte Datei gespeichert und ausgeführt, wenn das Dokument geöffnet wird. Dies ist der richtige Ort für Funktionsdefinitionen und gemeinsamen Zustand. Ein aktionsspezifisches Skript ist an einen Link oder ein Feld angehängt und wird nur ausgeführt, wenn dieses Objekt aktiviert wird. Dies ist der richtige Ort für die einzelne Zeile, die eine bereits im Paket definierte Funktion aufruft.

PDFlibPas stellt beides bereit. AddGlobalJavaScript speichert ein benanntes Paket auf Dokumentenebene; die Wiederverwendung eines Namens ersetzt den zuvor darunter gespeicherten Inhalt. AddLinkToJavaScript hängt ein Skript an einen Hotspot an, sodass ein Klick dieses ausführt.

// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
  'function recalcTotal() {' +
  '  var net = this.getField("Net").value;' +
  '  var tax = this.getField("Tax").value;' +
  '  this.getField("Gross").value = Number(net) + Number(tax);' +
  '}');

// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

Die Funktion im globalen Paket zu halten und den Aufruf im Link zu platzieren, ist keine reine Geschmacksfrage. Es verhindert, dass derselbe Code auf jedem Steuerelement dupliziert wird, das ihn benötigt. Zudem bedeutet es, dass ein Viewer mit deaktiviertem Scripting beim Klick einfach nichts tut, anstatt an einem fehlerhaften Inline-Blob abzustürzen. Es hält auch die aktionsspezifischen Einträge klein, was die Datei lesbar hält, wenn Sie sie später untersuchen.

Felder, untergeordnete Felder und Einfrieren des Ergebnisses

Aktionen benötigen Felder, auf denen sie ausgeführt werden können, daher ist es hilfreich zu sehen, wie ein Feld entsteht. NewFormField erstellt ein Feld auf der aktuellen Seite und gibt dessen Index zurück; der Integer-Typ wählt die Art aus, wobei 1 Text, 2 Pushbutton, 3 Checkbox, 4 Radiobutton, 5 Choice, 6 Signature und 7 ein Parent-Element ist, das untergeordnete Elemente besitzt, aber selbst nichts zeichnet. Der übergebene Titel darf keinen Punkt enthalten, da der Punkt als Trennzeichen in den vollqualifizierten Namen dient, die Aktionen zum Adressieren von Kindelementen verwenden.

Radiogruppen und hierarchische Formulare werden erstellt, indem man einem übergeordneten Feld untergeordnete Elemente zuweist. NewChildFormField fügt ein untergeordnetes Feld unter einem benannten übergeordneten Feld hinzu, und für Radio- und Choice-Fälle fügt AddFormFieldSub die einzelnen Optionen hinzu und übergibt einen temporären Index, mit dem Sie jede Option positionieren. Wenn die interaktive Phase vorbei ist und Sie ein Feld einfrieren möchten, sodass sein aktuelles Erscheinungsbild zu permanentem Seiteninhalt wird, zeichnet FlattenFormField das Feld auf die Seite und entfernt es aus dem Formular. Nach dem Flatten verschieben sich die Indizes nachfolgender Felder um eins nach unten, was die eine sache ist, an die man denken muss, wenn man mehrere Felder in einer Schleife flattet.

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // top-left origin
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // A text field the Hide action will target by its title.
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Wire a Hide link and a navigation link to this page.
    Pdf.DrawText(40, 110, 'Toggle shipping block:');
    Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
    Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1);  // Last page

    // A document-level script available to every event in the file.
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Freeze the field if the output should no longer be editable.
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

Der Flatten-Aufruf ist absichtlich auskommentiert. Lassen Sie ihn weg, und das Dokument wird als interaktives Formular ausgeliefert, dessen Aktionen im Reader ausgeführt werden. Aktivieren Sie ihn, und das Feld wird zu statischen Zeichnungen gerendert. Das ist das, was Sie möchten, wenn das Formular ausgefüllt wurde und das Ergebnis als unveränderliches Dokument weitergegeben werden soll. Dasselbe Feld, derselbe Code, zwei völlig unterschiedliche Dokumente, je nachdem, ob Sie es einfrieren.

Auswahl des richtigen Verbs

Die vier Aktionen lassen sich sauber danach unterteilen, was sie beeinflussen. Eine benannte Aktion verschiebt den Viewport und benötigt kein Feld. Eine Hide-Aktion ändert die Sichtbarkeit und benötigt Feldtitel, wobei die String-vs-Array-Kodierung für Sie erledigt wird. Eine Import-Data-Aktion greift auf eine Datei auf der Festplatte zu und ist daher in PDF/A tabu. Eine JavaScript-Aktion führt beliebige Logik aus und wird am besten in ein globales Funktionspaket und kleine, aktionsspezifische Aufrufe aufgeteilt. Greifen Sie zur einfachsten Lösung, die die Aufgabe erfüllt: Eine Hide-Aktion ist portabler als ein Skript, das ein Hidden-Flag setzt, und eine benannte Aktion ist langlebiger als ein gespeichertes Seitenziel, da keine Seitenzahl gepflegt werden muss.

Von hier aus runden zwei benachbarte Themen das Bild ab. Wenn das Formular Teil eines barrierefreien Dokuments ist, wird der Strukturbaum, dem Screenreader folgen, in unserem Artikel über getaggte PDFs und Barrierefreiheitsstrukturen behandelt. Wenn das ausgefüllte Formular gesperrt und signiert werden muss, wird der Workflow im Leitfaden für die Compliance- und Signatur-Workbench beschrieben. Alle drei bauen auf derselben Engine auf, die als PDF-Bibliothek für Delphi zusammen mit den an anderer Stelle in diesem Blog behandelten APIs zur Erstellung, für Formulare und Signaturen ausgeliefert wird.