Když dynamický formulář XFA v prohlížeči v Delphi přidá nebo ubere stránky, hlásí PDFium Component nový součet přes TPdf.PageCount a TPdf.OnXfaPageCountChanged od v3.126.1, protože nativní stránková událost nese přidanou/odebranou deltu, nikoli součet. Windows V8 knihovny ve v3.126.1 taky stěhují vstupní oblasti s přemístěnými poli a v3.126.2 znovu načítá zastaralé handly stránek po návratu layout callbacku. Bug report, který to rozpoutal, byl formulář vyúčtování: klikněte dvakrát na Add Row, formulář naroste na dvě stránky a indikátor stránek hrdě hlásí 1 z 1. Píšete-li do pole, které se přestěhovalo na stránku 2, dopadnou klávesy někam neviditelně. Nic z toho se neukázalo na vzorových formulářích s pevnou délkou, kterými se testuje každý nejdřív, a důvody stojí za to znát, když do aplikace vkládáte prohlížeč formulářů
Co se stane, když se dynamický formulář XFA přestránkuje?
Dynamický formulář XFA nemá pevný seznam stránek, takže jeho počet stránek je výstup layoutu a může se změnit pokaždé, když uživatel upraví data. XFA 3.3 popisuje formulář jako strom subformů; opakovaný subform řídí instanceManager a skript jako _Row.addInstance() naklonuje jednu řádku navíc. Layout procesor pak znovu protéká obsah do stránkových ploch, což může přidat stránku, ubrat stránku nebo tlačit stávající pole na jinou stránku. ISO 32000-1 §12.7.8 definuje jen to, jak XFA pakety cestují uvnitř PDF; všechno, co přijde potom, patří XFA jádru, které je v PDFium Component vlastní XFA layout PDFium běžící v hostitelském procesu. Prohlížeč v Delphi se proto potýká s dokumentem, jehož počet stránek, velikosti stránek i pozice widgetů jsou živý stav. Tři věci se pokazí, když hostitel předpokládá opak:
- Počet stránek, který hostitel kešuje pro navigaci, rozsahy scrollování a přepínače stránek, zastará, nebo hůř, aktualizuje se špatným číslem
- Pole, která se přestěhují, ukazují rámeček na nové pozici, zatímco editor a oblast zásahů myši zůstávají na starých souřadnicích
- Prohlížeč drží handle stránky, který layout vystřídal, takže kliky a kreslení míří na stránku, která v tom formuláři už neexistuje
Udržet úpravy řádků napříč uložením a znovuotevřením je zvláštní problém s vlastními pravidly; tenhle článek zůstává u toho, co se děje za běhu uvnitř prohlížeče
Které PDFium runtime dynamické XFA potřebuje?
Dynamické XFA v PDFium Component vyžaduje build V8/XFA nativní knihovny, vybraný globální proměnnou EnableV8Engine v unitě PDFium před načtením prvního dokumentu. Proces se na jedno DLL zaváže v okamžiku, kdy jakýkoli TPdf knihovnu načte, a prostý build PDFium nedokáže XFA jádro roztáčet vůbec. Když se dokument otevře, TPdf se do souboru podívá po XFA značkách a na V8 build přepne automaticky, ale jen když v procesu ještě nebyla načtena žádná prostá knihovna. Když už se závazek rozhodl špatným směrem, TPdf.OnXfaRuntimeMissing jednou zabliká, aby mohl hostitel říct uživateli, ať aplikaci restartuje. Explicitní nastavení příznaku při startu vyhazuje dohadování ze hry. Struktura callbacků FPDF_FORMFILLINFO, která nese XFA události, musí navíc sedět na DLL; pozadí najdete v FPDF_FORMFILLINFO verze 2 a XFA callback ABI a rozpoznávání formulářů XFA a čtení jejich paketů pokrývá rozeznávání typů formulářů, než otevřete prohlížeč
uses
PDFium;
procedure TClaimForm.FormCreate(Sender: TObject);
begin
// Rozhodněte se, než první TPdf načte nativní knihovnu:
// proces už nedokáže později přepnout 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;
Proč hlásil PageCount 1 u dvoustránkového formuláře?
Před v3.126.1 si PDFium Component ukládal argument page_count nativní stránkové události jako součet dokumentu, a ten argument je doopravdy absolutní rozdíl mezi novým a starým počtem stránek. PDFium vyhodí FFI_PageEvent po dokončení layout průchodu s typem události stránka přidána nebo stránka odebrána; uvnitř si nejdřív aktualizuje uložený počet stránek a pak podá abs(new - old). Při počátečním layoutu je starý počet nula, takže delta se rovná součtu a třístránkový statický vzorek hlásí tři stránky, jak se čeká. Přesně proto formuláře s pevnou délkou tenhle bug nikdy neodhalily. Jakmile dynamický formulář poprvé naroste z jedné stránky na dvě, delta je 1 a wrapper nastavil TPdf.PageCount i parametr NewCount události OnXfaPageCountChanged na 1. Odebrání řádku ze třístránkového formuláře vyrobilo stejný druh nesmyslů opačným směrem
Sčítat deltu k předchozí hodnotě není bezpečná oprava taky. Pořadí inicializačních a layout callbacků znamená, že wrapper si nemůže vždycky vzít svůj dřívější počet jako základ, takže běžící součet může bloudit. Od v3.126.1 ignoruje callback argument jako počet a zavolá FPDF_GetPageCount nad dokumentem, který čte součet z layoutu, který právě doběhl. Pak vyčistí kešované stránkové scény, uloží ten součet jako XFA přepsání počtu stránek za TPdf.PageCount a teprve pak vyhodí OnXfaPageCountChanged. V okamžiku, kdy běží váš handler, souhlasí NewCount a FPdf.PageCount
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
// v3.126.1+: NewCount je součet dokončeného layoutu, nikdy delta.
// Tohle běží uvnitř layout callbacku PDFium: aktualizujte jen UI stav hostitele,
// nezavírejte odtud dokument ani znovu nenačítejte stránky
UpdatePageRange(NewCount);
end;
procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
// Vyhodí se po každém znovunačtení stránky, včetně odloženého 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;
Událost se vyhodí jen pro Full XFA formuláře, jejichž layout se mění za běhu. Statické XFA a AcroForm dokumenty ji nevyhodí nikdy, takže prohlížeč, který obsluhuje obojí, může nechat přiřazený tentýž handler. Nechat ho nepřiřazený je taky bezpečné; přepsání za TPdf.PageCount se aplikuje stejně a událost existuje proto, aby si hostitel mohl osvěžit všechno, co kešoval
Proč zůstává vstupní box na staré stránce, když se pole přestěhuje?
Rámeček se přestěhoval a editor ne, protože nativní XFA notifier porovnával obdélník sám se sebou. Když layout změní geometrii už načteného widgetu, PDFium má zpozorovat nový obdélník a zavolat na widgetu PerformLayout, což přemístí textový editor i jeho oblast zásahů. Kontrola porovnávala GetWidgetRect() s RecacheWidgetRect(). Obě funkce vrací const referenci na téhož člena a recache ten člena přepíše na místě, takže srovnání vždycky vidělo dvě identické hodnoty a načtené widgety přeskočily svůj relayout
Příznak se vynořil, když test změnil výšku subformu tak, aby se stávající pole přehoupla na další stránku. Na obou V8 architekturách se rámeček pole kreslil na nové pozici, zatímco napsaný text a oblast zásahů myši zůstávaly na předchozí souřadnici Y. Explicitní relayout to neopravil a znovunačtení stránky taky ne, protože widget si pořád myslel, že jeho geometrie je aktuální. Windows V8 knihovny dodané s v3.126.1 zkopírují starý obdélník hodnotou před recache a porovnávají tu kopii, takže přestěhované widgety projdou relayoutem a upravená hodnota se objeví přesně tam, kde je rámeček. Tahle oprava je nativní: cestuje s DLL, takže aktualizace Pascal unit při zachování staršího pdfium.v8.dll nechá špatně umístěné oblasti zásahů na místě. Regresní test, který ji vypátral, nejdřív upraví přeživší řádek na ne-výchozí hodnotu a pak vyžaduje tu hodnotu na nové pozici pole, protože řádek postavený znovu s výchozími hodnotami by jinak vypadal jako úspěch
Jak znovunačítá TPdfView stránky, aniž by PDFium vytáhl handle pod rukama?
Od v3.126.2 odkládá TPdfView znovunačtení stránky, které následuje po změně XFA layoutu, dokud se nativní call stack nesbalí. Stránková událost většinou vyhodí, když PDFium pořád zpracovává vstup: uživatel klikl na tlačítko Add Row, klik spustil skript, skript změnil počet instancí a layout doběhl uvnitř téhož nativního volání. Zavřít a znovu otevřít handle stránky v ten okamžik by uvolnilo objekt, se kterým volající pořád pracuje. Před v3.126.2 se prohlížeč jen zneplatnil, takže zobrazený handle stránky mohl ukazovat na stav před layoutem, a byl-li uživatel na poslední stránce, když zmizela, bylo vybrané číslo stránky mimo rozsah
Odložený refresh běží v pár malých krocích a ty vysvětlují chování, které vidíte z hostitele:
- Callback stránkové události označí pohled jako mající nevyřízený XFA layout refresh a pošle soukromou window zprávu; opakované události, než zpráva dorazí, se sloučí do jednoho refresh
- Pohled bez window handlu si drží nevyřízený příznak a pošle zprávu z
CreateWnd, zatímco změna dokumentu, deaktivace pohledu nebo jeho zničení příznak vymaže - Když zpráva dorazí, pohled vymaže výběr textu, zvýraznění hledání a index zaměřeného pole, protože všechny tři odkazovaly na starý layout
- Vybraná stránka se upne na nový
PageCount; změněné číslo stránky jde normálním přepnutím stránky, jinak se znovu načte aktuální stránka a znovu se aplikuje režim fit - Nechá-li layout vůbec žádné stránky, pohled vyloží svůj starý handle stránky místo kreslení stránky, která už neexistuje
Táž omezení platí pro váš vlastní kód. OnXfaPageCountChanged běží uvnitř toho nativního layout callbacku, takže k němu přistupujte jako k notifikaci: aktualizujte tam popisky, rozsahy spinneru a stav toolbaru a cokoli těžšího, jako zavření dokumentu nebo otevření jiného, dejte do fronty přes poslanou zprávu, aby to běželo po návratu callbacku. TPdfView.OnPageChange vám pak řekne, kdy pohled doopravdy znovu načetl stránku a přečtení PdfView1.PageNumber v ten okamžik dá upnutou hodnotu. Procházení klávesou Tab a kontrola FormType, které prohlížeč formulářů pustí při otevření, pokrývá navigace formulářových polí PDF s PDFium Component
Proč klik do pole Full XFA hodí „Cannot open text page“?
Full XFA stránky nemají textovou stránku PDF a před v3.126.2 se výchozí výběr textu a detekce odkazů prohlížeče přesto snažily jednu načíst. S TPdfView.AllowUserTextSelection na výchozí hodnotě True se hover ptal textové vrstvy na znak pod myší a mouse-up klik pustil automatickou URL sondu nad textem stránky. Na stránce Full XFA se textová stránka otevřít nedá, takže obyčejný klik do pole mohl skončit výjimkou Cannot open text page. Od v3.126.2 vracejí obě interní cesty žádný výsledek, když je TPdf.FormType ftXfaFull a XFA runtime je dostupný, takže výchozí nastavení funguje a vstup do polí zůstává dostupný
Vypnout AllowUserTextSelection u dokumentů Full XFA zůstává rozumnou volbou UI, protože není co vybírat a tažení gestem nemá spouštět režim výběru. Není to ale náhrada za upgrade: na starších verzích URL sonda při kliku nezávisela na té vlastnosti, takže prohlížeč mohl narazit na tutéž výjimku i se zakázaným výběrem
procedure TClaimForm.ConfigureViewerForForm;
begin
// FormType čte otevřený dokument, takže volejte po FPdf.Active := True
if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
begin
// Full XFA stránky nemají textovou vrstvu PDF; pole zůstávají editovatelná
PdfView1.AllowUserTextSelection := False;
StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
[FPdf.PageCount]);
end
else
PdfView1.AllowUserTextSelection := True;
end;
Psaní si ve v3.126.2 řeklo o vlastní opravu. Nativní XFA textový editor nenahradí výběr, když dostane znak: FORM_OnChar vkládá na kurzor a Backspace maže jediný znak, takže vybrat hodnotu a napsat přes ni vyprodukovalo starý a nový text bok po boku. PDFium Component si teď pamatuje, že klik dopadl na textové pole XFA, a routuje napsané znaky, Backspace a Delete přes FORM_ReplaceSelection, kdykoli existuje výběr a dokument dovoluje vyplňování formulářů nebo úpravu. O tom, zda se smí měnit read-only XFA pole, pořád rozhoduje nativní editor, takže pole označené ve formuláři read-only si drží hodnotu i v dokumentu, který jinak vyplňování dovoluje. Nastavení TPdfView.AllowFormEvents na False navíc zastaví tohle keyboard routing, což udrží read-only prohlížeč read-only
Rychlý přehled: dynamické XFA v prohlížeči v Delphi
| Příznak | Příčina | Opraveno ve |
|---|---|---|
| Počet stránek ukazuje 1 poté, co formulář naroste na dvě stránky | Nativní stránková událost podá přidanou/odebranou deltu, ne součet | v3.126.1 (wrapper) |
| Rámeček pole se přestěhuje, napsaný text a oblast zásahů zůstávají pozadu | Načtený widget přeskočil relayout po samosrovnání | v3.126.1 (Windows V8 knihovny) |
| Prohlížeč kreslí nebo routuje vstup na stav stránky před layoutem | Handle stránky se po přestránkování znovu nenačetl | v3.126.2 (odložený refresh) |
| Klik do pole hodí Cannot open text page | Výběr textu a URL sonda na stránkách bez textové vrstvy | v3.126.2 |
| Psaní přes vybranou hodnotu přidává místo nahrazení | Nativní XFA editor vkládá na kurzor | v3.126.2 |
- Nastavte
EnableV8EnginenaTrue, než se načte jakýkoli dokument, a obsluhujteOnXfaRuntimeMissingpro případ, že se nejdřív načetla prostá knihovna - Čtěte součet z
TPdf.PageCountnebo z parametruNewCountudálostiOnXfaPageCountChanged; nikdy si počty stránek nepřičítejte ani neodečítejte sami - Držte handler
OnXfaPageCountChangedlehký, protože běží uvnitř nativního layout callbacku - Synchronizujte indikátor aktuální stránky v
TPdfView.OnPageChange, který se vyhodí poté, co odložené znovunačtení upne číslo stránky - Nasazujte Windows V8 DLL ve v3.126.1 a novější spolu s unitami; oprava relayoutu widgetů bydlí v nativním kódu
- Testujte s formulářem, který doopravdy mění počet stránek a přestěhuje upravené pole přes zlom stránky, protože vzorky s pevnou délkou skryjí každý bug z tohohle seznamu
Dynamické XFA mění počet stránek a geometrii polí v živé hodnoty a prohlížeč zůstává správný jen tehdy, když si je bere z dokončeného layoutu a znovu načítá stránky v bezpečném okamžiku. PDFium Component obsluhuje oboje uvnitř TPdf a TPdfView, takže hostitel už jen naslouchá. Detaily a stažení najdete na stránce produktu PDFium Component pro Delphi