Technisch artikel

AcroForm JavaScript uitvoeren in Delphi met PDFium Component

Een formulier waarvan de totalen in Acrobat wel opnieuw worden berekend maar in uw eigen Delphi-viewer bevroren blijven, is vrijwel nooit een renderingsfout. PDFium schakelt alle document-JavaScript uit tenzij de host een JS-platform koppelt, en PDFium Component koppelt dat platform automatisch vanaf versie 2.13.2, zodat AcroForm JavaScript, scripts op documentniveau en veldberekeningen worden uitgevoerd zodra de voor V8 geschikte DLL is geladen. De host behoudt de controle via een set gebeurtenissen (events) die alles wat het script probeert to doen kunnen weergeven, omleiden of verwerpen

Die ene zin lost een ondersteuningsvraag op die we in verschillende vormen hebben gezien: "mijn bestelformulier berekent de regeltotalen in Acrobat wel, maar niet in mijn app", "app.alert wordt nooit geactiveerd", "het datumveld formatteert zichzelf niet". Ze zijn allemaal terug te voeren op hetzelfde contract, en het is de moeite waard om daar tien minuten bij stil te staan, omdat ditzelfde contract ook uw beveiligingsgrens is tegen kwaadaardige documenten

Waarom werken berekeningen in PDF-formulieren niet in PDFium?

PDFium behandelt document-JavaScript als strikt opt-in: als het lid m_pJsPlatform van de formulierinvulomgeving NULL is, wordt elk script in het document geruisloos overgeslagen. Geen foutmelding, geen logboekregel, geen gedeeltelijke uitvoering. Berekende velden behouden hun laatst opgeslagen waarde, app.alert verdwijnt en opmaakscripts worden nooit uitgevoerd. Acrobat, dat altijd met een eigen JavaScript-engine wordt geleverd, voert hetzelfde bestand probleemloos uit. Dit is de reden waarom het verschil zo erg lijkt op een bug in uw code, terwijl het in feite een bewuste standaardinstelling in de onderliggende bibliotheek is

PDFium Component had hiervoor een tweede, historische laag. Tot en met versie 2.13.1 koppelde de binding m_pJsPlatform alleen binnen de initialisatietak van XFA, waardoor gewone AcroForm-documenten — die de overgrote meerderheid van de scripted PDF's vormen — helemaal geen JavaScript-platform kregen, zelfs niet wanneer de V8-compatibele DLL aanwezig was. Versie 2.13.2 haalde de koppeling uit de XFA-beslissing: InitializeFormFill bouwt nu de tabel IPDF_JsPlatform op telkens wanneer V8FeaturesAvailable True retourneert, onafhankelijk van de vraag of het document XFA is. Het praktische resultaat is dat scripts op documentniveau, veldberekeningsketens en app.alert nu ook in gewone AcroForm-documenten worden uitgevoerd

Welke V8-DLL heeft AcroForm JavaScript nodig in Delphi?

AcroForm JavaScript heeft een PDFium-binary nodig die is gecompileerd met de V8-engine, die PDFium Component laadt als pdfium.v8.dll wanneer de globale vlag EnableV8Engine True is voordat de bibliotheek voor het eerst wordt geladen. Er is hier één valkuil die we duidelijk moeten vermelden: de component detecteert XFA-documenten automatisch door vooraf te scannen op /XFA-markeringen en schakelt EnableV8Engine voor u in, maar een gewoon AcroForm-document met JavaScript bevat geen XFA-markering, dus er vindt geen automatische detectie plaats. Als uw viewer formulierscripts moet uitvoeren, stelt u de vlag zelf eenmalig in voordat het eerste document wordt geladen; nadat een gewone pdfium.dll is geladen, kan het proces niet meer van engine wisselen

uses PDFium;

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  // Moet worden uitgevoerd voordat de singleton PDFium-bibliotheek voor het eerst wordt geladen;
  // zodra de gewone pdfium.dll in het proces zit, kan deze niet meer worden verwisseld
  EnableV8Engine := True;
end;

procedure TViewerForm.OpenDocument(const FileName: string);
begin
  FPdf.LoadDocument(FileName);
  if not V8FeaturesAvailable then
    StatusBar.SimpleText :=
      'JavaScript uitgeschakeld: pdfium.v8.dll niet geïmplementeerd';
end;

V8FeaturesAvailable is de betrouwbare runtime-controle: het rapporteert of de geladen binary daadwerkelijk de V8-afhankelijke exports oplost, zodat de controle werkt ongeacht welke DLL een installatieprogramma uiteindelijk heeft geïmplementeerd. Dezelfde engine en dezelfde vlag drijven ook de dynamische XFA-rendering aan; de achtergrond over hoe XFA-detectie en de automatische V8-selectie op elkaar inwerken, wordt behandeld in het detecteren van XFA-formulieren en het extraheren van XFA-pakketten met PDFium

Hostgebeurtenissen voor waarschuwingen, afdrukken, e-mail en formulierverzending

Elk zichtbaar ding dat een script doet, wordt via TPdf-gebeurtenissen (events) teruggeleid naar uw code, en elk van deze gebeurtenissen valt standaard terug op een veilige no-op wanneer ze niet zijn toegewezen. OnJavaScriptAlert ontvangt het bericht, de titel, het knoptype en het pictogram van app.alert en stelt u in staat het resultaat van de ingedrukte knop te retourneren; OnJavaScriptResponse bedient invoerprompts van app.response, inclusief de wachtwoordvlag; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail en OnJavaScriptSubmitForm brengen de beep-, print-, mail- en indieningsverzoeken naar de oppervlakte met hun volledige parametersets. Een viewer die geen van deze toewijst, rendert en berekent nog steeds correct; het document kan simpelweg geen dialoogvensters openen, afdrukken of iets verzenden

procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
  const Msg, Title: WString; ButtonType, Icon: Integer;
  var Result_: Integer; var Handled: Boolean);
begin
  // Leid app.alert via de VCL zodat deze eigendom is van ons venster
  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
  // Er wordt niets verzonden tenzij deze handler ervoor kiest om het te verzenden
  LogAudit(Format('Document heeft formulierverzending aangevraagd naar %s (%d bytes)',
    [Url, Length(Data)]));
end;

De splitsing in het standaardgedrag is belangrijk voor het opstellen van bedreigingsmodellen. OnJavaScriptMail en OnJavaScriptSubmitForm zijn van het type notificatie: PDFium Component voert uit zichzelf geen netwerk- of MAPI-acties uit, dus een kwaadaardig document dat this.submitForm of this.mailDoc aanroept tegen een onvoorbereide host bereikt helemaal niets. De host moet zich aanmelden door een handler toe te wijzen en het transport zelf te implementeren, wat betekent dat de beslissing om gegevens van de machine te verplaatsen altijd aan u is, genomen in uw code, met de URL en de payload bij de hand

Hoe voorkomt u dat een kwaadaardige PDF URL's start?

URI-acties zijn de enige plaats waar de historische standaard actief in plaats van passief was, en dat is waarom ze een specifiek veto hebben gekregen. Vóór versie 2.13.1 voerde de callback FFI_DoURIActionWithKeyboardModifier onvoorwaardelijk elke URI uit die een XFA-veld leverde, wat een kwaadaardig document de mogelijkheid gaf om bij een klik willekeurige URL's te starten. OnXfaUriAction sluit dat lek: de component raadpleegt de gebeurtenis voordat deze wordt uitgevoerd, en een handler die Handled op True stelt, onderdrukt de standaard ShellExecuteW-actie volledig. Als de gebeurtenis niet is toegewezen, blijft het oude gedrag behouden vanwege de compatibiliteit, dus een veiligheidsbewuste viewer moet deze altijd toewijzen

procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
  const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
  // Verwerp alles wat niet gewoon https is; de standaardinstelling zou anders de URI as-is ShellExecuteW uitvoeren
  if not SameText(Copy(Uri, 1, 8), 'https://') then
  begin
    LogAudit('Geblokkeerde URI-actie: ' + Uri);
    Handled := True;
  end;
end;

Een allow-list op het schema is het minimum; een strengere viewer vraagt om bevestiging van de gebruiker of leidt alles via een eigen gesandeerde browsercomponent. Dezelfde veto-filosofie strekt zich uit over de gebeurtenissenset van v2.13.1: OnXfaEmail, OnXfaHttpRequest en OnXfaOpenFile tonen elk de tekstuele parameters van de aangevraagde actie, terwijl de component zelf geen netwerk- of bestandstransport uitvoert. Hoe deze beveiligingen passen in een bredere strategie voor niet-vertrouwde documenten, inclusief rendering-isolatie, is het onderwerp van het bouwen van een veilige PDF-voorbeeldweergave in Delphi

Schrijven naar het gefocuste veld met SetFocusedFormFieldText

TPdf.SetFocusedFormFieldText, toegevoegd in versie 2.13.2, is de schrijfkant-tegenhanger van FocusedFormFieldText: het selecteert de huidige inhoud van het gefocuste veld via FORM_SelectAllText en overschrijft deze via FORM_ReplaceSelection, precies alsof de gebruiker de vervanging had getypt. Omdat de tekst via het interactieve bewerkingspad binnenkomt, worden eventuele toetsaanslag-, opmaak- en berekeningsscripts die aan het veld zijn gekoppeld op dezelfde manier geactiveerd als bij handmatige invoer. Dit maakt het de juiste primitief voor programmatisch invullen in een viewer die JavaScript live houdt. Velddoorloop en focusbeheer eromheen worden behandeld in formulierveldnavigatie met PDFium Component

if FPdf.FocusedFormFieldIndex >= 0 then
begin
  if not FPdf.SetFocusedFormFieldText('42.50') then
    ShowMessage('Geen enkel gefocust veld accepteert tekst op deze pagina');
  // AcroForm: waarde wordt vastgelegd in /V wanneer de focus het veld verlaat.
  // XFA: de invoer blijft alleen in de bewerkingsbuffer in het geheugen en overleeft een opslagactie niet
end;

De asymmetrie in de persistentie in die opmerking is een echte grens, geen waarschuwing omwille van de vorm. Voor AcroForm-tekst- en combovelden wordt de bewerkte waarde bij focusverlies vastgelegd in de vermelding /V van het veld en blijft deze behouden na het opslaan. Voor XFA-tekstvelden belandt de invoer alleen in de bewerkingsbuffer in het geheugen, omdat PDFium geen openbare API biedt om widgetwaarden terug te synchroniseren naar het XFA-gegevenspakket; het opslaan van het document zal de wijziging niet behouden. Als duurzame bewerking van XFA-gegevens vereist is, bewerk dan het datasets XML-pakket zelf in plaats van de widget

Limieten die u moet kennen voordat u distribueert

Er blijven twee eerlijke grenzen over. Ten eerste is de JavaScript-API die PDFium implementeert een werkende subset van het Acrobat-objectmodel, gecentreerd rond de kern van de formulierscripting: veldtoegang, berekenings- en opmaakgebeurtenissen, app.alert en app.response, print-, mail- en indieningsverzoeken. Documenten die leunen op exotische, alleen voor Acrobat bestemde objecten zullen degraderen, meestal geruisloos, dus test de daadwerkelijke formulieren die uw gebruikers hanteren in plaats van uit te gaan van gelijkwaardigheid. Ten tweede betekent het inschakelen van V8 dat u pdfium.v8.dll moet meeleveren, een aanzienlijk grotere binary dan de gewone build; een viewer die nooit scripting of XFA nodig heeft, kan legitiem de kleinere DLL behouden en m_pJsPlatform niet koppelen, wat op zich al een geldige beveiligingshouding is

Het patroon dat hieruit rolt is prettig saai: stel EnableV8Engine in bij het opstarten, wijs de alert- en response-gebeurtenissen toe zodat dialoogvensters er systeemeigen uitzien, wijs OnXfaUriAction toe en log de mail- en submit-gebeurtenissen, en laat al het andere op de standaard no-op-instelling staan totdat een vereiste anders voorschrijft. De volledige gebeurtenisreferentie en de API voor formulierinvulling zijn gedocumenteerd op de productpagina van het PDFium Component