Technisch artikel

PDFium Component Dynamic XFA: het pagina-aantal is een delta

Voegt een dynamisch XFA-formulier in een Delphi-viewer pagina's toe of weg, dan meldt PDFium Component het nieuwe totaal sinds v3.126.1 via TPdf.PageCount en TPdf.OnXfaPageCountChanged, want het native page-event draagt een toegevoegd/verwijderd-delta in plaats van een totaal. De Windows V8-bibliotheken in v3.126.1 verplaatsen invoer-hit-areas ook mee met verplaatste velden, en v3.126.2 herlaadt verouderde page-handvatten nadat de layout-callback terugkeert. De bugmelding waarmee dit begon betrof een onkostenformulier: klik twee keer op Rij toevoegen, het formulier groeit naar twee pagina's, en de pagina-indicator meldt fier 1 van 1. Typ in een veld dat naar pagina 2 is verhuisd en de toetsaanslagen landen ergens onzichtbaar. Geen ervan kwam naar voren bij de vastelengte-sampleformulieren waar iedereen het eerst mee test, en de redenen waarom zijn het weten waard als u een formulier-viewer inbedt

Wat gebeurt er wanneer een dynamisch XFA-formulier herpagineert?

Een dynamisch XFA-formulier heeft geen vaste paginalijst, dus zijn pagina-aantal is een output van layout en kan bij elke datawijziging van de gebruiker veranderen. XFA 3.3 beschrijft het formulier als een boom van subforms; een herhalend subform wordt gestuurd door een instanceManager, en een script als _Row.addInstance() kloont er één rij bij. De layoutprocessor laat de inhoud daarna opnieuw door de pagina-areas stromen, wat een pagina kan toevoegen, een pagina kan laten vallen of bestaande velden naar een andere pagina kan duwen. ISO 32000-1 §12.7.8 definieert alleen hoe de XFA-packets in de PDF meereizen; alles wat daarna gebeurt hoort bij de XFA-engine, die in PDFium Component de eigen XFA-layout van PDFium is, draaiend in het hostproces. Een Delphi-viewer heeft dus te maken met een document waarvan pagina-aantal, paginagroottes en widgetposities allemaal levende toestand zijn. Drie dingen gaan mis wanneer de host iets anders aanneemt:

  • Het pagina-aantal dat de host cachet voor navigatie, scrollbereiken en paginadraaiknoppen veroudert, of erger, wordt bijgewerkt met het verkeerde getal
  • Velden die verhuizen tonen hun rand op de nieuwe positie terwijl de editor en het muis-hit-area op de oude coördinaten blijven
  • De viewer houdt een page-handle vast die de layout heeft vervangen, dus klikken en tekeningen gaan naar een pagina die in dat formulier niet meer bestaat

Rijbewerkingen bewaren over opslaan en heropenen heen is een apart probleem met eigen regels; dit artikel blijft bij wat er tijdens runtime binnen de viewer gebeurt

Welke PDFium-runtime heeft dynamische XFA nodig?

Dynamische XFA in PDFium Component vereist de V8/XFA-build van de nativebibliotheek, gekozen via de globale variabele EnableV8Engine in de unit PDFium voordat het eerste document laadt. Het proces zet in op één DLL zodra een eerste TPdf de bibliotheek laadt, en een kale PDFium-build kan de XFA-engine helemaal niet draaien. Bij het openen van een document kijkt TPdf wel even in het bestand naar XFA-markers en schakelt automatisch naar de V8-build, maar alleen als er in dat proces nog geen kale bibliotheek is geladen. Is de inzet al de verkeerde kant op gegaan, dan vuurt TPdf.OnXfaRuntimeMissing één keer zodat de host de gebruiker om een herstart kan vragen. De vlag expliciet bij de opstart zetten haalt het giswerk weg. Ook de callbackstructuur FPDF_FORMFILLINFO die de XFA-events draagt moet bij de DLL passen; de achtergrond staat bij FPDF_FORMFILLINFO versie 2 en de XFA-callback-ABI, en XFA-formulieren detecteren en hun packets uitlezen behandelt hoe u de formuliertypes uit elkaar houdt voordat u een viewer opent

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Beslis voordat de eerste TPdf de nativebibliotheek laadt:
  // het proces kan later niet van pdfium.dll naar pdfium.v8.dll wisselen
  EnableV8Engine := True;

  FPdf := TPdf.Create(nil);
  FPdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
  FPdf.OnXfaPageCountChanged := PdfXfaPageCountChanged;
  FPdf.FileName := 'C:\Forms\expense-claim.pdf';
  FPdf.Active := True;

  PdfView1.Pdf := FPdf;
  PdfView1.OnPageChange := PdfViewPageChange;
  PdfView1.Active := True;

  UpdatePageRange(FPdf.PageCount);
end;

procedure TClaimForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  StatusBar1.SimpleText :=
    'This XFA form needs the V8 runtime; restart the application to enable it';
end;

Waarom meldde PageCount 1 voor een formulier van twee pagina's?

Vóór v3.126.1 sloeg PDFium Component het argument page_count van het native page-event op als documenttotaal, en dat argument is in werkelijkheid het absolute verschil tussen het nieuwe en het oude pagina-aantal. PDFium vuurt FFI_PageEvent af zodra een layoutpasse klaar is, met een eventtype pagina toegevoegd of pagina verwijderd; intern werkt hij eerst zijn opgeslagen pagina-aantal bij en geeft daarna abs(new - old) door. Bij de initiële layout is het oude aantal nul, dus de delta is gelijk aan het totaal, en een statische sample van drie pagina's meldt zoals verwacht drie pagina's. Dat is precies waarom vastelengte-testformulieren de bug nooit lieten zien. De eerste keer dat een dynamisch formulier groeit van één naar twee pagina's is de delta 1, en de wrapper zette zowel TPdf.PageCount als de parameter NewCount van OnXfaPageCountChanged op 1. Een rij uit een formulier van drie pagina's halen produceerde dezelfde soort onzin in de andere richting

De delta op de vorige waarde optellen is ook geen veilige reparatie. De volgorde van initialisatie- en layout-callbacks betekent dat de wrapper zijn eerdere telling niet altijd als basis kan vertrouwen, dus een lopende som kan afdrijven. Sinds v3.126.1 negeert de callback het argument als telling en roept FPDF_GetPageCount aan op het document, dat het totaal leest uit de layout die net is afgerond. Daarna wist hij de gecachte pagina-scenes, bewaart dat totaal als de XFA page-count-override achter TPdf.PageCount, en pas daarna vuurt hij OnXfaPageCountChanged af. Tegen de tijd dat uw handler draait zijn NewCount en FPdf.PageCount het eens

PDFium Component-diagram voor dynamische XFA waarin het toevoegen van een rij een formulier van één pagina naar twee herpagineert en FFI_PageEvent abs(new minus old) als delta doorgeeft, zodat de oude wrapper TPdf.PageCount 1 meldde terwijl v3.126.1 FPDF_GetPageCount leest en het juiste totaal meldt
Het native page-event meldt een toegevoegd-of-verwijderd-delta, geen totaal, dus v3.126.1 negeert het argument en leest de voltooide layout voordat hij OnXfaPageCountChanged vuurt
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount is het totaal van de voltooide layout, nooit een delta.
  // Dit draait binnen de layout-callback van PDFium: werk alleen de host-UI bij,
  // sluit het document niet en herlaad hier geen pagina's
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Vuurt na elke page-herlaad, inclusief de uitgestelde XFA-refresh
  PageSpin.Value := PdfView1.PageNumber;
end;

procedure TClaimForm.UpdatePageRange(Count: Integer);
begin
  PageSpin.MinValue := 1;
  PageSpin.MaxValue := Count;
  PageLabel.Caption := Format('of %d', [Count]);
end;

Het event vuurt alleen voor Full XFA-formulieren waarvan de layout tijdens runtime verandert. Statische XFA- en AcroForm-documenten vuuren hem nooit af, dus een viewer die beide aan kan kan dezelfde handler toegewezen laten. Hem niet toewijzen is ook veilig; de override achter TPdf.PageCount wordt sowieso toegepast, en het event bestaat zodat de host kan verversen wat hij heeft gecached

Waarom blijft het invoervak op de oude pagina wanneer een veld verhuist?

De rand verhuisde en de editor niet, omdat de native XFA-notifier een rechthoek met zichzelf vergeleek. Verandert de layout de geometrie van een al geladen widget, dan hoort PDFium de nieuwe rechthoek op te merken en PerformLayout op de widget aan te roepen, wat de teksteditor en zijn hit-area verplaatst. De controle vergeleek GetWidgetRect() met RecacheWidgetRect(). Beide functies geven een const-referentie terug naar hetzelfde lid, en de recache overschrijft dat lid op zijn plek, dus de vergelijking zag altijd twee identieke waarden en geladen widgets sloegen hun relayout over

Het symptoom kwam boven water toen een test de hoogte van een subform veranderde zodat bestaande velden naar de volgende pagina overschreden. Op beide V8-architecturen werd de veldrand op zijn nieuwe positie getekend terwijl de getypte tekst en het muis-hit-area op de vorige Y-coördinaat bleven. Een expliciete relayout fixte het niet, en de pagina herladen ook niet, want de widget bleef geloven dat zijn geometrie actueel was. De Windows V8-bibliotheken die met v3.126.1 verschijnen kopiëren de oude rechthoek by value vóór het hersen en vergelijken die kopie, dus verplaatste widgets doen hun relayout en de bewerkte waarde verschijnt exact waar de rand staat. Dit is een native fix: hij reist met de DLL's mee, dus de Pascal-units bijwerken terwijl een oudere pdfium.v8.dll blijft staan laat de verkeerd geplaatste hit-areas staan. De regressiecontrole die hem aandreef bewerkt eerst een overlevende rij naar een niet-defaultwaarde en eist die waarde daarna op de nieuwe locatie van het veld, want een rij die met defaultwaarden is herbouwd zou anders als geslaagd ogen

PDFium Component-widget-relayoutdiagram dat de oude zelfvergelijking, waarin GetWidgetRect en RecacheWidgetRect één gedeeld lid terug gaven zodat verplaatste widgets PerformLayout oversloegen, afzet tegen de v3.126.1 Windows V8-copy-by-value-controle die de editor en het muis-hit-area op de hertekende rand positioneert
Een rechthoek met zichzelf vergelijken faalt nooit, dus de rand verhuisde terwijl getypte tekst en klikken achterbleven tot de controle eerst een kopie by value bewaarde

Hoe herlaadt TPdfView pagina's zonder een handle onder PDFium weg te trekken?

Sinds v3.126.2 stelt TPdfView de page-herlaad die op een XFA-layoutwijziging volgt uit tot de native call stack is ontspannen. Het page-event vuurt meestal terwijl PDFium nog invoer verwerkt: de gebruiker klikte op een Rij-toevoegen-knop, de klik draaide een script, het script veranderde het instance-aantal, en de layout klaarde binnen diezelfde native aanroep. De page-handle op dat moment sluiten en heropenen zou een object vrijgeven dat de aanroeper nog gebruikt. Vóór v3.126.2 ongeldigde de viewer zichzelf alleen, dus de getoonde page-handle kon blijven wijzen naar pre-layout-toestand, en was de gebruiker op de laatste pagina toen die verdween, dan viel het gekozen paginanummer buiten bereik

De uitgestelde refresh werkt in een paar kleine stappen, en die verklaren het gedrag dat u vanuit de host ziet:

  1. De page-event-callback markeert de view als met een uitgestelde XFA-layoutrefresh en post een privé window-message; herhaalde events voordat de message aankomt smelten samen tot één refresh
  2. Een view die nog geen window-handle heeft houdt de pending-vlag vast en post de message vanuit CreateWnd, terwijl het wisselen van documenten, deactiveren van de view of vernietigen ervan de vlag wist
  3. Als de message aankomt wist de view de tekstselectie, de zoek-highlight en de index van het gefocuste veld, want alle drie verwezen ze naar de oude layout
  4. De gekozen pagina wordt bijgeklempt op het nieuwe PageCount; een veranderd paginanummer gaat via de normale paginawissel, anders wordt de huidige pagina herladen, en de fit-modus wordt opnieuw toegepast
  5. Laat de layout helemaal geen pagina's na, dan ontlaadt de view zijn oude page-handle in plaats van een pagina te tekenen die niet meer bestaat
PDFium Component TPdfView-diagram van de uitgestelde XFA-refresh waarin een page-event binnen de native layout call stack alleen een pending-refresh markeert en een window-message post, die later verouderde selectietoestand wist, de pagina op het nieuwe PageCount bijklempt en de page-handle herlaadt of ontlaadt
De herlaad wacht tot de native call stack is ontspannen: een geposte message voegt herhaalde events samen, daarna klempt de view de pagina bij, herlaadt haar en vuurt OnPageChange af

Dezelfde beperking geldt voor uw eigen code. OnXfaPageCountChanged draait binnen die native layout-callback, dus behandel hem als een notificatie: werk daar labels, spinnerbereiken en toolbarstatus bij, en zet alles wat zwaarder is, zoals het document sluiten of een ander openen, in de wachtrij via een geposte message zodat het draait nadat de callback terugkeert. TPdfView.OnPageChange vertelt u daarna wanneer de view de pagina werkelijk heeft herladen, en op dat moment PdfView1.PageNumber lezen geeft de bijgeklempte waarde. Tab-toets-navigatie en de FormType-controles die een formulier-viewer bij openen draait staan bij PDF-formulierveldnavigatie met PDFium Component

Waarom geeft een klik op een Full XFA-veld Cannot open text page?

Full XFA-pagina's hebben geen PDF-tekstpagina, en vóór v3.126.2 probeerden de standaard tekstselectie en linkdetectie van de viewer er toch een te laden. Met TPdfView.AllowUserTextSelection op zijn default True vroeg hoveren de tekstlaag om een teken onder de muis, en een klik bij het loslaten van de muis draaide een automatische URL-probe over de paginatext. Op een Full XFA-pagina kan de tekstpagina niet worden geopend, dus kon een gewone klik in een veld eindigen in een exceptie Cannot open text page. Sinds v3.126.2 geven beide interne paden geen resultaat terug wanneer TPdf.FormType ftXfaFull is en de XFA-runtime beschikbaar is, dus de standaardinstellingen werken en veldinvoer blijft beschikbaar

AllowUserTextSelection uitzetten voor Full XFA-documenten blijft een redelijke UI-keuze, want er is geen paginatext te selecteren en sleepbewegingen mogen geen selectiemodus starten. Het is geen vervanging van upgraden: op eerdere versies hing de URL-probe bij een klik niet van die property af, dus kon een viewer dezelfde exceptie treffen met selectie uitgeschakeld

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType leest het open document, dus roep dit aan na FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // Op Full XFA-pagina's bestaat geen PDF-tekstlaag; velden blijven bewerkbaar
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Typen had in v3.126.2 zijn eigen reparatie nodig. De native XFA-teksteditor vervangt een selectie niet wanneer hij een teken ontvangt: FORM_OnChar voegt in bij de caret, en Backspace wist één teken, dus een waarde selecteren en er overheen typen leverde oude en nieuwe tekst naast elkaar op. PDFium Component onthoudt nu dat de klik op een XFA-tekstveld landde en routeert getypte tekens, Backspace en Delete via FORM_ReplaceSelection zodra er een selectie bestaat en het document fill-forms- of modify-rechten geeft. Of een read-only XFA-veld mag veranderen blijft beslist door de native editor, dus een veld dat read-only is gemarkeerd in het formulier houdt zijn waarde, zelfs in een document dat verder invullen toestaat. Ook TPdfView.AllowFormEvents op False zetten stopt deze toetsenbordrouting, wat een read-only viewer read-only houdt

Snelnaslag: dynamische XFA in een Delphi-viewer

SymptoomOorzaakGefixt in
Pagina-aantal toont 1 nadat het formulier naar twee pagina's is gegroeidHet native page-event geeft een toegevoegd/verwijderd-delta door, geen totaalv3.126.1 (wrapper)
Veldrand verhuist, getypte tekst en hit-area blijven achterGeladen widget sloeg relayout over na een zelfvergelijkingv3.126.1 (Windows V8-bibliotheken)
Viewer tekent of routeert invoer naar pre-layout-paginatoestandPage-handle niet herladen na herpagineringv3.126.2 (uitgestelde refresh)
Klik in een veld geeft Cannot open text pageTekstselectie en URL-probe op pagina's zonder tekstlaagv3.126.2
Over een geselecteerde waarde typen voegt toe in plaats van te vervangenDe native XFA-editor voegt in bij de caretv3.126.2
  • Zet EnableV8Engine op True voordat een document laadt, en behandel OnXfaRuntimeMissing voor het geval de kale bibliotheek eerst was geladen
  • Lees het totaal uit TPdf.PageCount of de parameter NewCount van OnXfaPageCountChanged; tel nooit zelf pagina-aantallen op of af
  • Houd de handler OnXfaPageCountChanged licht, want hij draait binnen de native layout-callback
  • Synchroniseer de pagina-indicator in TPdfView.OnPageChange, die vuurt nadat de uitgestelde herlaad het paginanummer heeft bijgeklempt
  • Rol de Windows V8-DLL's van v3.126.1 of later samen met de units uit; de widget-relayout-fix zit in native code
  • Test met een formulier dat zijn pagina-aantal werkelijk verandert en een bewerkt veld over een paginagrens verplaatst, want vastelengte-samples verbergen elke bug op deze lijst

Dynamische XFA maakt van pagina-aantal en veldgeometrie levende waarden, en een viewer blijft alleen kloppen als hij ze uit de voltooide layout haalt en pagina's herlaadt op een veilig moment. PDFium Component regelt beide binnen TPdf en TPdfView, dus de host hoeft alleen te luisteren. Details en downloads staan op de productpagina PDFium Component for Delphi