Tehnički članak

PDFium Component dinamički XFA: broj stranica je delta

Kad dinamički XFA obrazac u Delphi pregledaču doda ili ukloni stranice, PDFium Component od v3.126.1 prijavljuje novi ukupan broj kroz TPdf.PageCount i TPdf.OnXfaPageCountChanged, jer nativni događaj stranice nosi dodato/uklonjeno delta, a ne ukupan broj. Windows V8 biblioteke u v3.126.1 takođe pomeraju površine pogodaka unosa sa preseljenim poljima, a v3.126.2 ponovo učitava zastarele page handle-ove posle što se layout callback vrati. Prijava baga koja je ovo pokrenula bila je obrazac za naplatu troškova: klikni Add Row dvaput, obrazac naraste na dve stranice, i indikator stranice ponosno piše 1 of 1. Kucaj u polje koje je preseljeno na stranicu 2 i pritisci tastere dospevaju negde nevidljivo. Ništa od toga se nije pokazalo na obrascima fiksnih dužina s kojima svi prvo testiraju, a razlozi zašto vredi da ih znate ako ugrađujete pregledač obrazaca

Šta se dešava kad se dinamički XFA obrazac repaginira?

Dinamički XFA obrazac nema fiksnu listu stranica, pa mu je broj stranica izlaz rasporeda i može se menjati svaki put kad korisnik menja podatke. XFA 3.3 opisuje obrazac kao stablo podobrazaca; ponavljajući podobrazac kontroliše instanceManager, a skripta poput _Row.addInstance() klonira još jedan red. Procesor rasporeda zatim ponovo uliva sadržaj u površine stranica, što može da doda stranicu, oduzme stranicu ili prebaci postojeća polja na drugu stranicu. ISO 32000-1 §12.7.8 definiše samo kako XFA paketi putuju unutar PDF-a; sve što se posle toga dešava pripada XFA engine-u, koji je u PDFium Component-u PDFium-ov sopstveni XFA raspored koji radi u procesu domaćina. Delphi pregledač se dakle bavi dokumentom čiji su broj stranica, veličine stranica i pozicije widgeta svi živo stanje. Tri stvari krenu naopako kad domaćin pretpostavi drugačije:

  • Broj stranica koji domaćin kešira za navigaciju, opsege klizanja i brojače stranica zastareva, ili još gore, ažurira se pogrešnim brojem
  • Polja koja se presele pokazuju ivicu na novoj poziciji dok editor i površina pogodaka miša ostaju na starim koordinatama
  • Pregledač zadržava page handle koji je raspored zamenio, pa klikovi i crtanje idu na stranicu koja više ne postoji u tom obrascu

Održavanje izmena redova kroz čuvanje i ponovno otvaranje je odvojen problem sa svojim pravilima; ovaj tekst ostaje kod onoga što se dešava za vreme rada unutar pregledača

Koji PDFium runtime treba dinamički XFA?

Dinamički XFA u PDFium Component-u traži V8/XFA build nativne biblioteke, biran globalnom promenljivom EnableV8Engine u jedinici PDFium pre nego što se prvi dokument učita. Proces se opredeljuje za jedan DLL prvi put kad bilo koji TPdf učita biblioteku, i običan PDFium build ne može uopšte da pokrene XFA engine. Kad se dokument otvori, TPdf zirisne u fajl za XFA oznake i prebacuje se na V8 build automatski, ali samo ako još nijedna obična biblioteka nije učitana u tom procesu. Kad se opredeljenje već krenulo na pogrešnu stranu, TPdf.OnXfaRuntimeMissing opali jednom da domaćin može reći korisniku da restartuje. Eksplicitno postavljanje flaga pri startu uklanja nagađanje. Struktura callback-a FPDF_FORMFILLINFO koja nosi XFA događaje takođe mora da se poklapa s DLL-om; pozadina je u tekstu o FPDF_FORMFILLINFO verziji 2 i XFA callback ABI-ju, a otkrivanje XFA obrazaca i čitanje njihovih paketa pokriva razlikovanje tipova obrazaca pre nego što otvorite pregledač

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Odluči pre nego što prvi TPdf učita nativnu biblioteku:
  // proces ne može kasnije da pređe s 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;

Zašto je PageCount prijavio 1 za obrazac od dve stranice?

Pre v3.126.1 PDFium Component je čuvao argument page_count nativnog događaja stranice kao ukupan broj dokumenta, a taj argument je zapravo apsolutna razlika između novog i starog broja stranica. PDFium podiže FFI_PageEvent kad prolaz rasporeda završi s tipom događaja stranica dodata ili uklonjena; interno prvo ažurira svoj sačuvani broj stranica pa onda prosleđuje abs(new - old). Na početnom rasporedu stari broj je nula, pa delta jednaka ukupnom broju, i statički ogledni obrazac od tri stranice prijavljuje tri stranice kao što se očekuje. Baš zato obrasci za test fiksnih dužina nikad nisu otkrili bag. Prvi put kad se dinamički obrazac proširi sa jedne na dve stranice, delta je 1, i omotač je postavio i TPdf.PageCount i parametar NewCount od OnXfaPageCountChanged na 1. Uklanjanje reda iz obrasca od tri stranice dalo je istu vrstu besmislica u drugom smeru

Nagomilavanje delte na prethodnu vrednost nije ni sigurna popravka. Redosled callback-a inicijalizacije i rasporeda znači da omotač ne može uvek da veruje svom ranijem broju kao bazi, pa tekući zbir može da odstupi. Od v3.126.1 callback ignoriše argument kao broj i zove FPDF_GetPageCount nad dokumentom, koji čita ukupan broj iz upravo završenog rasporeda. Zatim briše keširane scene stranica, čuva taj ukupan broj kao XFA premošćenje broja stranica iza TPdf.PageCount-a, i tek onda podiže OnXfaPageCountChanged. Do trenutka kad vaš handler radi, NewCount i FPdf.PageCount se slažu

PDFium Component dijagram dinamičkog XFA gde dodavanje reda repaginira obrazac od jedne stranice na dve i FFI_PageEvent prosleđuje abs(novo minus staro) kao delta, pa je stari omotač prijavio TPdf.PageCount 1 dok v3.126.1 čita FPDF_GetPageCount i prijavljuje pravi ukupan broj
Nativni događaj stranice prijavljuje dodato-ili-uklonjeno delta, a ne ukupan broj, pa v3.126.1 ignoriše argument i čita završeni raspored pre nego što podigne OnXfaPageCountChanged
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount je ukupan broj završenog rasporeda, nikad delta.
  // Ovo radi unutar PDFium layout callback-a: ažuriraj samo UI stanje domaćina,
  // ne zatvaraj dokument i ne učitavaj stranice ponovo odavde
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Okida se posle svakog ponovnog učitavanja stranice, uključujući odloženi 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;

Događaj opali samo za Full XFA obrasce čiji se raspored menja za vreme rada. Statički XFA i AcroForm dokumenti ga nikad ne podižu, pa pregledač koji rukuje oba može da ostavi isti handler dodeljen. Neodateljan handler je takođe bezbedan; premošćenje iza TPdf.PageCount-a primenjuje se svakako, a događaj postoji da domaćin osveži šta god je keširao

Zašto kutija za unos ostaje na staroj stranici kad se polje preseli?

Ivica se pomerila a editor nije jer je nativni XFA notifier poredio pravougaonik sa samim sobom. Kad raspored promeni geometriju već učitanog widgeta, od PDFium-a se očekuje da primeti nov pravougaonik i pozove PerformLayout nad widgetom, koji premešta tekstualni editor i njegovu površinu pogodaka. Provera je uporedila GetWidgetRect() s RecacheWidgetRect(). Obe funkcije vraćaju const referencu na istog člana, a recache prepisuje tog člana na mestu, pa je poređenje uvek videlo dve identične vrednosti i učitani widgeti su preskakali svoj relayout

Simptom je isplivao kad je test promenio visinu podobrasca tako da postojeća polja pređu na sledeću stranicu. Na obe V8 arhitekture ivica polja crtala se na novoj poziciji dok su kucani tekst i površina pogodaka miša ostajali na prethodnoj Y koordinati. Eksplicitan relayout to nije ispravio, ni ponovno učitavanje stranice, jer je widget i dalje verovao da mu je geometrija tekuća. Windows V8 biblioteke isporučene s v3.126.1 kopiraju stari pravougaonik po vrednosti pre recache-a i porede tu kopiju, pa se preseljeni widgeti relayout-uju i uređena vrednost ispadne tačno tamo gde je ivica. Ovo je nativna popravka: putuje s DLL-ovima, pa ažuriranje Pascal jedinica uz zadržavanje starijeg pdfium.v8.dll-a ostavlja pomerene površine pogodaka na mestu. Regresiona provera koja je to gonila prvo uređuje preživeli red u ne-podrazumevanu vrednost, a zatim traži tu vrednost na novoj lokaciji polja, jer bi red obnovljen podrazumevanim vrednostima inače izgledao kao prolaz

PDFium Component dijagram relayout-a widgeta koji suprotstavlja staro samo-poređenje gde su GetWidgetRect i RecacheWidgetRect vraćali istog deljenog člana pa su preseljeni widgeti preskakali PerformLayout, s v3.126.1 Windows V8 proverom kopije-po-vrednosti koja premešta editor i površinu pogodaka miša na precrtanu ivicu
Poređenje pravougaonika sa samim sobom nikad ne pada, pa se ivica pomerila dok su kucani tekst i klikovi ostajali iza, sve dok provera nije prvo sačuvala kopiju po vrednosti

Kako TPdfView ponovo učitava stranice ne vadeći handle ispod PDFium-a?

Od v3.126.2 TPdfView odlaže ponovno učitavanje stranice koje sledi XFA promenu rasporeda dok se nativni stek poziva ne odmota. Događaj stranice obično opali dok PDFium još obrađuje unos: korisnik je kliknuo dugme Add Row, klik je pokrenuo skriptu, skripta je promenila broj instanci, i raspored je završio unutar tog istog nativnog poziva. Zatvaranje i ponovno otvaranje page handle-a u tom trenutku oslobodilo bi objekat koji pozivaoc još koristi. Pre v3.126.2 pregledač se samo invalidirao, pa prikazani page handle mogao je da ostane usmeren na stanje pre rasporeda, i ako je korisnik bio na poslednjoj stranici kad je nestala, izabrani broj stranice bio je van opsega

Odloženo osvežavanje radi u par malih koraka, i oni objašnjavaju ponašanje koje vidite s domaćinom:

  1. Callback događaja stranice označava prikaz kao s odloženim XFA osvežavanjem rasporeda i objavljuje privatnu prozorsku poruku; ponovljeni događaji pre dolaska poruke stapaju se u jedno osvežavanje
  2. Prikaz bez window handle-a još zadržava flag čekanja i objavljuje poruku iz CreateWnd-a, dok promena dokumenta, deaktivacija prikaza ili njegovo uništavanje brišu flag
  3. Kad poruka stigne, prikaz briše izbor teksta, highlight pretrage i indeks fokusiranog polja, jer se sva tri odnosila na stari raspored
  4. Izabrana stranica se stega na novi PageCount; promenjeni broj stranice ide kroz normalnu zamenu stranice, inače se trenutna stranica ponovo učitava, i režim prilagođavanja se ponovo primenjuje
  5. Ako raspored ne ostavi nijednu stranicu, prikaz istovaruje stari page handle umesto da crta stranicu koja više ne postoji
PDFium Component TPdfView dijagram odloženog XFA osvežavanja gde događaj stranice unutar nativnog steka poziva rasporeda samo označava odloženo osvežavanje i objavljuje prozorsku poruku, koja kasnije briše zastarelo stanje izbora, stegne stranicu na novi PageCount i ponovo učita ili istovari page handle
Ponovno učitavanje čeka da se nativni stek poziva odmota: objavljena poruka stapa ponovljene događaje, pa prikaz stegne stranicu, ponovo je učita i podigne OnPageChange

Isto ograničenje važi i za vaš sopstveni kod. OnXfaPageCountChanged radi unutar tog nativnog layout callback-a, pa ga tretirajte kao obaveštenje: ažurirajte oznake, opsege brojača i stanje trake alata tamo, a sve teže, poput zatvaranja dokumenta ili otvaranja drugog, stavite u red s objavljenom porukom da se izvrši posle što se callback vrati. TPdfView.OnPageChange vam zatim kaže kad je prikaz zaista ponovo učitao stranicu, i čitanje PdfView1.PageNumber-a u tom trenutku daje vam stegnutu vrednost. Prelazak Tab tasterom i FormType provere koje pregledač obrazaca radi pri otvaranju pokriveni su u tekstu o navigaciji po poljima PDF obrazaca s PDFium Component-om

Zašto klik na Full XFA polje podiže "Cannot open text page"?

Full XFA stranice nemaju PDF tekstualnu stranicu, i pre v3.126.2 podrazumevani izbor teksta i detekcija veza u pregledaču pokušali su da je učitaju svakako. S TPdfView.AllowUserTextSelection na podrazumevanom True-u, lebdenje je pitalo tekstualni sloj za znak ispod miša, a klik mišem puštao je automatsku URL proveru nad tekstom stranice. Na Full XFA stranici tekstualna stranica ne može da se otvori, pa običan klik u polje mogao je da se završi izuzetkom Cannot open text page. Od v3.126.2 oba interna puta vraćaju bez rezultata kad je TPdf.FormType ftXfaFull i XFA runtime dostupan, pa podrazumevane postavke rade i unos u polja ostaje dostupan

Isključivanje AllowUserTextSelection-a za Full XFA dokumente je i dalje razumna UI odluka, jer nema teksta stranice za izbor i gestovi vučenja ne treba da pokreću režim izbora. Nije to zamena za nadogradnju, međutim: na ranijim verzijama URL provera pri kliku nije zavisila od tog svojstva, pa je pregledač mogao da pogodi isti izuzetak i sa isključenim izborom

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType čita otvoreni dokument, pa ovo zovi posle FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // Na Full XFA stranicama ne postoji PDF tekstualni sloj; polja ostaju uređiva
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Kucanje je dobilo svoju popravku u v3.126.2. Nativni XFA tekstualni editor ne zamenjuje izbor kad primi znak: FORM_OnChar ubacuje na kursor, a Backspace briše jedan znak, pa je izbor vrednosti i kucanje preko nje davalo stari i novi tekst jedan uz drugi. PDFium Component sada pamti da je klik dospeo na XFA tekstualno polje i usmerava kucane znakove, Backspace i Delete kroz FORM_ReplaceSelection kad god izbor postoji i dokument daje dozvolu popunjavanja ili izmene. Da li se read-only XFA polje sme menjati i dalje odlučuje nativni editor, pa polje označeno read-only u obrazcu zadržava vrednost čak i u dokumentu koji inače dozvoljava popunjavanje. Postavljanje TPdfView.AllowFormEvents na False takođe prekida ovo usmeravanje tastature, što pregledač samo za čitanje drži samo za čitanje

Brzi podsetnik: dinamički XFA u Delphi pregledaču

SimptomUzrokIspravljeno u
Broj stranica pokazuje 1 posle što obrazac naraste na dve straniceNativni događaj stranice prosleđuje dodato/uklonjeno delta, a ne ukupan brojv3.126.1 (omotač)
Ivica polja se pomera, kucani tekst i površina pogodaka ostaju izaUčitani widget preskočio relayout posle samo-poređenjav3.126.1 (Windows V8 biblioteke)
Pregledač crta ili usmerava unos na stanje stranice pre rasporedaPage handle nije ponovo učitan posle repaginacijev3.126.2 (odloženo osvežavanje)
Klik u polje podiže Cannot open text pageIzbor teksta i URL provera na stranicama bez tekstualnog slojav3.126.2
Kucanje preko izabrane vrednosti dodaje umesto da zameniNativni XFA editor ubacuje na kursorv3.126.2
  • Postavite EnableV8Engine na True pre nego što se bilo koji dokument učita, i rukujte OnXfaRuntimeMissing-om za slučaj da je obična biblioteka učitana prva
  • Čitajte ukupan broj iz TPdf.PageCount-a ili parametra NewCount od OnXfaPageCountChanged-a; nikad sami ne sabirajte ni oduzimajte brojeve stranica
  • Držite OnXfaPageCountChanged handler lagan, jer radi unutar nativnog layout callback-a
  • Sinhronizujte indikator tekuće stranice u TPdfView.OnPageChange-u, koji opali posle što odloženo ponovno učitavanje stegne broj stranice
  • Rasporedite v3.126.1 ili novije Windows V8 DLL-ove zajedno s jedinicama; popravka relayout-a widgeta živi u nativnom kodu
  • Testirajte s obrascem koji zaista menja broj stranica i premešta uređeno polje preko preloma stranice, jer obrasci fiksnih dužina kriju svaki bag sa ove liste

Dinamički XFA pretvara broj stranica i geometriju polja u žive vrednosti, i pregledač ostaje ispravan samo ako ih uzima iz završenog rasporeda i ponovo učitava stranice u bezbednom trenutku. PDFium Component rukuje obojim unutar TPdf-a i TPdfView-a, pa domaćin samo treba da sluša. Detalji i preuzimanja su na stranici proizvoda PDFium Component for Delphi