Teknisk artikel

PDFium Component Dynamic XFA: sidantalet är en delta

När ett dynamiskt XFA-formulär i en Delphi-visare lägger till eller tar bort sidor rapporterar PDFium Component den nya summan via TPdf.PageCount och TPdf.OnXfaPageCountChanged sedan v3.126.1, för det nativa sidoevenemanget bär en tillagd/borttagen delta i stället för en summa. Windows V8-biblioteken i v3.126.1 flyttar också input-träffytor med omlokaliserade fält, och v3.126.2 laddar om inaktuella sidohandtag efter att layoutåteranropet returnerat. Buggrapporten som startade detta var ett utläggsformulär: klicka Lägg till rad två gånger, formuläret växer till två sidor, och sidindikatorn stoltserar med 1 av 1. Skriver du i ett fält som flyttat till sida 2 landar tangenttryckningarna någonstans osynligt. Inget av det visade sig i formulärexemplen med fast längd som alla testar med först, och skälen är värda att känna till om du bäddar in en formulärvisare

Vad händer när ett dynamiskt XFA-formulär gör om sidläggningen?

Ett dynamiskt XFA-formulär har ingen fast sidolista, så dess sidantal är en utdata från layouten och kan ändras varje gång användaren redigerar data. XFA 3.3 beskriver formuläret som ett träd av subformulär; ett upprepande subformulär styrs av en instanceManager, och ett skript som _Row.addInstance() klonar ytterligare en rad. Layoutprocessorn flyter sedan innehållet i sidytorna igen, vilket kan lägga till en sida, ta bort en sida eller skjuta befintliga fält till en annan sida. ISO 32000-1 §12.7.8 definierar bara hur XFA-paketen rider inuti PDF:en; allt som händer därefter tillhör XFA-motorn, som i PDFium Component är PDFiums egen XFA-layout som kör i värdprocessen. En Delphi-visare har alltså att göra med ett dokument vars sidantal, sidstorlekar och widgetpositioner alla är levande tillstånd. Tre saker går fel när värden antar något annat:

  • Sidantalet som värden cachar för navigering, rullningsintervall och sidospinnare blir inaktuellt, eller värre, uppdateras med fel nummer
  • Fält som omlokaliserar sig visar sin ram i den nya positionen medan redigeraren och musens träffyta stannar på de gamla koordinaterna
  • Visaren behåller ett sidohandtag som layouten har ersatt, så klick och ritningar går till en sida som inte längre finns i det formuläret

Att bevara radredigeringar över sparande och återöppning är ett separat problem med egna regler; den här artikeln stannar vid vad som händer vid körning inuti visaren

Vilken PDFium-körning behöver dynamisk XFA?

Dynamisk XFA i PDFium Component kräver V8/XFA-bygget av det nativa biblioteket, valt av den globala variabeln EnableV8Engine i PDFium-enheten innan det första dokumentet läses in. Processen binder sig till en DLL första gången någon TPdf laddar biblioteket, och ett vanligt PDFium-bygge kan inte köra XFA-motorn alls. När ett dokument öppnas tittar TPdf visserligen i filen efter XFA-markörer och växlar automatiskt till V8-bygget, men bara om inget vanligt bibliotek har laddats ännu i processen. Har bindningen redan gått åt fel håll utlöses TPdf.OnXfaRuntimeMissing en gång så att värden kan be användaren starta om. Att sätta flaggan explicit vid uppstart tar bort gissningsleken. Återanropsstrukturen FPDF_FORMFILLINFO som bär XFA-evenemangen måste också matcha DLL:en; bakgrunden finns i FPDF_FORMFILLINFO version 2 och XFA-återanropets ABI, och att upptäcka XFA-formulär och läsa deras paket täcker hur man skiljer formulärtyperna åt innan man öppnar en visare

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Bestäm före den första TPdf laddar det nativa biblioteket:
  // processen kan inte växla från pdfium.dll till pdfium.v8.dll senare
  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;

Varför rapporterade PageCount 1 för ett tvåsidigt formulär?

Före v3.126.1 lagrade PDFium Component argumentet page_count från det nativa sidoevenemanget som dokumenttotalen, och det argumentet är egentligen den absoluta skillnaden mellan det nya och det gamla sidantalet. PDFium utlöser FFI_PageEvent efter att ett layoutpass avslutats med en evenemangstyp av sida tillagd eller sida borttagen; internt uppdaterar det sitt lagrade sidantal först och skickar sedan abs(new - old). Vid den första layouten är det gamla talet noll, så deltan är lika med summan, och ett tresidigt statiskt exempel rapporterar tre sidor som förväntat. Det är exakt därför testformulär med fast längd aldrig avslöjade buggen. Första gången ett dynamiskt formulär växer från en sida till två är deltan 1, och wrappern satte både TPdf.PageCount och parametern NewCount hos OnXfaPageCountChanged till 1. Att ta bort en rad från ett tresidigt formulär gav samma sorts nonsens i den andra riktningen

Att ackumulera deltan på det tidigare värdet är ingen säker reparation heller. Ordningen av initialisering och layoutåteranrop betyder att wrappern inte alltid kan lita på sitt tidigare tal som baslinje, så en löpande summa kan driva. Sedan v3.126.1 ignorerar återanropet argumentet som ett tal och anropar FPDF_GetPageCount på dokumentet, som läser summan från layouten som just fullbordats. Det rensar sedan de cachade sidoscenerna, lagrar den summan som XFA-sidantalöverstyrningen bakom TPdf.PageCount, och först därefter utlöses OnXfaPageCountChanged. När din hanterare kör håller NewCount och FPdf.PageCount med varandra

PDFium Component-diagram för dynamisk XFA där en tillagd rad gör om sidläggningen i ett ensidigt formulär till två sidor och FFI_PageEvent skickar abs(nytt minus gammalt) som en delta, så att den gamla wrappern rapporterade TPdf.PageCount 1 medan v3.126.1 läser FPDF_GetPageCount och rapporterar rätt summa
Det nativa sidoevenemanget rapporterar en tillagd-eller-borttagen delta, inte en summa, så v3.126.1 ignorerar argumentet och läser den fullbordade layouten innan OnXfaPageCountChanged utlöses
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount är den fullbordade layoutens total, aldrig en delta.
  // Detta körs inuti PDFiums layoutåteranrop: uppdatera bara värd-UI-tillstånd,
  // stäng inte dokumentet eller ladda om sidor härifrån
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Utlöses efter varje sidladdning om, inklusive den uppskjutna XFA-uppdateringen
  PageSpin.Value := PdfView1.PageNumber;
end;

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

Evenemanget utlöses bara för Full XFA-formulär vars layout ändras under körning. Statiska XFA- och AcroForm-dokument utlöser det aldrig, så en visare som hanterar båda kan låta samma hanterare vara tilldelad. Att lämna den otilldelad är också säkert; överstyrningen bakom TPdf.PageCount tillämpas ändå, och evenemanget finns för att värden ska kunna uppdatera vad det än cachat

Varför stannar input-rutan på gamla sidan när ett fält flyttar?

Ramen flyttade och redigeraren gjorde inte det för att den nativa XFA-notifieraren jämförde en rektangel med sig själv. När layouten ändrar geometrin hos en widget som redan är laddad ska PDFium märka den nya rektangeln och anropa PerformLayout på widgeten, vilken omplacerar textredigeraren och dess träffyta. Kontrollen jämförde GetWidgetRect() med RecacheWidgetRect(). Båda funktionerna returnerar en const-referens till samma medlem, och recachen skriver över den medlemmen på plats, så jämförelsen såg alltid två identiska värden och laddade widgetar hoppade över sin omläggning

Symptomet kom fram när ett test ändrade ett subformulärs höjd så att befintliga fält korsade över till nästa sida. På båda V8-arkitekturerna ritades fältramarna i sin nya position medan den inskrivna texten och musens träffyta stannade på föregående Y-koordinat. En explicit omläggning fixade det inte, och inte heller att ladda om sidan, för widgeten trodde fortfarande att sin geometri var aktuell. Windows V8-biblioteken som levereras med v3.126.1 kopierar den gamla rektangeln per värde innan recachen och jämför den kopian, så flyttade widgetar lägger om sig och det redigerade värdet dyker upp exakt där ramen är. Det är en nativ fix: den färdas med DLL:erna, så att uppdatera Pascal-enheterna medan man behåller en äldre pdfium.v8.dll lämnar de felplacerade träffytorna kvar. Regressionskontrollen som drev den redigerar en överlevande rad till ett icke-standardvärde först och kräver sedan det värdet på fältets nya plats, för en rad återbyggd med standardvärden skulle annars se ut som ett godkänt

PDFium Component-diagram över widgetomläggning som kontrasterar den gamla självjämförelsen där GetWidgetRect och RecacheWidgetRect returnerade en gemensam medlem så att flyttade widgetar hoppade över PerformLayout, med v3.126.1 Windows V8-kontrollen med kopiering per värde som omplacerar redigeraren och musens träffyta på den omritade ramen
Att jämföra en rektangel med sig själv misslyckas aldrig, så ramen flyttade medan inskriven text och klick stannade kvar tills kontrollen sparade en kopia per värde först

Hur laddar TPdfView om sidor utan att rycka ett handtag undan PDFium?

Sedan v3.126.2 skjuter TPdfView upp sidladdningen om som följer en XFA-layoutändring tills den nativa anropsstacken har vecklat ut sig. Sidoevenemanget utlöses oftast medan PDFium fortfarande bearbetar input: användaren klickade på en Lägg till rad-knapp, klicket körde ett skript, skriptet ändrade instansantalet, och layouten fullbordades inuti samma nativa anrop. Att stänga och öppna sidohandtaget i det ögonblicket skulle frigöra ett objekt som anroparen fortfarande använder. Före v3.126.2 ogiltigförklarade visaren bara sig själv, så det visade sidohandtaget kunde fortsätta peka på tillstånd före layouten, och om användaren hade varit på sista sidan när den försvann var det valda sidonumret utanför intervallet

Den uppskjutna uppdateringen fungerar i några små steg, och de förklarar beteendet du ser från värden:

  1. Sidoevenemangsåteranropet märker vyn som med en väntande XFA-layoutuppdatering och postar ett privat fönstermeddelande; upprepade evenemang innan meddelandet anländer slås ihop till en uppdatering
  2. En vy utan fönsterhandtag ännu behåller den väntande flaggan och postar meddelandet från CreateWnd, medan att byta dokument, avaktivera vyn eller förstöra den rensar flaggan
  3. När meddelandet anländer rensar vyn textmarkeringen, sökmarkeringen och indexet för fokuserat fält, för alla tre syftade på den gamla layouten
  4. Den valda sidan begränsas till det nya PageCount; ett ändrat sidonummer går genom det normala sidbytet, i annat fall laddas aktuella sidan om, och anpassningsläget tillämpas igen
  5. Lämnar layouten inga sidor alls lossar vyn sitt gamla sidohandtag i stället för att rita en sida som inte längre finns
PDFium Component TPdfView-diagram över uppskjuten XFA-uppdatering där ett sidoevenemang inuti den nativa layoutens anropsstack bara märker en väntande uppdatering och postar ett fönstermeddelande, som senare rensar inaktuell markeringstilstånd, begränsar sidan till det nya PageCount och laddar om eller lossar sidohandtaget
Omladdningen väntar tills den nativa anropsstacken vecklar ut sig: ett postat meddelande slår ihop upprepade evenemang, sedan begränsar vyn sidan, laddar om den och utlöser OnPageChange

Samma begränsning gäller din egen kod. OnXfaPageCountChanged körs inuti det nativa layoutåteranropet, så behandla det som en notis: uppdatera etiketter, spinnarintervall och verktygsradstillstånd där, och kölägg allt tyngre, som att stänga dokumentet eller öppna ett annat, med ett postat meddelande så att det körs efter att återanropet returnerat. TPdfView.OnPageChange berättar sedan när vyn faktiskt har laddat om sidan, och att läsa PdfView1.PageNumber vid det laget ger dig det begränsade värdet. Tab-tangentsgenomgång och de FormType-kontroller en formulärvisare kör vid öppning tas upp i PDF-formulärfältsnavigering med PDFium Component

Varför ger ett klick på ett Full XFA-fält "Cannot open text page"?

Full XFA-sidor har ingen PDF-textsida, och före v3.126.2 försökte visarens standardtextmarkering och länkdetektering ändå ladda en. Med TPdfView.AllowUserTextSelection på sitt standardvärde True frågade hovring textlagret efter ett tecken under musen, och ett musuppklick körde en automatisk URL-sondering över sidtexten. På en Full XFA-sida går textsidan inte att öppna, så ett vanligt klick i ett fält kunde sluta i ett undantag Cannot open text page. Sedan v3.126.2 returnerar båda interna vägarna inget resultat när TPdf.FormType är ftXfaFull och XFA-körningen är tillgänglig, så standardinställningarna fungerar och fältinput förblir tillgänglig

Att stänga av AllowUserTextSelection för Full XFA-dokument är fortfarande ett rimligt UI-val, för det finns ingen sidtext att markera och draggester ska inte starta ett markeringsläge. Det är ingen ersättning för att uppgradera dock: på tidigare versioner berodde URL-sonderingen vid klick inte på den egenskapen, så en visare kunde träffa samma undantag med markering avstängd

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType läser det öppna dokumentet, så anropa detta efter FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // Ingen PDF-textlager finns på Full XFA-sidor; fält förblir redigerbara
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Inskrivning behövde sin egen reparation i v3.126.2. Den nativa XFA-textredigeraren ersätter inte en markering när den tar emot ett tecken: FORM_OnChar infogar vid textmarkören, och Backspace raderar ett enskilt tecken, så att markera ett värde och skriva över det gav gammal och ny text sida vid sida. PDFium Component kommer nu ihåg att klicket landade på ett XFA-textfält och dirigerar inskrivna tecken, Backspace och Delete genom FORM_ReplaceSelection närhelst en markering finns och dokumentet beviljar ifyllning eller ändringstillstånd. Huruvida ett skrivskyddat XFA-fält får ändras avgörs fortfarande av den nativa redigeraren, så ett fält märkt skrivskyddat i formuläret behåller sitt värde även i ett dokument som i övrigt tillåter ifyllning. Att sätta TPdfView.AllowFormEvents till False stoppar också denna tangentbordsdirigering, vilket håller en skrivskyddad visare skrivskyddad

Snabbreferens: dynamisk XFA i en Delphi-visare

SymptomOrsakFixat i
Sidantalet visar 1 efter att formuläret växt till två sidorDet nativa sidoevenemanget skickar en tillagd/borttagen delta, inte en summav3.126.1 (wrapper)
Fältramen flyttar, inskriven text och träffyta stannar kvarLaddad widget hoppade över omläggning efter en självjämförelsev3.126.1 (Windows V8-bibliotek)
Visaren ritar eller dirigerar input till sidtillstånd före layoutSidohandtag inte omladdat efter omsidläggningv3.126.2 (uppskjuten uppdatering)
Klick i ett fält ger Cannot open text pageTextmarkering och URL-sondering på sidor utan textlagerv3.126.2
Att skriva över ett markerat värde lägger till i stället för att ersättaDen nativa XFA-redigeraren infogar vid textmarkörenv3.126.2
  • Sätt EnableV8Engine till True innan något dokument läses in, och hantera OnXfaRuntimeMissing för fallet att det vanliga biblioteket laddades först
  • Läs summan från TPdf.PageCount eller parametern NewCount hos OnXfaPageCountChanged; lägg aldrig till eller subtrahera sidantal själv
  • Håll OnXfaPageCountChanged-hanteraren lätt, för den körs inuti det nativa layoutåteranropet
  • Synka aktuella sidindikatorn i TPdfView.OnPageChange, som utlöses efter att den uppskjutna omladdningen begränsat sidnumret
  • Distribuera Windows V8-DLL:erna v3.126.1 eller senare tillsammans med enheterna; widgetomläggningsfixen bor i nativ kod
  • Testa med ett formulär som faktiskt ändrar sitt sidantal och flyttar ett redigerat fält över en sidbrytning, för exempel med fast längd gömmer varje bugg på listan

Dynamisk XFA gör sidantal och fältgeometri till levande värden, och en visare förblir korrekt bara när den hämtar dem från den fullbordade layouten och laddar om sidor vid en säker stund. PDFium Component hanterar båda inuti TPdf och TPdfView, så värden behöver bara lyssna. Detaljer och nedladdningar finns på produktsidan för PDFium Component for Delphi