A form whose totals recalculate in Acrobat but sit frozen in your own Delphi viewer is almost never a rendering bug. PDFium disables all document JavaScript unless the host attaches a JS platform, and PDFium Component wires that platform automatically from version 2.13.2 onward, so AcroForm JavaScript, doc-level scripts, and field calculations execute whenever the V8-enabled DLL is loaded. The host stays in charge through a set of events that can display, redirect, or veto everything the script tries to do
That one sentence resolves a support question we have seen in several forms: "my order form computes line totals in Acrobat but not in my app", "app.alert never fires", "the date field does not format itself". All of them trace back to the same contract, and understanding it is worth ten minutes because the same contract is also your security boundary against hostile documents
Why do PDF form calculations not work in PDFium?
PDFium treats document JavaScript as strictly opt-in: if the m_pJsPlatform member of the form-fill environment is NULL, every script in the document is silently skipped. No error, no log line, no partial execution. Calculated fields keep their last stored value, app.alert vanishes, format scripts never run. Acrobat, which always ships its own JavaScript engine, happily runs the same file, which is why the discrepancy looks so much like a bug in your code when it is actually a deliberate default in the library underneath
PDFium Component had a second, historical layer to this. Through version 2.13.1 the binding only attached m_pJsPlatform inside the XFA initialisation branch, so plain AcroForm documents, which are the overwhelming majority of scripted PDFs, never got a JavaScript platform at all even when the V8-enabled DLL was present. Version 2.13.2 pulled the attachment out of the XFA decision: InitializeFormFill now builds the IPDF_JsPlatform table whenever V8FeaturesAvailable returns True, independent of whether the document is XFA. The practical result is that doc-level scripts, field calculation chains, and app.alert now execute in ordinary AcroForm documents too
Which V8 DLL does AcroForm JavaScript need in Delphi?
AcroForm JavaScript needs a PDFium binary compiled with the V8 engine, which PDFium Component loads as pdfium.v8.dll when the global EnableV8Engine flag is True before the library first loads. There is one trap here worth stating plainly: the component auto-detects XFA documents by pre-scanning for /XFA markers and flips EnableV8Engine for you, but a plain AcroForm document with JavaScript carries no XFA marker, so no auto-detection happens. If your viewer should run form scripts, set the flag yourself, once, before the first document loads; after a plain pdfium.dll is loaded the process cannot switch engines
uses PDFium;
procedure TViewerForm.FormCreate(Sender: TObject);
begin
// Must run before the singleton PDFium library first loads;
// once plain pdfium.dll is in the process it cannot be swapped
EnableV8Engine := True;
end;
procedure TViewerForm.OpenDocument(const FileName: string);
begin
FPdf.LoadDocument(FileName);
if not V8FeaturesAvailable then
StatusBar.SimpleText :=
'JavaScript disabled: pdfium.v8.dll not deployed';
end;
V8FeaturesAvailable is the honest runtime probe: it reports whether the loaded binary actually resolves the V8-dependent exports, so the check works no matter which DLL an installer ended up deploying. The same engine and the same flag also power dynamic XFA rendering; the background on how XFA detection and the V8 auto-selection interact is covered in detecting XFA forms and extracting XFA packets with PDFium
Host events for alerts, print, mail, and form submission
Every visible thing a script does routes back to your code through TPdf events, and every one of them defaults to a safe no-op when unassigned. OnJavaScriptAlert receives the message, title, button type, and icon from app.alert and lets you return the pressed-button result; OnJavaScriptResponse services app.response input prompts including the password flag; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail, and OnJavaScriptSubmitForm surface the beep, print, mail, and submit requests with their full parameter sets. A viewer that assigns none of these still renders and calculates correctly; the document simply cannot open dialogs, print, or send anything
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;
The default-behaviour split matters for threat modelling. OnJavaScriptMail and OnJavaScriptSubmitForm are notification-style: PDFium Component performs no network or MAPI action on its own, so a malicious document that calls this.submitForm or this.mailDoc against an unprepared host accomplishes exactly nothing. The host must opt in by assigning a handler and implementing the transport itself, which means the decision to move data off the machine is always yours, made in your code, with the URL and payload in hand
How do you stop a malicious PDF from launching URLs?
URI actions are the one place where the historical default was active rather than passive, and that is why they got a dedicated veto. Before version 2.13.1 the FFI_DoURIActionWithKeyboardModifier callback shell-executed whatever URI an XFA field supplied, unconditionally, which handed a malicious document the ability to launch arbitrary URLs on click. OnXfaUriAction closes that: the component consults the event before executing, and a handler that sets Handled to True suppresses the ShellExecuteW default entirely. Leaving the event unassigned preserves the old behaviour for compatibility, so a security-conscious viewer should always assign it
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;
An allow-list on the scheme is the minimum; a stricter viewer confirms with the user or routes everything through its own sandboxed browser component. The same veto philosophy extends across the v2.13.1 event set: OnXfaEmail, OnXfaHttpRequest, and OnXfaOpenFile each expose the textual parameters of the requested action while the component itself performs no network or file transport. How these guards fit into a broader untrusted-document strategy, including rendering isolation, is the subject of building a secure PDF preview in Delphi
Writing to the focused field with SetFocusedFormFieldText
TPdf.SetFocusedFormFieldText, added in version 2.13.2, is the write-side companion to FocusedFormFieldText: it selects the focused field's current content via FORM_SelectAllText and overwrites it through FORM_ReplaceSelection, exactly as if the user had typed the replacement. Because the text enters through the interactive editing path, any keystroke, format, and calculate scripts attached to the field fire the same way they do for manual input, which makes it the right primitive for programmatic filling in a viewer that keeps JavaScript live. Field traversal and focus management around it are covered in form field navigation with PDFium Component
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;
The persistence asymmetry in that comment is a real boundary, not a caveat for form's sake. For AcroForm text and combo fields the edited value commits to the field's /V entry on focus loss and persists across save. For XFA text fields the write lands only in the in-memory edit buffer, because PDFium exposes no public API to reconcile widget values back into the XFA datasets packet; saving the document will not carry the change. If durable XFA data editing is the requirement, edit the datasets XML itself rather than the widget
Limits worth knowing before you ship
Two honest boundaries remain. First, the JavaScript API PDFium implements is a working subset of the Acrobat object model, centred on the form-scripting core: field access, calculation and format events, app.alert and app.response, print, mail, and submit requests. Documents that lean on exotic Acrobat-only objects will degrade, usually silently, so test the actual forms your users handle rather than assuming parity. Second, enabling V8 means shipping pdfium.v8.dll, a substantially larger binary than the plain build; a viewer that never needs scripting or XFA can legitimately keep the smaller DLL and leave m_pJsPlatform unattached, which is a valid security posture in itself
The pattern that falls out of all of this is pleasantly boring: set EnableV8Engine at startup, assign the alert and response events so dialogs look native, assign OnXfaUriAction and audit-log the mail and submit events, and leave everything else at its no-op default until a requirement says otherwise. The complete event reference and the form-fill API surface are documented on the PDFium Component product page