Tehnični članak

PDFium Component dinamični XFA: števec strani je delta

Ko dinamični obrazec XFA v pregledovalniku v Delphiju doda ali odstrani strani, komponenta PDFium Component od v3.126.1 poroča novo skupno vsoto prek TPdf.PageCount in TPdf.OnXfaPageCountChanged, ker izvorni dogodek strani nosi delto dodanih/odstranjenih in ne vsote. Knjižnici Windows V8 v v3.126.1 prav tako premakneta območja zadetkov vnosa s prestavljenimi polji, v3.126.2 pa po vrnitvi povratnega klica postavitve znova naloži zastarele ročaje strani. Hroščje poročilo, ki je to sprožilo, je bil obrazec za povračilo stroškov: kliknite Dodaj vrstico dvakrat, obrazec zraste na dve strani, kazalnik strani pa ponosno pokaže 1 od 1. Vtipkajte v polje, ki se je preselilo na stran 2, in pritiski tipk pristanejo nekje nevidno. Nič od tega se ni pokazalo na obrazcih s fiksno dolžino, s katerimi vsi najprej testirajo, razlogi pa so vredni poznavanja, če vgrajujete pregledovalnik obrazcev

Kaj se zgodi, ko se dinamični obrazec XFA znova paginira?

Dinamični obrazec XFA nima fiksne tabele strani, zato je njegov števec strani izhod postavitve in se lahko spremeni vsakič, ko uporabnik uredi podatke. XFA 3.3 opisuje obrazec kot drevo podform; ponavljajočo se podformo nadzoruje instanceManager, skript, kot je _Row.addInstance(), pa klonira še eno vrstico. Procesor postavitve nato vsebino spet prelije v območja strani, kar lahko doda stran, odvrgne stran ali potisne obstoječa polja na drugo stran. ISO 32000-1 §12.7.8 definira samo, kako se paketi XFA prevažajo znotraj PDF; vse, kar se zgodi kasneje, pripada pogonu XFA, ki je v komponenti PDFium Component lastna postavitev XFA PDFiuma, tekmoča v gostiteljskem procesu. Pregledovalnik v Delphiju se torej spopada z dokumentom, katerega števec strani, velikosti strani in položaji widgetov so vsi živo stanje. Tri stvari gredo narobe, kadar gostitelj domneva drugače:

  • Števec strani, ki ga gostitelj spravi v predpomnilnik za navigacijo, razpone premikanja in vrtiljake strani, zastara, ali še huje, dobi posodobljen z napačnim številom
  • Polja, ki se preselijo, pokažejo svoj rob na novem položaju, medtem ko urejevalnik in območje zadetkov miške ostanejeta na starih koordinatah
  • Pregledovalnik obdrži ročaj strani, ki ga je postavitev zamenjala, zato kliki in risanja gredo na stran, ki v tem obrazcu ne obstaja več

Ohranjanje urejanj vrstic čez shranjevanje in ponovno odpiranje je ločena težava s svojimi pravili; ta članek ostane pri tem, kaj se zgodi med tekom znotraj pregledovalnika

Kateri pogon PDFium potrebuje dinamični XFA?

Dinamični XFA v komponenti PDFium Component potrebuje gradnjo V8/XFA izvorne knjižnice, izbrano z globalno spremenljivko EnableV8Engine v enoti PDFium, preden se naloži prvi dokument. Proces se zaveže eni DLL takoj, ko kateri koli TPdf naloži knjižnico, navadna gradnja PDFium pa pogona XFA sploh ne zna pognati. Ko se dokument odpre, TPdf sicer povoha datoteko po označevalcih XFA in samodejno preklopi na gradnjo V8, a samo, če v tem procesu še ni bila naložena navadna knjižnica. Ko se zaveza že iztekla v napačno smer, se enkrat sproži TPdf.OnXfaRuntimeMissing, da lahko gostitelj uporabniku pove, naj ponovno zažene aplikacijo. Zastavico nastavite izrecno ob zagonu in ugibanja ni. Struktura povratnih klicev FPDF_FORMFILLINFO, ki nosi dogodke XFA, se mora prav tako ujemati z DLL; ozadje je v FPDF_FORMFILLINFO različici 2 in ABI povratnih klicev XFA, zaznavanje obrazcev XFA in branje njihovih paketov pa pokrije ločevanje vrst obrazcev, preden odprete pregledovalnik

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Odločite se, preden prvi TPdf naloži izvorno knjižnico:
  // proces ne more kasneje preklopiti 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;

Zakaj je PageCount poročal 1 za dvostranski obrazec?

Pred v3.126.1 je komponenta PDFium Component shranila argument page_count izvornega dogodka strani kot skupno vsoto dokumenta, ta argument pa je v resnici absolutna razlika med novim in starim števcem strani. PDFium sproži FFI_PageEvent, ko se prehod postavitve zaključi z vrsto dogodka stran dodana ali stran odstranjena; interno najprej posodobi svoj shranjen števec strani in nato podaja abs(new - old). Ob začetni postavitvi je stari števec nič, zato je delta enaka vsoti in tristranski statični vzorec poroča tri strani, kot pričakovano. Točno zato obrazci za testiranje s fiksno dolžino hrošča nikoli niso izpostavili. Prvič, ko dinamični obrazec zraste z ene strani na dve, je delta 1, ovijalnik pa je nastavil tako TPdf.PageCount kot parameter NewCount od OnXfaPageCountChanged na 1. Odstranitev vrstice s tristranskega obrazca je v drugo smer izdelala enako vrsto nesmisla

Zanašanje delte na prejšnjo vrednost ni tudi varen popravek. Vrstni red inicializacije in povratnih klicev postavitve pomeni, da ovijalnik svojega prejšnjega števca ne more vedno zaupati kot izhodišče, zato se tekoča vsota lahko zdrsi. Od v3.126.1 povratni klic ignorira argument kot števec in pokliče FPDF_GetPageCount nad dokumentom, ki prebere vsoto iz postavitve, ki se je pravkar zaključila. Nato počisti predpomnjene prizore strani, to vsoto shrani kot preglasitev števca strani XFA za TPdf.PageCount in šele potem sproži OnXfaPageCountChanged. Ko se vaš ročaj izvede, sta NewCount in FPdf.PageCount usklajena

PDFium Component diagram dinamičnega XFA, kjer dodajanje vrstice ponovno paginira enostranski obrazec na dve strani in FFI_PageEvent podaja abs(novo minus staro) kot delto, tako da je stari ovijalnik poročal TPdf.PageCount 1, v3.126.1 pa prebere FPDF_GetPageCount in poroča pravo vsoto
Izvorni dogodek strani poroča delto dodano-ali-odstranjeno, ne vsote, zato v3.126.1 ignorira argument in prebere zaključeno postavitev, preden sproži OnXfaPageCountChanged
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount je skupna vsota zaključene postavitve, nikoli delta.
  // To teče znotraj povratnega klica postavitve PDFium: posodabljajte samo stanje
  // gostiteljskega vmesnika; od tu ne zapirajte dokumenta in ne nalagajte strani
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Sproži se po vsakem ponovnem nalaganju strani, vključno z odloženim osveževanjem XFA
  PageSpin.Value := PdfView1.PageNumber;
end;

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

Dogodek se sproži samo za obrazce Full XFA, katerih postavitev se med tekom spremeni. Statični XFA in dokumenti AcroForm ga nikoli ne sprožijo, zato lahko pregledovalnik, ki opravi z obema, pusti dodeljen isti ročaj. Varno je tudi puščanje nedodeljeno; preglasitev za TPdf.PageCount se uporabi vseeno, dogodek pa obstaja, da lahko gostitelj osveži vse, kar je spravil v predpomnilnik

Zakaj vnosno polje ostane na stari strani, ko se polje preseli?

Rob se je premaknil, urejevalnik pa ne, ker je izvorni obveščevalec XFA primerjal pravokotnik s samim seboj. Ko postavitev spremeni geometrijo že naloženega widgeta, bi moral PDFium opaziti nov pravokotnik in na widgetu poklicati PerformLayout, ki prestavi urejevalnik besedila in njegovo območje zadetkov. Preizkus je primerjal GetWidgetRect() s RecacheWidgetRect(). Obe funkciji vrneta konstantno referenco na istega člana, ponovno zapisovanje v predpomnilnik pa tega člana prepiše na mestu, zato je primerjava vedno videla dve enaki vrednosti in naloženi widgeti so izpustili svojo ponovno postavitev

Simptom je prišel na dan, ko je preskus spremenil višino podformel tako, da so obstoječa polja prečkala na naslednjo stran. Na obeh arhitekturah V8 je bil rob polja narisan na novem položaju, medtem ko sta vtipkano besedilo in območje zadetkov miške ostala na prejšnji koordinati Y. Izrecna ponovna postavitev tega ni popravila, niti ponovno nalaganje strani ne, ker je widget še vedno verjel, da je njegova geometrija aktualna. Knjižnice Windows V8, dostavljene z v3.126.1, stari pravokotnik kopirajo po vrednosti, preden ga znova zapišejo v predpomnilnik, in primerjajo to kopijo, zato se preseljeni widgeti znova postavijo in urejena vrednost se pokaže točno tam, kjer je rob. To je izvorni popravek: potuje skupaj z DLL-i, torej posodobitev Pascalovih enot ob hranjenju starejšega pdfium.v8.dll pusti premaknjena območja zadetkov na mestu. Regresijski preizkus, ki ga je pognal, najprej uredi preživelo vrstico na neprivzeto vrednost in nato zahteva to vrednost na novem položaju polja, ker bi se vrstica, znova zgrajena s privzetimi vrednostmi, sicer izdala za uspeh

PDFium Component diagram ponovne postavitve widgeta, ki postavlja nasproti staro samo-primerjavo, kjer sta GetWidgetRect in RecacheWidgetRect vračala enega skupnega člana, zato so preseljeni widgeti izpustili PerformLayout, in preizkus kopiranja-po-vrednosti Windows V8 v3.126.1, ki prestavi urejevalnik in območje zadetkov miške na znova narisan rob
Primerjava pravokotnika s samim seboj nikoli ne spodleti, zato se je rob premaknil, medtem ko sta vtipkano besedilo in kliki zaostajala, dokler preizkus ni najprej shranil kopije po vrednosti

Kako TPdfView znova naloži strani, ne da bi PDFiumu iztrgal ročaja pod nogami?

Od v3.126.2 TPdfView ponovno nalaganje strani, ki sledi spremembi postavitve XFA, odloži, dokler se izvorni sklic klicev ne razvije. Dogodek strani se običajno sproži, medtem ko PDFium še obdeluje vnos: uporabnik je kliknil gumb Dodaj vrstico, klik je pognal skript, skript je spremenil števec instanc, postavitev pa se je zaključila znotraj istega izvornega klica. Zapiranje in ponovno odpiranje ročaja strani v tem trenutku bi sprostilo objekt, ki ga klicatelj še uporablja. Pred v3.126.2 se je pregledovalnik samo razveljavil, tako da je lahko prikazani ročaj strani še kazal na stanje pred postavitvijo, če je bil uporabnik ob izginotju na zadnji strani, pa je bila izbrana številka strani izven razpona

Odloženo osveževanje dela v nekaj majhnih korakih, ti pa razložijo obnašanje, ki ga vidite od gostitelja:

  1. Povratni klic dogodka strani označi pogled kot imetnika obvisjajočega osveževanja postavitve XFA in pošlje zasebno okensko sporočilo; ponovljeni dogodki pred prispelim sporočilom se združijo v eno osvežitev
  2. Pogled brez okenskega ročaja še obdrži zastavico obvisjanja in sporočilo pošlje iz CreateWnd, sprememba dokumenta, deaktivacija pogleda ali njegovo uničenje pa zastavico počisti
  3. Ko sporočilo prispe, pogled počisti izbor besedila, poudarek iskanja in indeks polja v žarišču, ker so vsi trije kazali na staro postavitev
  4. Izbrana stran se prižge na nov PageCount; spremenjena številka strani gre skozi običajni preklop strani, sicer pa se trenutna stran znova naloži, način prilagajanja pa se znova uporabi
  5. Če postavitev pusti sploh brez strani, pogled razloži svoj stari ročaj strani, namesto da bi risal stran, ki ne obstaja več
PDFium Component diagram odloženega osveževanja XFA TPdfView, kjer dogodek strani znotraj izvornega sklada klicev postavitve samo označi obvisjajočo osvežitev in pošlje okensko sporočilo, ki pozneje počisti zastarelo stanje izbora, prižge stran na nov PageCount ter znova naloži ali razloži ročaj strani
Ponovno nalaganje počaka, da se izvorni sklic klicev razvije: poslano sporočilo združi ponovljene dogodke, nato pogled prižge stran, jo znova naloži in sproži OnPageChange

Ista omejitev velja za vašo lastno kodo. OnXfaPageCountChanged teče znotraj tega izvornega povratnega klica postavitve, zato ga obravnavajte kot obvestilo: tam posodabljajte nalepke, razpone vrtiljakov in stanje orodne vrstice, vse težje — kot zapiranje dokumenta ali odpiranje drugega — pa vrstite v poslano sporočilo, da steče po vrnitvi povratnega klica. TPdfView.OnPageChange vam potem pove, kdaj je pogled stran dejansko znova naložil, branje PdfView1.PageNumber v tistem trenutku pa vam da prižgano vrednost. Prehajanje s tipko Tab in preizkusi FormType, ki jih pregledovalnik obrazcev požene ob odpiranju, so pokriti v navigaciji po poljih obrazcev PDF s komponento PDFium Component

Zakaj klik na polje Full XFA sproži »Cannot open text page«?

Strani Full XFA nimajo strani besedila PDF, pred v3.126.2 pa je privzeti izbor besedila in zaznavanje povezav pregledovalnika vseeno poskusil naložiti eno. S TPdfView.AllowUserTextSelection na privzeti True je lebdenje vprašalo plast besedila po znaku pod miško, klik ob spustitvi miške pa je pognal samodejno sondiranje URL-jev čez besedilo strani. Na strani Full XFA strani besedila ni mogoče odpreti, zato se je navaden klik v polje lahko končal z izjemo Cannot open text page. Od v3.126.2 obe notranji poti vrneta brez rezultata, kadar je TPdf.FormType ftXfaFull in je pogon XFA na voljo, tako da privzete nastavitve delujejo in vnos v polja ostane na voljo

Izklop AllowUserTextSelection za dokumente Full XFA je še vedno razumna izbira vmesnika, ker besedila strani ni mogoče izbrati in gesti vleka ne bi smele začeti načina izbora. Nadomestilo za nadgradnjo pa ni: na zgodnejših različicah URL-jeva sonda ob kliku te lastnosti ni upoštevala, zato je lahko pregledovalnik naletel na isto izjemo tudi z izklopljenim izborom

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType bere odprti dokument, pokličite torej po FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // Na straneh Full XFA ni plasti PDF besedila; polja ostanejo urejanju dostopna
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Vtipkanje je potrebovalo svoj popravek v v3.126.2. Izvorni urejevalnik besedila XFA izbire ne zamenja, ko dobi znak: FORM_OnChar vstavi na vstavljalnik, Backspace pa izbriše en znak, zato je izbira vrednosti in vtipkanje čez njo izdelalo staro in novo besedilo drug ob drugem. Komponenta PDFium Component si zdaj zapomni, da je klik pristal na besedilnem polju XFA, vtipkane znake, Backspace in Delete pa usmerja skozi FORM_ReplaceSelection, kadar koli izbira obstaja in dokument odobri dovoljenje za izpolnjevanje ali urejanje. Ali se sme spremeniti polje XFA, ki je samo za branje, še vedno odloča izvorni urejevalnik, zato polje, označeno samo za branje, obdrži svojo vrednost tudi v dokumentu, ki sicer dovoljuje izpolnjevanje. Nastavitev TPdfView.AllowFormEvents na False prav tako ustavi to usmerjanje tipkovnice, kar pregledovalnik samo za branje ohrani samo-za-branje

Hiter pregled: dinamični XFA v pregledovalniku Delphi

SimptomVzrokPopravljeno v
Števec strani pokaže 1, potem ko obrazec zraste na dve straniIzvorni dogodek strani podaja delto dodanih/odstranjenih, ne vsotev3.126.1 (ovijalnik)
Rob polja se premakne, vtipkano besedilo in območje zadetkov zaostanetaNaloženi widget je izpustil ponovno postavitev po samo-primerjaviv3.126.1 (knjižnice Windows V8)
Pregledovalnik riše ali usmerja vnos v stanje strani pred postavitvijoRočaj strani po ponovni paginaciji ni bil znova naloženv3.126.2 (odloženo osveževanje)
Klik v polje sproži Cannot open text pageIzbor besedila in URL sonda na straneh brez plasti besedilav3.126.2
Vtipkanje čez izbrano vrednost pripne namesto da bi zamenjaloIzvorni urejevalnik XFA vstavi na vstavljalnikv3.126.2
  • Nastavite EnableV8Engine na True, preden se naloži kateri koli dokument, in opravite se OnXfaRuntimeMissing za primer, ko je bila najprej naložena navadna knjižnica
  • Vsoto berite iz TPdf.PageCount ali parametra NewCount od OnXfaPageCountChanged; števcev strani nikoli ne seštevajte in ne odštevajte sami
  • Ročaj OnXfaPageCountChanged naj ostane lahek, ker teče znotraj izvornega povratnega klica postavitve
  • Trenutni kazalnik strani uskladite v TPdfView.OnPageChange, ki se sproži, potem ko odloženo ponovno nalaganje prižge številko strani
  • Razporedite DLL-e Windows V8 v3.126.1 ali novejše skupaj z enotami; popravek ponovne postavitve widgeta živi v izvorni kodi
  • Testirajte z obrazcem, ki dejansko spremeni svoj števec strani in preseli urejeno polje čez prelom strani, ker vzorci s fiksno dolžino skrijejo vsak hrošč na tem seznamu

Dinamični XFA spremeni števec strani in geometrijo polj v žive vrednosti, pregledovalnik pa ostane pravilen samo, kadar ju vzame iz zaključene postavitve in strani znova naloži na varnem trenutku. Komponenta PDFium Component opravi oboje znotraj TPdf in TPdfView, tako da gostitelj samo posluša. Podrobnosti in prenosi so na strani izdelka PDFium Component za Delphi