Teknisk artikel

Köra AcroForm-JavaScript i Delphi med PDFium Component

Ett formulär vars totalsummor räknas om i Acrobat men förblir frysta i din egen Delphi-visare är nästan aldrig ett renderingsfel. PDFium inaktiverar allt dokument-JavaScript såvida inte värdprogrammet ansluter en JS-plattform, och PDFium-komponenten kopplar in den plattformen automatiskt från och med version 2.13.2. Det innebär att AcroForm-JavaScript, skript på dokumentnivå och fältberäkningar körs så snart den V8-aktiverade DLL-filen läses in. Värdprogrammet behåller kontrollen via en uppsättning händelser som kan visa, omdirigera eller stoppa (veto) allt som skriptet försöker göra

Den meningen sammanfattar en supportfråga vi har sett i flera olika former: "mitt beställningsformulär beräknar radsummor i Acrobat men inte i min app", "app.alert utlöses aldrig", "datumfältet formaterar inte sig självt". Alla dessa problem går tillbaka till samma mekanism, och att förstå den är värt tio minuter eftersom samma mekanism också utgör din säkerhetsgräns mot skadliga dokument

Varför fungerar inte beräkningar i PDF-formulär i PDFium?

PDFium behandlar dokument-JavaScript som strikt opt-in: om medlemmen m_pJsPlatform i formulärfyllningsmiljön är NULL, hoppas varje skript i dokumentet tyst över. Inga felmeddelanden, inga loggrader, ingen partiell exekvering. Beräknade fält behåller sitt senast lagrade värde, app.alert försvinner, formateringsskript körs aldrig. Acrobat, som alltid levereras med en egen JavaScript-motor, kör filen utan problem, vilket är anledningen till att avvikelsen ser ut som en bugg i din kod när det i själva verket är ett medvetet standardbeteende i det underliggande biblioteket

PDFium-komponenten hade ett andra, historiskt skikt i detta. Fram till version 2.13.1 anslöt bindningen endast m_pJsPlatform inuti XFA-initieringsgrenen, så vanliga AcroForm-dokument (som utgör den överväldigande majoriteten av skriptade PDF-filer) fick aldrig någon JavaScript-plattform alls, även när den V8-aktiverade DLL-filen var närvarande. Version 2.13.2 lyfte ut anslutningen ur XFA-beslutet: InitializeFormFill bygger nu upp tabellen IPDF_JsPlatform så snart V8FeaturesAvailable returnerar True, oberoende av om dokumentet är XFA eller inte. Det praktiska resultatet är att skript på dokumentnivå, fältberäkningskedjor och app.alert nu exekveras även i vanliga AcroForm-dokument

Vilken V8-DLL behöver AcroForm-JavaScript i Delphi?

AcroForm-JavaScript kräver en PDFium-binärfil kompilerad med V8-motorn, vilken PDFium-komponenten laddar som pdfium.v8.dll om den globala flaggan EnableV8Engine är True innan biblioteket laddas för första gången. Det finns en fälla här som är värd att nämna: komponenten upptäcker automatiskt XFA-dokument genom att förhandsskanna efter /XFA-markörer och aktiverar EnableV8Engine åt dig, men ett vanligt AcroForm-dokument med JavaScript bär inte på någon XFA-markör, så ingen automatisk upptäckt sker. Om din visare ska köra formulärskript, ställ in flaggan själv en gång innan det första dokumentet laddas; efter att en vanlig pdfium.dll har lästs in kan processen inte byta motor

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 den tillförlitliga runtime-kontrollen: den rapporterar om den laddade binärfilen faktiskt löser upp de V8-beroende exporterna, så kontrollen fungerar oavsett vilken DLL-fil ett installationsprogram slutade med att distribuera. Samma motor och samma flagga driver också dynamisk XFA-rendering; bakgrunden till hur XFA-upptäckt och det automatiska V8-valet samverkar beskrivs i artikeln om att upptäcka XFA-formulär och extrahera XFA-paket med PDFium

Värdhändelser för meddelanden, utskrift, e-post och formulärinsändning

Allt synligt som ett skript gör slussas tillbaka till din kod via TPdf-händelser, och var och en av dem har ett säkert standardbeteende (no-op) när de inte tilldelats. OnJavaScriptAlert tar emot meddelande, titel, knapptyp och ikon från app.alert och låter dig returnera resultatet av den nedtryckta knappen; OnJavaScriptResponse hanterar inmatningspromptar från app.response, inklusive lösenordsflaggan; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail och OnJavaScriptSubmitForm fångar upp anrop för ljudsignal (beep), utskrift, e-post och formulärinsändning med tillhörande parametrar. En visare som inte tilldelar någon av dessa renderar och beräknar fortfarande korrekt; dokumentet kan helt enkelt inte öppna dialogrutor, skriva ut eller skicka något

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;

Denna uppdelning av standardbeteenden är viktig för hotmodellering. OnJavaScriptMail och OnJavaScriptSubmitForm fungerar som aviseringar: PDFium-komponenten utför ingen nätverks- eller MAPI-åtgärd på egen hand, så ett skadligt dokument som anropar this.submitForm or this.mailDoc mot en oförberedd värd åstadkommer absolut ingenting. Värden måste registrera en hanterare och själv implementera överföringen, vilket innebär att beslutet att flytta data från maskinen alltid ligger hos dig, i din kod, med URL och innehåll till hands

Hur förhindrar du en skadlig PDF från att öppna webbadresser?

URI-åtgärder är det enda ställe där det historiska standardbeteendet var aktivt snarare än passivt, och det är därför de fick en dedikerad spärr (veto). Före version 2.13.1 körde callback-funktionen FFI_DoURIActionWithKeyboardModifier villkorslöst vilken URI som helst som ett XFA-fält tillhandahöll via operativsystemet (shell execute), vilket gav ett skadligt dokument möjlighet att starta godtyckliga webbadresser vid klick. Händelsen OnXfaUriAction stänger detta: komponenten rådfrågar händelsen innan exekvering, och en hanterare som sätter Handled till True undertrycker standardbeteendet med ShellExecuteW helt. Att lämna händelsen otilldelad bevarar det gamla beteendet för kompatibilitet, så en säkerhetsmedveten visare bör alltid tilldela den

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;

En tillåtelselista (allow-list) för schemat är ett minimum; en mer strikt visare bekräftar med användaren eller leder allt genom en egen sandlåde-baserad webbläsarkomponent. Samma spärrfilosofi sträcker sig över händelseuppsättningen i version 2.13.1: OnXfaEmail, OnXfaHttpRequest och OnXfaOpenFile exponerar alla de textuella parametrarna för den begärda åtgärden, medan komponenten själv inte utför någon nätverks- eller filöverföring. Hur dessa skydd passar in i en bredare strategi för ej betrodda dokument, inklusive renderingsisolering, beskrivs i artikeln om att bygga en säker PDF-förhandsvisning i Delphi

Skriva till fältet i fokus med SetFocusedFormFieldText

TPdf.SetFocusedFormFieldText, som lades till i version 2.13.2, är motsvarigheten på skrivsidan till FocusedFormFieldText: den väljer det fokuserade fältets aktuella innehåll via FORM_SelectAllText och skriver över det med FORM_ReplaceSelection, exakt som om användaren hade skrivit in ersättningen. Eftersom texten matas in via den interaktiva redigeringsbanan utlöses alla knapptrycks-, formaterings- och beräkningsskript som är kopplade till fältet på samma sätt som vid manuell inmatning, vilket gör det till rätt funktion för programmatisk ifyllning i en visare som håller JavaScript aktivt. Fältnavigering och fokushantering kring detta täcks i formulärfältnavigering med 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;

Asymmetrin i hur ändringar sparas är en verklig gräns, inte en reservation för formens skull. För AcroForm-textfält och kombinationsrutor sparas det redigerade värdet i fältets /V-post vid fokusförlust och kvarstår när man sparar. För XFA-textfält landar skrivningen endast i redigeringsbufferten i minnet, eftersom PDFium inte exponerar något offentligt API för att stämma av widgetvärden tillbaka till XFA-datasetpaketet; att spara dokumentet kommer inte att överföra ändringen. Om permanent XFA-dataredigering krävs bör du redigera själva XML-datamängden snarare än widgeten

Gränser värda att känna till före leverans

Två ärliga begränsningar kvarstår. För det första är det JavaScript-API som PDFium implementerar en fungerande delmängd av Acrobats objektmodell, centrerad kring kärnan för formulärskript: fältåtkomst, beräknings- och formateringshändelser, app.alert och app.response, samt anrop för utskrift, e-post och formulärinsändning. Dokument som förlitar sig på exotic objekt som bara finns i Acrobat kommer att fungera sämre, vanligtvis tyst, så testa de faktiska formulär dina användare hanterar snarare än att anta full likvärdighet. För det andra innebär det att aktivera V8 att du måste distribuera pdfium.v8.dll, en betydligt större binärfil än standardbygget; en visare som aldrig behöver skript eller XFA kan legitimt behålla den mindre DLL-filen och lämna m_pJsPlatform otilldelad, vilket i sig är ett fullt giltigt säkerhetsbeslut

Mönstret som faller ut av allt detta är behagligt enkelt: ställ in EnableV8Engine vid uppstart, tilldela meddelande- och svarshändelserna så att dialogrutorna ser inbyggda ut, tilldela OnXfaUriAction och granskningslogga e-post- och sändningshändelserna, och lämna allt annat på sina standardinställningar tills ett krav säger annorlunda. Den fullständiga händelsereferensen och API:et för formulärfyllning finns dokumenterade på produktsidan för PDFium Component