Teknisk artikel

PDFium Component Dynamic XFA: Sideantal er en delta

Når en dynamisk XFA-form i en Delphi-viewer tilføjer eller fjerner sider, rapporterer PDFium Component det nye total gennem TPdf.PageCount og TPdf.OnXfaPageCountChanged siden v3.126.1, fordi det native side-event bærer en tilføjet/fjernet-delta frem for et total. Windows V8-bibliotekerne i v3.126.1 flytter også input-hit-områder med flyttede felter, og v3.126.2 genindlæser forældede sidehandles, efter layout-callback'en returnerer. Bug-rapporten, der startede det, var en udgiftsgodtgørelsesform: klik Add Row to gange, formen vokser til to sider, og sideindikatoren læser stolt 1 af 1. Tast i et felt, der er flyttet til side 2, og tastetrykkene lander et usynligt sted. Intet af det viste sig med de faste længde-eksempelformer, alle tester med først, og grundene er værd at kende, hvis du indlejrer en form-viewer

Hvad sker der, når en dynamisk XFA-form repaginerer?

En dynamisk XFA-form har ingen fast sideliste, så dens sideantal er et output af layout og kan ændre sig, hver gang brugeren redigerer data. XFA 3.3 beskriver formen som et træ af subformer; en gentagende subform styres af en instanceManager, og et script som _Row.addInstance() kloner endnu en række. Layout-processoren flyder derefter indholdet ud i sideområderne igen, hvilket kan tilføje en side, droppe en side eller skubbe eksisterende felter over på en anden side. ISO 32000-1 §12.7.8 definerer kun, hvordan XFA-pakkerne rider inde i PDF'en; alt, hvad der sker bagefter, tilhører XFA-motoren, som i PDFium Component er PDFiums egen XFA-layout, kørende i værtsprocessen. En Delphi-viewer har derfor med et dokument at gøre, hvis sideantal, sidestørrelser og widgetpositioner alle er levende tilstand. Tre ting går galt, når værten antager andet:

  • Det sideantal, værten cacher til navigation, scroll-områder og side-spinners bliver forældet, eller værre, opdateres med det forkerte tal
  • Felter, der flytter sig, viser deres kant i den nye position, mens editoren og muse-hit-området bliver ved de gamle koordinater
  • Vieweren holder fast i en sidehandle, som layoutet har erstattet, så klik og malinger går til en side, der ikke længere findes i den form

At fastholde rækkeredigeringer på tværs af gem og genåbning er et separat problem med sine egne regler; denne artikel bliver ved det, der sker ved runtime inde i vieweren

Hvilken PDFium-runtime behøver dynamic XFA?

Dynamic XFA i PDFium Component kræver V8/XFA-buildet af det native bibliotek, valgt af den globale EnableV8Engine-variabel i PDFium-unitet, før det første dokument indlæses. Processen forpligter sig til én DLL, første gang enhver TPdf indlæser biblioteket, og et almindeligt PDFium-build kan slet ikke køre XFA-motoren. Når et dokument åbnes, kigger TPdf i filen efter XFA-markører og skifter automatisk til V8-buildet, men kun hvis intet almindeligt bibliotek endnu er indlæst i processen. Er forpligtelsen allerede gået den forkerte vej, udløses TPdf.OnXfaRuntimeMissing én gang, så værten kan bede brugeren om at genstarte. At sætte flaget eksplicit ved startup fjerner gætværket. FPDF_FORMFILLINFO-callback-strukturen, der bærer XFA-eventene, skal også matche DLL'en; baggrunden står i FPDF_FORMFILLINFO version 2 og XFA-callback-ABI'en, og detektering af XFA-former og udtrækning af deres pakker dækker at skelne formtyperne, før du åbner en viewer

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Beslut, før den første TPdf indlæser det native bibliotek:
  // processen kan ikke skifte fra pdfium.dll til pdfium.v8.dll senere
  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;

Hvorfor rapporterede PageCount 1 for en topages form?

Før v3.126.1 gemte PDFium Component page_count-argumentet fra det native side-event som dokumenttotal, og det argument er faktisk den absolutte forskel mellem det nye og det gamle sideantal. PDFium udløser FFI_PageEvent, efter et layout-gennemløb afsluttes med en event-type af page added eller page removed; internt opdaterer den sit gemte sideantal først og sender derefter abs(new - old). Ved det første layout er det gamle antal nul, så deltaen er lig totalen, og en tresidet statisk prøve rapporterer tre sider som forventet. Det er præcis derfor, fastlængede testformer aldrig afslørede buggen. Første gang en dynamisk form vokser fra én side til to, er deltaen 1, og wrapperen satte både TPdf.PageCount og NewCount-parameteren af OnXfaPageCountChanged til 1. At fjerne en række fra en tresidet form producerede den samme slags nonsens i den anden retning

At akkumulere deltaen oven på den tidligere værdi er heller ikke en sikker reparation. Rækkefølgen af initialiserings- og layout-callbacks betyder, at wrapperen ikke altid kan stole på sit tidligere antal som baseline, så en løbende sum kan drive. Siden v3.126.1 ignorerer callback'en argumentet som antal og kalder FPDF_GetPageCount på dokumentet, som læser totalen fra det layout, der netop er fuldført. Den rydder derefter de cachede sidescener, gemmer det total som XFA-sideantal-override bag TPdf.PageCount og udløser først derefter OnXfaPageCountChanged. Når din handler kører, er NewCount og FPdf.PageCount enige

PDFium Component dynamic XFA-diagram, hvor tilføjelse af en række repaginerer en topsidet form til to sider, og FFI_PageEvent sender abs(new minus old) som en delta, så den gamle wrapper rapporterede TPdf.PageCount 1, mens v3.126.1 læser FPDF_GetPageCount og rapporterer det korrekte total
Det native side-event rapporterer en tilføjet-eller-fjernet-delta, ikke et total, så v3.126.1 ignorerer argumentet og læser det fuldførte layout, før OnXfaPageCountChanged udløses
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount er det fuldførte layouts total, aldrig en delta.
  // Dette kører inde i PDFiums layout-callback: opdatér kun værts-UI-tilstand,
  // luk ikke dokumentet eller genindlæs sider herfra
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Udløses efter hver side-genindlæsning, inklusive den udskudte XFA-opfriskning
  PageSpin.Value := PdfView1.PageNumber;
end;

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

Eventet udløses kun for Full XFA-former, hvis layout ændrer sig ved runtime. Statisk XFA- og AcroForm-dokumenter udløser det aldrig, så en viewer, der håndterer begge, kan lade samme handler være tildelt. At lade det stå utildelt er også sikkert; overriden bag TPdf.PageCount anvendes alligevel, og eventet findes, så værten kan friske hvad end, den har cachet, op

Hvorfor bliver inputboksen på den gamle side, når et felt flytter sig?

Kanten flyttede sig, og editoren gjorde ikke, fordi det native XFA-notifikator sammenlignede et rektangel med sig selv. Når layout ændrer geometrien af en widget, der allerede er indlæst, burde PDFium bemærke det nye rektangel og kalde PerformLayout på widgetten, som omplacerer teksteditoren og dens hit-område. Tjekket sammenlignede GetWidgetRect() med RecacheWidgetRect(). Begge funktioner returnerer en const-reference til det samme medlem, og recachen overskriver det medlem på stedet, så sammenligningen altid så to identiske værdier, og indlæste widgets sprang deres relayout over

Symptomet kom frem, da en test ændrede en subform-højde, så eksisterende felder krydsede over på næste side. På begge V8-arkitekturer blev feltkanten tegnet i sin nye position, mens den indtastede tekst og muse-hit-området blev ved den tidligere Y-koordinat. En eksplicit relayout rettede det ikke, og heller ikke genindlæsning af siden, fordi widgetten stadig troede, dens geometri var aktuel. Windows V8-bibliotekerne, der skibede med v3.126.1, kopierer det gamle rektangel by value, før der recaches, og sammenligner den kopi, så flyttede widgets relayout'er, og den redigerede værdi optræder præcis dér, hvor kanten er. Dette er et nativt fix: det rejser med DLL'erne, så opdatering af Pascal-unitene, mens en ældre pdfium.v8.dll beholdes, efterlader de forkert placerede hit-områder. Regressionstjekket, der drev det, redigerer først en overlevende række til en ikke-standard-værdi og kræver derefter den værdi på feltets nye placering, fordi en række genopbygget med standardværdier ellers ville se ud som et bestået

PDFium Component widget-relayout-diagram, der kontrasterer den gamle selv-sammenligning, hvor GetWidgetRect og RecacheWidgetRect returnerede ét delt medlem, så flyttede widgets sprang PerformLayout over, med v3.126.1 Windows V8 copy-by-value-tjekket, der omplacerer editoren og muse-hit-området på den tegnede kant
At sammenligne et rektangel med sig selv fejler aldrig, så kanten flyttede sig, mens indtastet tekst og klik blev efterladt, indtil tjekket gemte en kopi by value først

Hvordan genindlæser TPdfView sider uden at trække en handle væk under PDFium?

Siden v3.126.2 udskyder TPdfView den side-genindlæsning, der følger efter en XFA-layout-ændring, til det native kald-stak er foldet ud. Side-eventet udløses som regel, mens PDFium stadig behandler input: brugeren klikkede en Add Row-knap, klikket kørte et script, scriptet ændrede instance-antallet, og layoutet fuldførte inde i det samme native kald. At lukke og genåbne sidehandle'n i det øjeblik ville frigøre et objekt, kalderen stadig bruger. Før v3.126.2 invaliderede vieweren kun sig selv, så den viste sidehandle kunne blive ved at pege på pre-layout-tilstand, og var brugeren kommet til den sidste side, da den forsvandt, var det valgte sidenummer uden for intervallet

Den udskudte opfriskning arbejder i et par små trin, og de forklarer den opførsel, du ser fra værten:

  1. Side-event-callback'en markerer viewet som havende en afventende XFA-layout-opfriskning og poster en privat window-besked; gentagne events, før beskeden ankommer, flettes til én opfriskning
  2. Et view uden window-handle endnu beholder det afventende flag og poster beskeden fra CreateWnd, mens dokumentskift, deaktivering af viewet eller dets destruktion rydder flaget
  3. Når beskeden ankommer, rydder viewet tekstmarkeringen, søgefremhævningen og fokuseret-felt-indekset, for alle tre refererede til det gamle layout
  4. Den valgte side clamps til det nye PageCount; et ændret sidenummer går gennem det normale sideskift, ellers genindlæses den aktuelle side, og fit-tilstanden anvendes igen
  5. Efterlader layoutet slet ingen sider, aflæsser viewet sin gamle sidehandle i stedet for at male en side, der ikke længere findes
PDFium Component TPdfView udskudt XFA-opfriskning-diagram, hvor et side-event inde i det native layout-kaldstak kun markerer en afventende opfriskning og poster en window-besked, som senere rydder forældet markerings-tilstand, clamper siden til det nye PageCount og genindlæser eller aflæsser sidehandle'n
Genindlæsningen venter, til det native kaldstak foldes ud: en postet besked fletter gentagne events, derefter clamper viewet siden, genindlæser den og udløser OnPageChange

Samme begrænsning gælder din egen kode. OnXfaPageCountChanged kører inde i det native layout-callback, så behandl det som en notifikation: opdatér labels, spinner-intervaller og toolbar-tilstand dér, og sæt alt tungere i kø, som at lukke dokumentet eller åbne et andet, med en postet besked, så det kører efter callback'en returnerer. TPdfView.OnPageChange fortæller dig derefter, hvornår viewet faktisk har genindlæst siden, og at læse PdfView1.PageNumber på det punkt giver dig den clampede værdi. Tab-tast-navigation og de FormType-tjek, en form-viewer kører ved åbning, er dækket i PDF form field-navigation med PDFium Component

Hvorfor udløser klik på et Full XFA-felt "Cannot open text page"?

Full XFA-sider har ingen PDF-tekstside, og før v3.126.2 forsøgte viewerens default tekstmarkering og linkdetektion alligevel at indlæse en. Med TPdfView.AllowUserTextSelection på sin default True spurgte hover tekstlaget efter et tegn under musen, og et mouse-up-klik kørte en automatisk URL-probe over sideteksten. På en Full XFA-side kan tekst-siden ikke åbnes, så et almindeligt klik ind i et felt kunne ende i en Cannot open text page-exception. Siden v3.126.2 returnerer begge interne veje intet resultat, når TPdf.FormType er ftXfaFull og XFA-runtime er tilgængelig, så default-indstillingerne virker, og feltinput forbliver tilgængeligt

At slå AllowUserTextSelection fra for Full XFA-dokumenter er stadig et fornuftigt UI-valg, for der er ingen sidetekst at markere, og trække-gestusser bør ikke starte en markeringstilstand. Det er dog ikke en erstatning for at opgradere: på tidligere versioner afhang URL-proben ved klik ikke af den property, så en viewer kunne ramme samme exception med markering slået fra

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType læser det åbne dokument, så kald dette efter FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // Intet PDF-tekstlag findes på Full XFA-sider; felter forbliver redigerbare
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Skrivning behøvede sin egen reparation i v3.126.2. Den native XFA-teksteditor erstatter ikke en markering, når den modtager et tegn: FORM_OnChar indsætter ved caret, og Backspace sletter et enkelt tegn, så at markere en værdi og taste over den producerede gammel og ny tekst side om side. PDFium Component husker nu, at klikket landede på et XFA-tekstfelt og ruter indtastede tegn, Backspace og Delete gennem FORM_ReplaceSelection, når en markering findes, og dokumentet tillader fill-forms- eller modify-tilladelse. Om et read-only XFA-felt må ændre besluttes stadig af den native editor, så et felt markeret read-only i formen beholder sin værdi selv i et dokument, der ellers tillader udfyldning. At sætte TPdfView.AllowFormEvents til False stopper også denne tastatur-routing, hvilket holder en read-only-viewer read-only

Hurtig reference: dynamic XFA i en Delphi-viewer

SymptomÅrsagFixet i
Sideantallet viser 1, efter formen er vokset til to siderDet native side-event sender en tilføjet/fjernet-delta, ikke et totalv3.126.1 (wrapper)
Feltkanten flytter sig, indtastet tekst og hit-område bliver efterIndlæst widget sprang relayout over efter en selv-sammenligningv3.126.1 (Windows V8-biblioteker)
Vieweren maler eller ruter input til pre-layout-sidetilstandSidehandle ikke genindlæst efter repagineringv3.126.2 (udskudt opfriskning)
Klik i et felt udløser Cannot open text pageTekstmarkering og URL-probe på sider uden tekstlagv3.126.2
At taste over en markeret værdi tilføjer i stedet for at erstatteDen native XFA-editor indsætter ved caretv3.126.2
  • Sæt EnableV8Engine til True, før noget dokument indlæses, og håndtér OnXfaRuntimeMissing for det tilfælde, hvor det almindelige bibliotek blev indlæst først
  • Læs totalen fra TPdf.PageCount eller NewCount-parameteren af OnXfaPageCountChanged; læg aldrig selv sideantal sammen eller træk dem fra
  • Hold OnXfaPageCountChanged-handleren let, for den kører inde i det native layout-callback
  • Synkronisér den aktuelle sideindikator i TPdfView.OnPageChange, som udløses, efter den udskudte genindlæsning har clampet sidenummeret
  • Deployér v3.126.1 eller senere Windows V8-DLL'er sammen med unitene; widget-relayout-fixet bor i nativ kode
  • Test med en form, der faktisk ændrer sit sideantal og flytter et redigeret felt over et sideskift, for fastlængede eksempler skjuler hver bug på denne liste

Dynamic XFA gør sideantal og feltgeometri til levende værdier, og en viewer forbliver korrekt kun, når den tager dem fra det fuldførte layout og genindlæser sider på et sikkert tidspunkt. PDFium Component håndterer begge inde i TPdf og TPdfView, så værten behøver kun at lytte. Detaljer og downloads står på produktsiden for PDFium Component for Delphi