Technical Article

Auto-Calculating PDF Forms in Delphi with HotPDF

Eine Rechnungs- oder Ausgaben-PDF-Datei, in der sich die einzelnen Posten selbst addieren und das Datum sich selbst formatiert, benötigt drei Dinge, die mit ihrem AcroForm verknüpft sind. HotPDF fügt einem geladenen Dokument in Delphi alle drei hinzu: ApplyLoadedFieldCalculation für AFSimple_Calculate-Summen, ApplyLoadedFieldValidation für Acrobat AF-Formatierungs- und Validierungs-Hilfsprogramme und EnsureLoadedFieldAppearanceStream, um den Wert einzubacken, den ein Viewer tatsächlich zeichnet

HotPDF ist eine native VCL-PDF-Komponente für Delphi und C++Builder. Diese drei Methoden arbeiten auf einem Dokument, das Sie mit LoadFromFile geöffnet haben, und nicht auf einem, das Sie von Grund auf neu erstellen. Jede Methode erwartet einen nullbasierten Feldindex in dem /Fields-Array des AcroForms — dieselbe Reihenfolge, die auch GetLoadedFormFieldNames zurückgibt. Die gesamte Aufgabe besteht also aus Laden, Indizieren, Anhängen und SaveLoadedDocument. Das Erstellen der Formularfelder selbst ist eine separate Aufgabe, die unter Hinzufügen von AcroForm-Feldern zu einer geladenen PDF in Delphi beschrieben ist, während dieser Beitrag zeigt, wie Sie bestehende Felder mit Berechnungen und Formatierungen versehen

Warum benötigt ein PDF-Formular Skripte, um seine eigenen Felder zu addieren?

Weil PDF über keine integrierte Tabellenkalkulationsebene verfügt. Ein Textfeld enthält eine Zeichenkette, keine Formel. Wenn sich ein Posten ändert, berechnet nichts in der Datei die Summe neu, es sei denn, es ist ein Skript dafür hinterlegt. Der Mechanismus hierfür ist das Feldverzeichnis für zusätzliche Aktionen, /AA (ISO 32000-1 §12.7.5), das Auslöser-Ereignisse mit Aktionen verknüpft: ein Tastaturanschlag-Auslöser (K) bei der Eingabe von Zeichen, ein Format-Auslöser für die Anzeige, ein Validierungs-Auslöser (V) beim Bestätigen der Eingabe und ein Berechnungs-Auslöser (C), der immer dann ausgelöst wird, wenn sich ein beliebiges Feld im Formular ändert. Jede Aktion ist eine JavaScript-Aktion, /S /JavaScript (ISO 32000-1 §12.6.4.14), und das ausgeführte Skript führt die Summe oder die Neuformatierung durch. Acrobat liefert eine Bibliothek vorgefertigter Helfer mit — AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx und weitere —, sodass das verknüpfte Skript meist aus einem einzeiligen Aufruf dieser Bibliothek besteht statt aus individuellem Code

Ein wichtiges Detail entscheidet darüber, ob ein mehrstufiges Formular das richtige Ergebnis liefert: die Berechnungsreihenfolge. Acrobat führt jedes Feld aus, das eine Berechnungsaktion enthält, und zwar in der Reihenfolge, in der das /CO-Array des AcroForms sie auflistet, nicht in der Reihenfolge der Seiten. ApplyLoadedFieldCalculation fügt die Berechnungsaktion an das von Ihnen benannte Feld an. Wenn ein berechnetes Feld in ein anderes einfließt — z. B. eine Zwischensumme, die in eine Steuerzeile einfließt, und dann eine Gesamtsumme, die beide Werte einliest —, müssen Sie sicherstellen, dass das /CO-Verzeichnis jeden Eingabewert vor dem Feld auflistet, das ihn verarbeitet. Andernfalls liest die Gesamtsumme einen veralteten Wert der Zwischensumme ein und hinkt der Bearbeitung um einen Schritt hinterher

Wie lässt man ein PDF-Formular Summen automatisch berechnen?

ApplyLoadedFieldCalculation erwartet den Index des Zielfeldes, eine Operation und die Namen der Quellfelder, die aggregiert werden sollen. HotPDF schreibt AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) in die Berechnungsaktion des Zielfeldes. Die Enumeration THPDFFieldCalcOp wählt dabei die Funktion: fccSum, fccAverage, fccProduct, fccMinimum und fccMaximum entsprechen den Acrobat-Funktionen SUM, AVG, PRD, MIN und MAX. Die Quellnamen sind AcroForm-Feldnamen, also genau die Zeichenketten, die GetLoadedFormFieldNames zurückgibt, sodass die Summierung über drei Posten und die Versandkosten in einem einzigen Aufruf erfolgt

var
  Pdf: THotPDF;
  Names: THPDFAnsiStringArray;
  i, TotalIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    Names := Pdf.GetLoadedFormFieldNames;
    TotalIdx := -1;
    for i := 0 to High(Names) do
      if Names[i] = 'grandTotal' then TotalIdx := i;
    if TotalIdx >= 0 then
    begin
      Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
        ['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
      Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
    end;
    Pdf.SaveLoadedDocument('invoice-live.pdf');
  finally
    Pdf.Free;
  end;
end;

Der Aufruf von EnsureLoadedFieldAppearanceStream für das Summenfeld ist keine rein kosmetische Maßnahme. Die Berechnung findet innerhalb des Viewers statt, sodass das Summenfeld keinen eigenen gerenderten Inhalt besitzt, bis ein skriptfähiger Reader die Datei öffnet. Das Einbacken eines Appearance-Streams (Darstellungsdaten) liefert in der Zwischenzeit einen Anzeigewert, den ein Druckjob oder ein Vorschaubildgenerator auslesen kann, wenn diese das Skript nicht ausführen

Formatieren und Validieren von Feldern mit der AF-Hilfsprogramm-Suite

ApplyLoadedFieldValidation verknüpft die Formatierungs-, Tastenanschlags- und Validierungs-Skripte in einem einzigen Aufruf. Welcher Acrobat-Helfer eingebunden wird, entscheidet das übergebene THPDFFieldValidationKind. Übergeben Sie fvkCurrency, schreibt HotPDF AFNumber_Format(2,0,0,0,"$ ",true) als Formatierungsskript mit dem passenden AFNumber_Keystroke. fvkNumber verwendet "." für das Dezimaltrennzeichen-Argument, fvkPercentage schreibt AFPercent_Format und fvkDate schreibt AFDate_FormatEx("dd/mm/yyyy"), es sei denn, Sie übergeben Ihre eigene Maske im Argument DateFormat. Die speziellen Formate — fvkZipCode, fvkSSN und fvkPhone — werden auf AFSpecial_Format abgebildet mit dem Code, den Acrobat für das jeweilige Format reserviert. fvkArbitraryMask erwartet eine Masken-Zeichenkette im Argument Mask für alle Fälle, die nicht durch diese Standardeinstellungen abgedeckt sind. Für die numerischen Typen fügt HotPDF zudem ein AFRange_Validate-Skript an, damit ungültige Eingaben beim Bestätigen abgewiesen werden

// Format the invoice date as ISO, the amount as currency, and mask a tax id
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

Die AF-Helfer decken die gängigen Formen ab, sodass Sie JavaScript für diese Fälle niemals manuell schreiben müssen. Wenn ein Feld eine Logik benötigt, die durch keine Standardeinstellung ausgedrückt werden kann — z. B. eine Prüfsumme, eine feldübergreifende Regel oder ein benutzerdefinierter regulärer Ausdruck —, verknüpfen Sie das rohe Skript manuell über die Formularfeld-Aktionsmethoden, wie unter Erstellen von AcroForm-Feldern und Aktionen mit HotPDF beschrieben, die eine JavaScript-Zeichenkette direkt entgegennehmen. Beide Wege schreiben in dasselbe /AA-Verzeichnis, sodass ein standardmäßiges Währungsformat für ein Feld und eine handgeschriebene Validierung für ein anderes im selben Dokument nebeneinander existieren können

Warum müssen Sie den Appearance-Stream neu generieren?

Weil der Wert-String (Value) und die Pixel, die ein Viewer zeichnet, zwei verschiedene Dinge in PDF sind. Das Ändern des einen ändert nicht das andere. Ein Feld speichert seinen Wert in /V, aber was ein Reader rendert, stammt aus dem Appearance-Stream des Feldes, /AP /N. Editieren Sie /V über den Objektgraphen, zeigt ein strenger Viewer dennoch die alte Darstellung an, bis diese neu generiert wird. Das Formularverzeichnis enthält ein Flag /NeedAppearances, das den Viewer auffordert, beim Öffnen alle Darstellungen neu zu erstellen, und Acrobat hält sich daran. In Browser eingebettete Renderer, mobile Reader, Druckserver und Vorschaubildgeneratoren ignorieren dieses Flag jedoch häufig und zeichnen den bereits vorhandenen Stream oder gar nichts. EnsureLoadedFieldAppearanceStream schließt diese Lücke, indem es das Form-XObject /AP /N aus den Felddaten /DA, /V und /Rect selbst generiert. So reist der gerenderte Wert innerhalb der Datei mit, statt darauf angewiesen zu sein, dass der Viewer ihn neu zeichnet

// Bake an appearance for every field before saving, so values render everywhere
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

Wo AF-Skripte nicht mehr funktionieren

Jedes AF*-Skript setzt voraus, dass der Viewer über eine JavaScript-Engine verfügt — und die meisten Viewer haben keine. Acrobat und eine Handvoll Desktop-Produkte führen diese Helfer aus. Die in Chrome, Edge, Firefox und den großen mobilen Plattformen integrierten PDF-Viewer sowie serverseitige Rasterizer und Druck-Pipelines führen überhaupt kein Formular-JavaScript aus. In diesen Readern wird die Berechnung niemals ausgelöst und die Formatierung niemals angewendet. Das Feld zeigt einfach den Appearance-Stream an, mit dem es zuletzt gespeichert wurde, weshalb das Einbacken von /AP /N so wichtig ist. Betrachten Sie die AF*-Ebene als Komfortmerkmal für jene Benutzer, die einen skriptfähigen Reader verwenden, niemals als Garantie. Jede Summe, die auf dem Server korrekt sein muss — z. B. ein Rechnungsbetrag, der abgerechnet werden soll, oder eine Steuerzahl, die eingereicht wird —, muss auf Ihrer Seite neu berechnet werden, nachdem das Formular zurückkommt, da Sie nicht davon ausgehen können, dass der Reader, der es ausgefüllt hat, auch nur eine Zeile Ihres Skripts ausgeführt hat

Ein weiterer Fall sollte genannt werden: Formulare, die als XFA vorliegen. Ein dynamisches XFA-Formular enthält seine eigene Berechnungs-Engine in XML-Daten, von denen die AF*-Helfer nichts wissen. Das Verknüpfen von AFSimple_Calculate mit einem XFA-Feld bewirkt daher nichts. Konvertieren Sie das XFA-Formular zuerst in ein einfaches AcroForm, wie unter XFA-zu-AcroForm-Konvertierung in Delphi beschrieben, und die drei hier vorgestellten Methoden lassen sich anschließend genau wie bei jedem anderen geladenen Formular auf die resultierenden AcroForm-Felder anwenden

Sich selbst berechnende und formatierende AcroForms erfordern drei Aufrufe an einem geladenen Dokument — Berechnung anhängen, Formatierung und Validierung anhängen, Darstellung einbacken — gefolgt von SaveLoadedDocument. Die hier gezeigte API für geladene Formulare ist Teil der HotPDF-Komponente für Delphi und C++Builder, welche die vollständigen Enumerationen THPDFFieldValidationKind und THPDFFieldCalcOp sowie den Rest der AcroForm-Schnittstelle dokumentiert