Ein Formular, dessen Summen sich in Acrobat neu berechnen, aber in Ihrem eigenen Delphi-Viewer eingefroren bleiben, ist fast nie ein Rendering-Fehler. PDFium deaktiviert jegliches JavaScript im Dokument, es sei denn, der Host stellt eine JS-Plattform bereit. Die PDFium-Komponente bindet diese Plattform ab Version 2.13.2 automatisch ein, sodass AcroForm-JavaScript, Skripte auf Dokumentebene und Feldberechnungen immer dann ausgeführt werden, wenn die V8-aktivierte DLL geladen ist. Der Host behält die Kontrolle über eine Reihe von Ereignissen, die alles, was das Skript zu tun versucht, anzeigen, umleiten oder blockieren können
Dieser eine Satz beantwortet eine Support-Frage, die uns in verschiedenen Formen begegnet ist: „Mein Bestellformular berechnet die Zeilensummen in Acrobat, aber nicht in meiner App“, „app.alert wird nie ausgelöst“, „das Datumsfeld formatiert sich nicht selbst“. Alle diese Probleme lassen sich auf denselben Vertrag zurückführen. Es lohnt sich, zehn Minuten in dessen Verständnis zu investieren, da derselbe Vertrag auch Ihre Sicherheitsgrenze gegenüber potenziell schädlichen Dokumenten darstellt
Warum funktionieren PDF-Formularberechnungen in PDFium nicht?
PDFium behandelt Dokument-JavaScript strikt als Opt-in: Wenn das Element m_pJsPlatform der Form-Fill-Umgebung NULL ist, wird jedes Skript im Dokument stillschweigend übersprungen. Kein Fehler, keine Protokollzeile, keine teilweise Ausführung. Berechnete Felder behalten ihren zuletzt gespeicherten Wert, app.alert verschwindet, Formatierungsskripte werden nie ausgeführt. Acrobat, das immer seine eigene JavaScript-Engine mitbringt, führt dieselbe Datei problemlos aus. Deshalb sieht die Diskrepanz so sehr nach einem Fehler in Ihrem Code aus, obwohl es sich in Wirklichkeit um eine bewusste Voreinstellung der zugrunde liegenden Bibliothek handelt
Die PDFium-Komponente hatte hierzu eine zweite, historische Ebene. Bis zur Version 2.13.1 band die Schnittstelle m_pJsPlatform nur innerhalb des XFA-Initialisierungszweigs ein. Daher erhielten einfache AcroForm-Dokumente, die die überwiegende Mehrheit der geskripteten PDFs ausmachen, überhaupt keine JavaScript-Plattform, selbst wenn die V8-aktivierte DLL vorhanden war. Version 2.13.2 hat diese Einbindung aus der XFA-Entscheidung herausgelöst: InitializeFormFill baut nun die Tabelle IPDF_JsPlatform immer dann auf, wenn V8FeaturesAvailable den Wert True zurückgibt, unabhängig davon, ob es sich bei dem Dokument um ein XFA-Dokument handelt. Das praktische Ergebnis ist, dass Skripte auf Dokumentebene, Feldberechnungsketten und app.alert nun auch in normalen AcroForm-Dokumenten ausgeführt werden
Welche V8-DLL benötigt AcroForm-JavaScript in Delphi?
AcroForm-JavaScript benötigt ein mit der V8-Engine kompiliertes PDFium-Binary, das die PDFium-Komponente als pdfium.v8.dll lädt, wenn das globale Flag EnableV8Engine vor dem ersten Laden der Bibliothek auf True gesetzt ist. Es gibt hier eine Falle, die man deutlich aussprechen sollte: Die Komponente erkennt XFA-Dokumente automatisch durch Vorab-Scannen nach /XFA-Markern und setzt EnableV8Engine für Sie um. Ein einfaches AcroForm-Dokument mit JavaScript enthält jedoch keinen XFA-Marker, sodass keine automatische Erkennung stattfindet. Wenn Ihr Viewer Formular-Skripte ausführen soll, setzen Sie das Flag selbst einmalig vor dem Laden des ersten Dokuments. Sobald eine einfache pdfium.dll in den Prozess geladen wurde, kann die Engine nicht mehr gewechselt werden
// WRONG: matches /Length1, /Length2, /Length3 as well
function ReadStreamLength(const Dict: AnsiString): Integer;
var
P: Integer;
begin
Result := -1;
P := Pos('/Length', Dict);
if P > 0 then
Result := ReadIntAt(Dict, P + Length('/Length'));
end;
Host-Ereignisse für Alerts, Drucken, E-Mail und Formularübermittlung
Jede sichtbare Aktion, die ein Skript ausführt, wird über TPdf-Ereignisse an Ihren Code zurückgeleitet, und jedes dieser Ereignisse verhält sich standardmäßig als sichere Nulloperation (No-op), wenn es nicht zugewiesen ist. OnJavaScriptAlert empfängt die Nachricht, den Titel, den Schaltflächentyp und das Symbol von app.alert und lässt Sie das Ergebnis der gedrückten Schaltfläche zurückgeben. OnJavaScriptResponse bedient app.response-Eingabeaufforderungen einschließlich des Passwort-Flags. OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail und OnJavaScriptSubmitForm machen die Beep-, Druck-, Mail- und Absende-Anforderungen mit ihren vollständigen Parametersätzen zugänglich. Ein Viewer, der keines dieser Ereignisse zuweist, rendert und berechnet dennoch korrekt; das Dokument kann lediglich keine Dialoge öffnen, drucken oder irgendetwas senden
procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
const Msg, Title: WString; ButtonType, Icon: Integer;
var Result_: Integer; var Handled: Boolean);
begin
// Route app.alert through the VCL so it is owned by our window
Result_ := MessageBox(Handle, PWideChar(Msg), PWideChar(Title),
MB_OK or MB_ICONINFORMATION);
Handled := True;
end;
procedure TViewerForm.PdfJavaScriptSubmitForm(Sender: TObject;
const Url: WString; const Data: TBytes);
begin
// Nothing is transmitted unless this handler chooses to send it
LogAudit(Format('Document requested form submit to %s (%d bytes)',
[Url, Length(Data)]));
end;
Die Aufteilung des Standardverhaltens ist für die Bedrohungsanalyse wichtig. OnJavaScriptMail und OnJavaScriptSubmitForm sind benachrichtigungsorientiert: Die PDFium-Komponente führt selbstständig keine Netzwerk- oder MAPI-Aktionen aus. Ein schädliches Dokument, das this.submitForm or this.mailDoc bei einem unvorbereiteten Host aufruft, erreicht also absolut nichts. Der Host muss sich explizit entscheiden (Opt-in), indem er einen Handler zuweist und den Transport selbst implementiert. Das bedeutet, dass die Entscheidung, Daten vom Computer zu übertragen, immer bei Ihnen liegt — getroffen in Ihrem Code, mit der URL und den Nutzdaten vorliegend
Wie verhindert man, dass ein schädliches PDF URLs aufruft?
URI-Aktionen sind der einzige Bereich, in dem das historische Standardverhalten aktiv statt passiv war, und deshalb haben sie ein dediziertes Veto erhalten. Vor der Version 2.13.1 führte der Callback FFI_DoURIActionWithKeyboardModifier bedingungslos jede URI per Shell-Befehl aus, die von einem XFA-Feld geliefert wurde, was einem schädlichen Dokument die Möglichkeit gab, beim Klicken beliebige URLs aufzurufen. OnXfaUriAction schließt diese Lücke: Die Komponente versucht das Ereignis vor der Ausführung zu konsultieren, und ein Handler, der Handled auf True setzt, unterdrückt das standardmäßige ShellExecuteW vollständig. Wenn das Ereignis nicht zugewiesen bleibt, wird das alte Verhalten aus Kompatibilitätsgründen beibehalten. Daher sollte ein sicherheitsbewusster Viewer dieses Ereignis immer zuweisen
procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
// Veto anything that is not plain https; the default would
// otherwise ShellExecuteW the URI as-is
if not SameText(Copy(Uri, 1, 8), 'https://') then
begin
LogAudit('Blocked URI action: ' + Uri);
Handled := True;
end;
end;
Eine Erlaubnisliste (Allowlist) für das Schema ist das Minimum; ein strengerer Viewer fragt beim Benutzer nach oder leitet alles über eine eigene, in einer Sandbox ausgeführte Browserkomponente. Dieselbe Veto-Philosophie erstreckt sich über die Ereignisse von Version 2.13.1: OnXfaEmail, OnXfaHttpRequest, und OnXfaOpenFile legen jeweils die Textparameter der angeforderten Aktion offen, während die Komponente selbst keinen Netzwerk- oder Dateitransport durchführt. Wie diese Schutzmechanismen in eine umfassendere Strategie für nicht vertrauenswürdige Dokumente (einschließlich Rendering-Isolierung) passen, ist Thema von Erstellung einer sicheren PDF-Vorschau in Delphi
Schreiben in das fokussierte Feld mit SetFocusedFormFieldText
TPdf.SetFocusedFormFieldText, hinzugefügt in Version 2.13.2, ist das schreibende Gegenstück zu FocusedFormFieldText: Es wählt den aktuellen Inhalt des fokussierten Feldes über FORM_SelectAllText aus und überschreibt ihn mittels FORM_ReplaceSelection, genau so, als ob der Benutzer den Ersatz eingegeben hätte. Da der Text über den interaktiven Bearbeitungspfad eingegeben wird, werden alle an das Feld angehängten Tastenanschlags-, Formatierungs- und Berechnungsskripte auf dieselbe Weise ausgelöst wie bei manueller Eingabe. Dies macht es zur richtigen Methode für das programmgesteuerte Ausfüllen in einem Viewer, der JavaScript aktiv hält. Die Feldnavigation und das Fokusmanagement drumherum werden in Formularfeld-Navigation mit der PDFium-Komponente behandelt
if FPdf.FocusedFormFieldIndex >= 0 then
begin
if not FPdf.SetFocusedFormFieldText('42.50') then
ShowMessage('No focused field accepts text on this page');
// AcroForm: value commits to /V when focus leaves the field.
// XFA: the write stays in the in-memory edit buffer only and
// will not survive a save
end;
Die Asymmetrie der Persistenz in diesem Kommentar ist eine echte Einschränkung, kein Vorwand aus formalen Gründen. Bei AcroForm-Text- und Kombinationsfeldern wird der bearbeitete Wert beim Fokusverlust in den Eintrag /V des Feldes übernommen und bleibt beim Speichern erhalten. Bei XFA-Textfeldern landet der Schreibvorgang nur im Bearbeitungspuffer im Speicher, da PDFium keine öffentliche API bereitstellt, um Widget-Werte wieder mit dem XFA-Datensatzpaket abzugleichen; das Speichern des Dokuments übernimmt diese Änderung nicht. Wenn eine dauerhafte XFA-Datenbearbeitung erforderlich ist, bearbeiten Sie die Datensatz-XML selbst und nicht das Widget
Grenzen, die man vor der Auslieferung kennen sollte
Zwei wesentliche Grenzen bleiben bestehen. Erstens ist die von PDFium implementierte JavaScript-API eine funktionierende Teilmenge des Acrobat-Objektmodells, die sich auf den Kern der Formularskripterstellung konzentriert: Feldzugriff, Berechnungs- und Formatierungsereignisse, app.alert und app.response, Druck-, Mail- und Submit-Anforderungen. Dokumente, die auf exotische Acrobat-eigene Objekte angewiesen sind, weisen Funktionseinbußen auf, meist stillschweigend. Testen Sie daher die tatsächlichen Formulare Ihrer Benutzer, anstatt eine vollständige Gleichwertigkeit anzunehmen. Zweitens bedeutet die Aktivierung von V8 die Auslieferung von pdfium.v8.dll, einer wesentlich größeren Binärdatei als das Standard-Build. Ein Viewer, der niemals Skripte oder XFA benötigt, kann legitimerweise die kleinere DLL behalten und m_pJsPlatform ungebunden lassen, was an sich eine sinnvolle Sicherheitsmaßnahme darstellt
Das daraus resultierende Muster ist erfreulich unspektakulär: Setzen Sie EnableV8Engine beim Start, weisen Sie die Alert- und Response-Ereignisse zu, damit Dialoge nativ aussehen, weisen Sie OnXfaUriAction zu und protokollieren Sie die Mail- und Submit-Ereignisse, und belassen Sie alles andere bei der Standard-Nulloperation, bis eine Anforderung etwas anderes verlangt. Die vollständige Ereignisreferenz und die Schnittstelle zum Ausfüllen von Formularen sind auf der Produktseite der PDFium-Komponente dokumentiert