Odborný článok

Dynamické XFA v PDFium Component: počet strán je delta

Keď dynamický XFA formulár v Delphi prehliadači pridá alebo odstráni strany, reportuje PDFium Component nový celok cez TPdf.PageCount a TPdf.OnXfaPageCountChanged od v3.126.1, pretože natívna straničná udalosť nesie deltu pridané/odstránené, nie celok. Windows V8 knižnice vo v3.126.1 presúvajú vstupné oblasti aj s premiestnenými poliami a v3.126.2 znovu načíta zorozené handle strán po návrate layout callbacku. Bug report, ktorý to odštartoval, bol formulár výdavkového priznania: kliknite dvakrát na Add Row, formulár narastie na dve strany a indikátor strán hrdo hlási 1 of 1. Napíšte do poľa, ktoré sa presunulo na stranu 2, a klávesy dopadnú niekam neviditeľne. Nič z toho sa neukázalo na vzorových formulároch pevnej dĺžky, ktorými sa začína u každého testovanie, a dôvody toho stoja za poznanie, ak vkladáte form viewer

Čo sa deje, keď sa dynamický XFA formulár repaginuje?

Dynamický XFA formulár nemá pevný zoznam strán, takže jeho počet strán je výstupom layoutu a môže sa meniť pri každej úprave dát používateľom. XFA 3.3 popisuje formulár ako strom subformulárov; opakujúci sa subformulár riadi instanceManager a skript ako _Row.addInstance() naklonuje ďalší riadok. Layout procesor potom znova prevalí obsah do oblastí strán, čo môže pridať stranu, ubrať stranu alebo pretlačiť existujúce polia na inú stranu. ISO 32000-1 §12.7.8 definuje len to, ako XFA balíky jazdia vo vnútri PDF; všetko, čo príde potom, patrí XFA engine, ktorým je v PDFium Component vlastné XFA layout PDFia bežiace v procese hostiteľa. Delphi prehliadač má preto činenie s dokumentom, ktorého počet strán, rozmery strán a pozície widgetov sú všetko živý stav. Keď hostiteľ predpokladá opak, pokazia sa tri veci:

  • Počet strán, ktorý si hostiteľ ukladá do cache pre navigáciu, rozsahy rolovania a spinne strán, zorozie, alebo horšie, sa aktualizuje nesprávnym číslom
  • Polia, ktoré sa premiestnia, ukazujú okraj na novej pozícii, kým editor a oblasť zásahu myši ostanú na starých súradniciach
  • Prehliadač drží handle strany, ktorý layout vymenil, takže kliky a kreslenie idú na stranu, ktorá už v tom formulári neexistuje

Trvanie úprav riadkov cez uloženie a znovuotvorenie je osobitný problém s vlastnými pravidlami; tento článok ostáva pri tom, čo sa deje za behu vo vnútri prehliadača

Ktorý PDFium runtime potrebuje dynamické XFA?

Dynamické XFA v PDFium Component vyžaduje V8/XFA zostavenie natívnej knižnice, volené globálnou premennou EnableV8Engine v jednotke PDFium pred načítaním prvého dokumentu. Proces sa prvý raz, keď ktorýkoľvek TPdf načíta knižnicu, zaviaže k jedinému DLL a holé zostavenie PDFium nedokáže XFA engine rozbehnúť vôbec. Keď sa dokument otvorí, TPdf sa síce pozrie do súboru po XFA značkách a prepne na V8 zostavenie automaticky, ale len ak ešte žiadna holá knižnica v tom procese nebola načítaná. Keď sa záväzok už vydal zlou stranou, TPdf.OnXfaRuntimeMissing sa spustí raz, aby mohol hostiteľ povedať používateľovi, nech reštartuje. Explicitné nastavenie príznaku pri štarte odstráni hádanie. Štruktúra callbackov FPDF_FORMFILLINFO nesúca XFA udalosti sa musí tiež zhodovať s DLL; pozadie popisuje FPDF_FORMFILLINFO verzia 2 a XFA callback ABI a rozpoznávanie XFA formulárov a čítanie ich balíkov pokrýva odlíšenie typov formulárov, skôr než otvoríte prehliadač

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Rozhodnite skôr, než prvý TPdf načíta natívnu knižnicu:
  // proces nemôže neskôr prejsť z pdfium.dll na pdfium.v8.dll
  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;

Prečo PageCount hlásilo 1 pri dvojstranovom formulári?

Pred v3.126.1 si PDFium Component ukladal argument page_count natívnej straničnej udalosti ako celok dokumentu a ten argument je v skutočnosti absolútny rozdiel medzi novým a starým počtom strán. PDFium spúšťa FFI_PageEvent po dokončení layout priechodu s typom udalosti strana pridaná alebo strana odstránená; interne si najprv aktualizuje uložený počet strán a potom podá abs(new - old). Pri úvodnom layoute je starý počet nula, takže delta sa rovná celku a trojstranný statický vzor hlási tri strany, ako sa čaká. Presne preto nikdy testovacie formuláre pevnej dĺžky neodhalili bug. Prvý raz, keď dynamický formulár narastie z jednej strany na dve, delta je 1 a wrapper nastavil TPdf.PageCount aj parameter NewCount OnXfaPageCountChanged na 1. Odobratie riadka z trojstranového formulára vyrobilo tú istú triedu nesmyslov opačným smerom

Sčítavanie delty na predchádzajúcu hodnotu tiež nie je bezpečná oprava. Poradie inicializačných a layout callbackov znamená, že wrapper si nemôže vždy siať k skoršiemu počtu ako k základni, takže bežný súčet môže drifovať. Od v3.126.1 ignoruje callback argument ako počet a volá FPDF_GetPageCount nad dokumentom, ktoré číta celok z layoutu, ktorý sa práve dokončil. Potom vyčistí cached scény strán, uloží ten celok ako XFA override počtu strán za TPdf.PageCount a až potom spustí OnXfaPageCountChanged. Kým sa váš handler spustí, NewCount a FPdf.PageCount sedia

Diagram dynamického XFA v PDFium Component, kde pridanie riadka repaginuje jednostranový formulár na dve strany a FFI_PageEvent podáva abs(new mínus old) ako deltu, takže starý wrapper reportoval TPdf.PageCount 1, kým v3.126.1 číta FPDF_GetPageCount a reportuje správny celok
Natívna straničná udalosť reportuje deltu pridané-odstránené, nie celok, takže v3.126.1 ignoruje argument a prečíta dokončený layout, než spustí OnXfaPageCountChanged
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount je celok dokončeného layoutu, nikdy delta.
  // Toto beží vo vnútri layout callbacku PDFium: aktualizujte len UI stav hostiteľa,
  // nezatvárajte odtiaľ dokument ani nenačítavajte strany znova
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Spustí sa po každom znovunačítaní strany, vrátane odkladanej XFA obnovy
  PageSpin.Value := PdfView1.PageNumber;
end;

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

Udalosť sa spúšťa len pri Full XFA formulároch, ktorých layout sa mení za behu. Statické XFA a AcroForm dokumenty ju nikdy nespustia, takže prehliadač obsluhujúci oboje môže nechať ten istý handler priradený. Ponechanie nepriradeného je tiež bezpečné; override za TPdf.PageCount sa aplikuje tak či onak a udalosť existuje preto, aby si hostiteľ mohol obnoviť, čo si cacheoval

Prečo ostané vstupný box na starej strane, keď sa pole presunie?

Okraj sa presunul a editor nie, pretože natívny XFA notifier porovnával obdĺžnik sám so sebou. Keď layout zmení geometriu už načítaného widgetu, PDFium má všimnúť si nový obdĺžnik a zavolať na widget PerformLayout, ktorý presunie textový editor aj jeho oblasť zásahu. Kontrola porovnávala GetWidgetRect() s RecacheWidgetRect(). Obidve funkcie vraciajú const referenciu na ten istý člen a recache prepíše tento člen na mieste, takže porovnanie vždy videlo dve identické hodnoty a načítané widgety preskočili svoj relayout

Príznak vyšiel na povrch, keď test zmenil výšku subformulára tak, že existujúce polia prekročili na ďalšiu stranu. Na oboch V8 architektúrach sa okraj poľa kreslil na novej pozícii, kým napísaný text aj oblasť zásahu myši ostali pri predchádzajúcej súradnici Y. Explicitný relayout to neopravil a znovunačítanie strany tiež nie, lebo widget stále veril, že jeho geometria je aktuálna. Windows V8 knižnice dodané s v3.126.1 skopírujú starý obdĺžnik hodnotou pred recache a porovnávajú tú kópiu, takže presunuté widgety prebehnú relayout a upravená hodnota sa objaví presne tam, kde je okraj. Toto je natívna oprava: cestuje s DLL, takže aktualizácia Pascal jednotiek pri zachovaní staršieho pdfium.v8.dll nechá premiestnené oblasti zásahu na mieste. Regresná kontrola, ktorá ju vynútila, najprv upraví preživší riadok na hodnotu odlišnú od predvolenej a potom žiada tú hodnotu na novej lokalite poľa, lebo riadok prestavený s predvolenými hodnotami by inak vyzeral ako úspech

Diagram relayoutu widgetov v PDFium Component kontrastujúci staré porovnávanie samého so sebou, kde GetWidgetRect a RecacheWidgetRect vracali jediný zdieľaný člen, takže presunuté widgety preskočili PerformLayout, s kontrolou kópiou hodnoty vo Windows V8 v3.126.1, ktorá presunie editor aj oblasť zásahu myši na prekreslený okraj
Porovnávanie obdĺžnika samého so sebou nikdy nezlyhá, takže sa okraj presunul, kým napísaný text a kliky ostávali pozadu, až kým kontrola najprv neuložila kópiu hodnotou

Ako znovu načíta TPdfView strany, než aby vytrhol handle PDFiu spod nôh?

Od v3.126.2 odkladá TPdfView znovunačítanie strany, ktoré nasleduje po zmene XFA layoutu, kým sa natívny zásobník volaní nevyprázdni. Straničná udalosť sa zvyčajne spustí, kým PDFium ešte spracováva vstup: používateľ klikol na tlačidlo Add Row, klik spustil skript, skript zmenil počet inštancií a layout sa dokončil vnútri toho istého natívneho volania. Zavrieť a znovu otvoriť handle strany v ten moment by uvoľnilo objekt, ktorý volajúci stále používa. Pred v3.126.2 sa prehliadač len invalidoval, takže zobrazovaný handle strany mohol naďalej ukazovať na stav pred layoutom a ak bol používateľ na poslednej strane, keď zmizla, vybrané číslo strany bolo mimo rozsahu

Odkladaná obnova funguje v pár malých krokoch a tie vysvetľujú správanie, ktoré vidíte z hostiteľa:

  1. Callback straničnej udalosti označí view, že má nevybavenú XFA layout obnovu, a pošle súkromnú window správu; opakované udalosti pred príchodom správy sa zlúčia do jednej obnovy
  2. View bez window handle si zatiaľ drží príznak čakajúcej obnovy a pošle správu z CreateWnd, zatiaľ čo zmena dokumentu, deaktivácia view alebo jeho zničenie príznak zmaže
  3. Keď správa dorazí, view vyčistí výber textu, hľadacie zvýraznenie a index zaostreného poľa, lebo všetky tri sa viazali na starý layout
  4. Vybraná strana sa svorkuje na nový PageCount; zmenené číslo strany ide cez normálnu výmenu strany, inak sa aktuálna strana znovu načíta a režim prispôsobenia sa aplikuje znova
  5. Ak layout nezanechá žiadne strany, view vyloží svoj starý handle strany namiesto kreslenia strany, ktorá už neexistuje
Diagram odkladanej XFA obnovy TPdfView v PDFium Component, kde straničná udalosť vo vnútri natívneho layout zásobníka volaní len označí čakajúcu obnovu a pošle window správu, ktorá neskôr vyčistí zorozený stav výberu, svorkuje stranu na nový PageCount a znovu načíta alebo vyloží handle strany
Znovunačítanie čaká, kým sa natívny zásobník volaní nevyprázdni: poslaná správa zlúči opakované udalosti, potom view svorkuje stranu, znovu ju načíta a spustí OnPageChange

To isté obmedzenie platí pre váš vlastný kód. OnXfaPageCountChanged beží vo vnútri toho natívneho layout callbacku, takže sa k nemu stavajte ako k notifikácii: tam aktualizujte návestia, rozsahy spinnerov a stav panela nástrojov a čokoľvek ťažšie, ako zatvorenie dokumentu alebo otvorenie iného, radte do fronty cez poslanú správu, aby bežalo po návrate callbacku. TPdfView.OnPageChange vám potom povie, kedy view skutočne znovu načítal stranu a čítanie PdfView1.PageNumber v ten okamih dáva svorkovanú hodnotu. Prechádzanie Tabom a kontroly FormType, ktoré form viewer robí pri otvorení, pokrýva navigácia formulárových polí PDF s PDFium Component

Prečo kliknutie na pole Full XFA vyhodí „Cannot open text page“?

Strany Full XFA nemajú PDF textovú stranu a pred v3.126.2 sa predvolený výber textu a rozpoznávanie odkazov v prehliadači aj tak snažili jednu načítať. S TPdfView.AllowUserTextSelection na predvolenom True sa preletenie myšou pýtalo textovej vrstvy na znak pod kurzorom a klik pri pustení tlačidla pustil automatickú URL sondu nad textom strany. Na strane Full XFA sa textová strana otvoriť nedá, takže obyčajný klik do poľa mohol skončiť výnimkou Cannot open text page. Od v3.126.2 vracajú obidve interné cesty žiadny výsledok, keď je TPdf.FormType ftXfaFull a XFA runtime je dostupný, takže predvolené nastavenia fungujú a vstup do polí ostáva dostupný

Vypnúť AllowUserTextSelection pre Full XFA dokumenty je stále rozumná UI voľba, lebo nie je žiadny text strany na výber a ťahové gestá nemajú spúšťať režim výberu. Nie je to však náhrada za upgrade: na skorších verziách URL sonda pri kliknutí na tejto vlastnosti nezávisela, takže prehliadač mohol naraziť na tú istú výnimku aj s vypnutým výberom

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType číta otvorený dokument, takže to volajte po FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // Na stranách Full XFA neexistuje PDF textová vrstva; polia ostávajú editovateľné
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Písanie si vo v3.126.2 vyžiadalo vlastnú opravu. Natívny XFA textový editor nenahrádza výber, keď dostane znak: FORM_OnChar vkladá na pozíciu kurzora a Backspace maže jediný znak, takže vybratie hodnoty a písanie cez ňu vyrábalo starý a nový text vedľa seba. PDFium Component si teraz pamätá, že klik pristál na XFA textovom poli a smeruje písané znaky, Backspace a Delete cez FORM_ReplaceSelection, kedykoľvek existuje výber a dokument udeľuje povolenie vyplňovať formuláre alebo upravovať. To, či sa smie zmeniť pole XFA len na čítanie, stále rozhoduje natívny editor, takže pole označené vo formulári ako read-only si drží hodnotu aj v dokumente, ktorý inak vyplňovanie dovoľuje. Nastavenie TPdfView.AllowFormEvents na False tiež zastaví toto smerovanie klávesnice, čím zostane read-only prehliadač read-only

Rýchly prehľad: dynamické XFA v Delphi prehliadači

PríznakPríčinaOpravené vo
Počet strán ukazuje 1, keď formulár narastie na dve stranyNatívna straničná udalosť podáva deltu pridané/odstránené, nie celokv3.126.1 (wrapper)
Okraj poľa sa presunie, napísaný text a oblasť zásahu ostanú pozaduNačítaný widget preskočil relayout po porovnaní samého so sebouv3.126.1 (Windows V8 knižnice)
Prehliadač kreslí alebo smeruje vstup na stav strany pred layoutomHandle strany sa po repaginácii znovu nenačítalv3.126.2 (odložená obnova)
Klik do poľa vyhodí Cannot open text pageVýber textu a URL sonda na stranách bez textovej vrstvyv3.126.2
Písanie cez vybranú hodnotu prilepí namiesto nahradeniaNatívny XFA editor vkladá na pozíciu kurzorav3.126.2
  • Nastavte EnableV8Engine na True pred načítaním akéhokoľvek dokumentu a obslúžte OnXfaRuntimeMissing pre prípad, že holá knižnica sa načítala ako prvá
  • Čítajte celok z TPdf.PageCount alebo parametra NewCount OnXfaPageCountChanged; nikdy si počty strán nesčítavajte ani neodpočítavajte sami
  • Držte handler OnXfaPageCountChanged ľahký, lebo beží vo vnútri natívneho layout callbacku
  • Synchronizujte indikátor aktuálnej strany v TPdfView.OnPageChange, ktorý zasahuje po tom, ako odkladané znovunačítanie svorkuje číslo strany
  • Nasádzajte Windows V8 DLL vo v3.126.1 a novšie spolu s jednotkami; oprava relayoutu widgetov býva v natívnom kóde
  • Testujte s formulárom, ktorý skutočne mení počet strán a presúva upravené pole cez zlom strany, lebo vzorky pevnej dĺžky skryjú každý bug z tohto zoznamu

Dynamické XFA mení počet strán a geometriu polí na živé hodnoty a prehliadač ostáva správny len vtedy, keď ich berie z dokončeného layoutu a znovu načíta strany v bezpečnom momente. PDFium Component obslúzhi oboje vo vnútri TPdf a TPdfView, takže hostiteľovi ostáva len počúvať. Detaily a stiahnutie sú na stránke produktu PDFium Component pre Delphi