Technischer Artikel

AcroForm-Felder zu einer geladenen PDF-Datei in Delphi hinzufügen

Sie haben eine Rechnungsvorlage eines Drittanbieters oder einen archivierten Vertrag, den jemand vor Jahren mit einer Software erstellt hat, die niemand mehr finden kann, und die Anforderung besteht darin, ihn interaktiv zu machen: Platzieren Sie ein Unterschriftenfeld in der Ecke, fügen Sie ein paar Textfelder hinzu, verwandeln Sie eine einfache Checkliste in echte Kontrollkästchen. Der Haken an der Sache ist, dass Sie diese PDF-Datei nicht von Grund auf neu erstellen. Sie existiert bereits, sie hat bereits Seiten, Inhaltsströme und Schriftarten, die Sie nicht kontrollieren, und Sie müssen AcroForm-Widgets in diesen Objektgraphen einpflegen, ohne ihn neu aufzubauen. Das ist ein anderes Problem als das Erstellen eines Formulars in einem neuen Dokument, und der Teil, der die Leute stolpern lässt, ist unsichtbar, bis Sie das Ergebnis in einem Viewer öffnen und die gerade geschriebenen Felder nirgendwo auf der Seite zu sehen sind

HotPDF ist eine native VCL-PDF-Komponente für Delphi und C++Builder, und ab Version v2.247.0 stellt sie eine spezielle Familie von Methoden genau dafür bereit: das Erstellen aller sechs Standardfeldtypen direkt in einem mit LoadFromFile geladenen Dokument. Dieser Artikel führt Sie durch die Funktionsweise dieser Methoden, das von ihnen erstellte ISO-32000-1-Dictionary und das eine Flag, ohne das die gesamte Übung im Stillen eine leer aussehende Datei erzeugt

Warum die Felderstellung in geladenen Dokumenten ein eigenständiger Codepfad ist

Wenn Sie eine PDF-Datei aus dem Nichts erstellen, besitzt HotPDF das gesamte Objektmodell. Jede Seite ist ein beschreibbarer THPDFPage-Wrapper, und das Hinzufügen eines Textfeldes über AddTextField verdrahtet das neue Widget mit dem Anmerkungsobjekt (Annotation Object) der Seite, dem Seitenobjekt und der Feldsammlung des Formulars und generiert dann einen Appearance Stream (Erscheinungsstrom) aus den Schriftressourcen des Dokuments. Der Appearance Stream ist die sichtbare Oberfläche des Widgets, das Feld und der Rahmen sowie standardmäßiger Text, gezeichnet als PDF-Zeichenoperatoren, die der Viewer eins zu eins wiedergibt

Ein geladenes Dokument bietet Ihnen nichts von diesem Gerüst. Die Seiten wurden als rohe Dictionaries eingelesen; es gibt keinen beschreibbaren THPDFPage-Wrapper, an den ein Widget angehängt werden könnte, und was noch wichtiger ist, es steht keine Schriftressourcen-Pipeline zur Verfügung, um Appearance Streams zu zeichnen. Der geladene Pfad nimmt daher eine andere Route. Er schreibt die Feld-Dictionaries direkt in den analysierten Objektgraphen und spricht Seiten über einen nullbasierten Index anstelle eines Seitenobjekts an. Die Feldtypen und die Flag-Bits entsprechen exakt dem Pfad für die Neuerstellung, sodass ein Textfeld in jedem Fall ein Textfeld bleibt; was sich ändert, ist die darunter liegende Struktur und vor allem die Art und Weise, wie die Oberfläche des Widgets gezeichnet wird

Das /NeedAppearances-Flag ist hier nicht optional

Dies ist die einzige Tatsache, die darüber entscheidet, ob Ihre Arbeit angezeigt wird. Da der geladene Pfad keine Appearance Streams generiert, kommt ein neu hinzugefügtes Widget beim Viewer ohne einen /AP-Eintrag an: ein Feld ohne beschriebene Oberfläche. Viele Viewer zeichnen überhaupt nichts, wenn sie ein Widget rendern sollen, das kein Erscheinungsbild und keine Anweisung zum Erstellen eines solchen hat. Das Feld ist in der Datei vorhanden, strukturell gültig, für ein Formularausfüllwerkzeug adressierbar und für einen Menschen völlig unsichtbar

Der Ausweg ist in ISO 32000-1 §12.7.3 definiert: Das AcroForm-Dictionary enthält einen /NeedAppearances-Boolean, und wenn dieser true ist, muss ein konformer Reader die fehlenden Appearance Streams selbst aus dem /DA-String (Default Appearance) und dem Wert des jeweiligen Feldes erstellen. HotPDF übernimmt dies für Sie. Wenn Sie zum ersten Mal ein Feld zu einem geladenen Dokument hinzufügen, wird EnsureLoadedAcroForm ausgeführt: Wenn der Katalog kein /AcroForm hat, wird eines erstellt, wenn kein /Fields-Array vorhanden ist, wird dieses erstellt, und es wird /NeedAppearances true erzwungen. Sie rufen es nicht direkt auf, aber das Wissen um seine Existenz erklärt das Verhalten. Es erklärt auch einen Bereitstellungshinweis, der deutlich ausgesprochen werden sollte: Einige wenige minimale oder nicht konforme Viewer ignorieren /NeedAppearances und rendern weiterhin nichts. Für Mainstream-Reader erfüllt das Flag seine Aufgabe, aber wenn Ihre Zielgruppe einen ungewöhnlichen eingebetteten Renderer verwendet, sollten Sie dies dort testen, bevor Sie etwas versprechen

Hinzufügen der sechs Feldtypen

Jede Methode folgt dem gleichen Muster. Sie übergeben den nullbasierten Seitenindex, die vier Ecken des Widget-Rechtecks in PDF-Benutzerraum-Koordinaten, den Feldnamen und alle weiteren Argumente, die der Typ benötigt. Das Rechteck ist X1, Y1, X2, Y2 mit dem PDF-Ursprung unten links auf der Seite, sodass größere Y-Werte weiter oben liegen; dies ist die Koordinatenkonvention aus dem Dateiformat, nicht die Konvention oben links auf dem Bildschirm, und diese zu vertauschen ist der zweithäufigste Fehler nach dem Vergessen des Flags. Jeder Aufruf gibt den nullbasierten Index des neuen Feldes zurück, oder -1, wenn der Seitenindex außerhalb des Bereichs lag oder das Seitenobjekt nicht aufgelöst werden konnte

var
  Pdf: THotPDF;
  Idx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('contract.pdf') <= 0 then Exit;

    // Text field: name, initial value, max length (0 = unlimited)
    Idx := Pdf.AddLoadedTextField(0, 72, 680, 320, 700, 'FullName', '', 0);

    // CheckBox: export value, initial checked state
    Pdf.AddLoadedCheckBox(0, 72, 640, 90, 658, 'AgreeTerms', 'Yes', False);

    // Signature field: just a name and a rectangle
    Pdf.AddLoadedSignatureField(0, 360, 72, 540, 132, 'ApproverSig');

    if Idx >= 0 then
      Pdf.SaveLoadedDocument('contract-interactive.pdf');
  finally
    Pdf.Free;
  end;
end;

Das dritte und vierte String-Argument des Textfeldes sind der Feldname und sein anfänglicher /V-Wert; die Ganzzahl ist /MaxLen, das nur geschrieben wird, wenn es größer als Null ist. HotPDF weist jedem editierbaren Feld eine standardmäßige Appearance-Zeichenfolge von /Helv 12 Tf 0 0 0 rg zu. Dies liest ein Viewer, der /NeedAppearances unterstützt, um Schriftart und Farbe zu bestimmen, in der er den Wert darstellt. Das Kontrollkästchen übernimmt einen Exportwert – die Zeichenfolge, die das Formular sendet, wenn das Feld angekreuzt ist – sowie einen Boolean für den Ausgangszustand. Intern schreibt es die entsprechenden Namenseinträge /V, /AS und /DV, sodass der Ein-/Aus-Zustand beim Öffnen der Datei konsistent ist. Ein leerer Exportwert wird standardmäßig auf Yes gesetzt, den herkömmlichen Aktivierungsnamen für Kontrollkästchen

Auswahlfelder und die /Ff-Bit-Flags

ComboBox und ListBox sind beides Auswahlfelder (Choice Fields), Feldtyp /Ch in ISO 32000-1 §12.7.4. Der Unterschied zwischen einem Dropdown-Menü und einer Scroll-Liste ist ein Bit im Feld-Flag-Integer /Ff: Bit 18, das Combo-Flag, Wert $40000. HotPDF setzt dieses Bit für AddLoadedComboBox und lässt es für AddLoadedListBox frei; ansonsten sind die beiden identisch, und beide übernehmen ihre Auswahlmöglichkeiten als offenes Array von Strings, die in den /Opt-Eintrag geschrieben werden

// Dropdown (Combo flag set internally) with an initial selection
Pdf.AddLoadedComboBox(0, 72, 600, 300, 620, 'Country', 'Canada',
  ['United States', 'Canada', 'Mexico']);

// Scrolling list, no initial value
Pdf.AddLoadedListBox(0, 72, 520, 300, 590, 'Priority', '',
  ['Low', 'Normal', 'High']);

// Push button with a caption drawn through /MK
Pdf.AddLoadedPushButton(0, 360, 600, 480, 626, 'SubmitBtn', 'Submit');

Zwei Anmerkungen zur Optionsliste: HotPDF schreibt jeden /Opt-Eintrag als einfachen String, bei dem der Exportwert und die angezeigte Beschriftung identisch sind. ISO 32000-1 §12.7.4.4 erlaubt auch das zweielementige Format [export display], wenn der übermittelte Wert von dem abweichen soll, was der Benutzer liest. Die geladenen Erstellungsmethoden verwenden die einfachere Einzel-String-Form. Wenn Sie also abweichende Export- und Anzeigewerte benötigen, müssen Sie diese selbst im resultierenden Dictionary festlegen. Und der Wert, den Sie als aktuelle Auswahl des Feldes übergeben, sollte eine der von Ihnen angegebenen Optionen sein, da der Viewer ihn mit der Liste abgleicht

Die Schaltfläche (PushButton) is der andere flaggesteuerte Fall: Feldtyp /Btn mit Bit 17, dem PushButton-Flag, Wert $10000. Dieses Bit unterscheidet eine klickbare Schaltfläche von einem Kontrollkästchen, das ebenfalls ein /Btn-Feld ist, aber ohne dieses Bit. Die von Ihnen übergebene Beschriftung (Caption) wird in das Appearance Characteristics Dictionary /MK als normale Beschriftung /CA geschrieben. Es lohnt sich, hier ehrlich über den Umfang zu sein: Die Schaltfläche wird mit ihrer Beschriftung und ihrem Rechteck erstellt, aber die geladene Erstellungsmethode hängt keine Aktion an, sodass sie für sich genommen eine Schaltfläche ist, die richtig aussieht, aber beim Anklicken nichts bewirkt. Das Einrichten von Submit-, Reset- oder JavaScript-Aktionen ist ein separates Thema. Für die Neuerstellung von Grund auf wird der Arbeitsablauf mit Feldern und Aktionen in AcroForm-Felder und -Aktionen in Delphi erstellen behandelt, was der richtige Vergleichspunkt für das ist, was der geladene Pfad bewusst auslässt

Das Dictionary, das sich alle Felder teilen

Hinter allen sechs Methoden steht ein gemeinsamer Builder, der die Widget-Annotation erstellt und an zwei Stellen registriert. Er schreibt /Type /Annot und /Subtype /Widget, das /Rect-Array aus Ihren vier Koordinaten, die Annotations-Flags /F 4 (wodurch das Print-Bit gesetzt wird, damit das Feld auf Papier und auf dem Bildschirm erscheint), den Feldnamen /T, den Feldtyp /FT, die Flags /Ff und eine Rückreferenz /P auf das Seitenobjekt. Anschließend hängt er das neue Feld an das /Fields-Array des AcroForms und an das /Annots-Array dieser Seite an, wobei er indirekte Referenzen auf dem Weg auflöst, sodass die tatsächlichen Arrays erweitert werden, anstatt das Widget verwaisen zu lassen

Diese doppelte Registrierung ist wichtig, da ein Widget, das nur in einer der beiden Listen enthalten ist, auf subtile Weise fehlerhaft ist. Ein Feld, das in /Fields vorhanden ist, aber in den /Annots der Seite fehlt, ist dem Formular bekannt, wird aber nie gezeichnet; der umgekehrte Fall wird gezeichnet, ist aber der Formularlogik unbekannt. HotPDF hält bei jedem Hinzufügen beide synchron, was die Art von Buchhaltung ist, die Sie andernfalls manuell exakt nach der Spezifikation durchführen müssten

Ein paar ehrliche Grenzen

Setzen Sie Erwartungen, bevor Sie darauf einen Arbeitsablauf aufbauen. Das Flatten-and-Regenerate-Verhalten hängt davon ab, ob der Viewer /NeedAppearances unterstützt. Dies gilt für Acrobat, moderne Browser-PDF-Engines und gängige Desktop-Reader, ist jedoch keine feste Garantie für jeden Renderer auf dem Markt. Wenn Sie eine Datei erstellen müssen, deren Felder überall identisch gerendert werden, auch in Viewern, die das Flag ignorieren, befinden Sie sich im Bereich der Appearance Streams, und der Neuerstellungspfad, der /AP für Sie zeichnet, ist besser geeignet. Das Unterschriftenfeld wird ebenfalls als leeres Signatur-Widget erstellt, das bereit zum Signieren ist; das Platzieren des Feldes ist nicht dasselbe wie das Anwenden einer kryptografischen Signatur

Um zu ändern, was bereits vorhanden ist, anstatt etwas hinzuzufügen, ist die entsprechende Operation das Formular-Flattening (Abflachen), bei dem Sie interaktive Felder wieder in statische Seiteninhalte einbacken, sodass die Werte dauerhaft und uneditierbar werden. Dieser Prozess, einschließlich des Umgangs mit XFA-haltigen Formularen, wird in XFA- und AcroForm-Felder in Delphi abflachen besprochen. Das Hinzufügen und das Abflachen von Feldern sind zwei Enden desselben Lebenszyklus: Dieser Artikel beschreibt, wie Sie Interaktivität in ein Dokument einbringen, dem sie fehlte, und das Abflachen ist der Weg, sie wieder zu entfernen, sobald das Formular seinen Zweck erfüllt hat

Die hier gezeigte Formular-API für geladene Dokumente wird als Teil der standardmäßigen HotPDF Component für Delphi und C++Builder ausgeliefert, zusammen mit der vollständigen Referenz für Feld-Flags, Appearance-Handling und dem Rest des AcroForm-Modells