Tekninen artikkeli

PDFium Component Dynamic XFA: sivumäärä on delta

Kun dynaaminen XFA-lomake Delphi-katselimessa lisää tai poistaa sivuja, PDFium Component ilmoittaa uuden yhteismäärän kohteen TPdf.PageCount ja TPdf.OnXfaPageCountChanged kautta versiosta v3.126.1 alkaen, koska natiivi sivutapahtuma kantaa lisätyn tai poistetun deltan eikä yhteismäärää. v3.126.1:n Windows V8 -kirjastot siirtävät myös syötealueet siirrettyjen kenttien mukana, ja v3.126.2 lataa vanhentuneet sivukahvat uudelleen, kun layout-callback on palannut. Tämän käynnistänyt bugiraportti oli kulukorvauslomake: klikkaa Lisää rivi kahdesti, lomake kasvaa kahdelle sivulle, ja sivuindikaattori ilmoittaa ylpeänä 1 / 1. Kirjoita kenttään, joka siirtyi sivulle 2, ja näppäilyt laskeutuvat jonnekin näkymättömään. Mikään näistä ei ilmestynyt kiinteäpituisten mallilomakkeiden kanssa, joita kaikki testaavat ensin, ja syyt kannattaa tietää, jos upotat lomakekatselimen

Mitä tapahtuu, kun dynaaminen XFA-lomake jakaa sivut uudelleen?

Dynaamisella XFA-lomakkeella ei ole kiinteää sivuluetteloa, joten sen sivumäärä on layoutin tuotos ja voi muuttua joka kerta, kun käyttäjä muokkaa dataa. XFA 3.3 kuvaa lomakkeen alilomakkeiden puuna; toistuvaa alilomaketta ohjaa instanceManager, ja skripti kuten _Row.addInstance() kloonaa yhden rivin lisää. Layout-prosessori virtaa sitten sisällön sivualueisiin uudelleen, mikä voi lisätä sivun, pudottaa sivun tai työntää olemassa olevia kenttiä toiselle sivulle. ISO 32000-1 §12.7.8 määrittelee vain, miten XFA-paketit kulkevat PDF:n sisällä; kaikki sen jälkeen kuuluu XFA-moottorille, joka PDFium Componentissa on PDFiumin oma XFA-layout isäntäprosessissa. Delphi-katselin käsittelee siis asiakirjaa, jonka sivumäärä, sivukoot ja widgetien paikat ovat kaikkiaan elävää tilaa. Kolme asiaa menee pieleen, kun isäntä olettaa muuta:

  • Sivumäärä, jonka isäntä välimuistittaa navigointia, vieritysalueita ja sivuspinnereitä varten, vanhenee tai pahempaa päivittyy väärällä numerolla
  • Kentät, jotka siirtyvät, näyttävät reunuksensa uudessa paikassa, kun taas editori ja hiiren osuma-alue pysyvät vanhoissa koordinaateissa
  • Katselin pitää sivukahvaa, jonka layout on korvannut, joten klikkaukset ja piirrot menevät sivulle, jota ei enää ole kyseisessä lomakkeessa

Rivimuutosten säilyttäminen tallennuksen ja uudelleenavauksen yli on erillinen ongelma omilla säännöillään; tämä artikkeli pysyy siinä, mitä tapahtuu ajonaikaisesti katselimen sisällä

Mitä PDFium-ajonaikaista dynaaminen XFA tarvitsee?

Dynaaminen XFA PDFium Componentissa vaatii natiivikirjaston V8/XFA-koontiversion, jonka valitsee globaali muuttuja EnableV8Engine PDFium-yksikössä ennen kuin ensimmäinen asiakirja latautuu. Prosessi sitoutuu yhteen DLL:ään ensimmäisellä kerralla, kun mikä tahansa TPdf lataa kirjaston, ja pelkkä PDFium-koontiversio ei voi ajaa XFA-moottoria lainkaan. Kun asiakirja avautuu, TPdf kyllä kurkistaa tiedostoon XFA-merkintöjen varalta ja vaihtaa V8-koontiversioon automaattisesti, mutta vain, jos pelkkää kirjastoa ei ole vielä ladattu kyseisessä prosessissa. Kun sitoutuminen on jo mennyt väärään suuntaan, TPdf.OnXfaRuntimeMissing laukeaa kerran, jotta isäntä voi kertoa käyttäjälle uudelleenkäynnistyksestä. Flagin asettaminen eksplisiittisesti käynnistyksessä poistaa arvailun. XFA-tapahtumat kantavan FPDF_FORMFILLINFO-callback-rakenteen on myös vastattava DLL:ää; tausta on artikkelissa FPDF_FORMFILLINFO versio 2 ja XFA-callback-ABI, ja XFA-lomakkeiden tunnistaminen ja niiden pakettien lukeminen kattaa lomaketyyppien erottelun ennen kuin avaat katselimen

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Päätä ennen kuin ensimmäinen TPdf lataa natiivikirjaston:
  // prosessi ei voi vaihtaa pdfium.dll:stä pdfium.v8.dll:ään myöhemmin
  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;

Miksi PageCount ilmoitti 1:n kaksisivuiselle lomakkeelle?

Ennen versiota v3.126.1 PDFium Component tallensi natiivin sivutapahtuman page_count-argumentin asiakirjan yhteismääränä, ja kyseinen argumentti on oikeasti uuden ja vanhan sivumäärän itseisarvoinen erotus. PDFium nostaa FFI_PageEventin, kun layout-kierros päättyy tapahtumatyypillä sivu lisätty tai sivu poistettu; sisäisesti se päivittää tallennetun sivumääränsä ensin ja välittää sitten arvon abs(new - old). Alkuperäisessä layoutissa vanha määrä on nolla, joten delta on yhtä suuri kuin yhteismäärä, ja kolmisivuinen staattinen malli ilmoittaa kolme sivua odotetusti. Juuri siksi kiinteäpituiset testilomakkeet eivät koskaan paljastaneet vikaa. Ensimmäisellä kerralla, kun dynaaminen lomake kasvaa yhdestä sivusta kahteen, delta on 1, ja kääre asetti sekä kohteen TPdf.PageCount että kohteen OnXfaPageCountChanged NewCount-parametrin arvoon 1. Rivin poistaminen kolmisivuisesta lomakkeesta tuotti samanlaisen hölynpölyn toiseen suuntaan

Deltan kasaaminen edellisen arvon päälle ei ole myöskään turvallinen korjaus. Alustuksen ja layout-callbackien järjestys tarkoittaa, ettei kääre voi aina luottaa aiempaan määräänsä perustana, joten juokseva summa voi ajautua harhaan. Versiosta v3.126.1 alkaen callback ohittaa argumentin määränä ja kutsuu funktiota FPDF_GetPageCount asiakirjalle, joka lukee yhteismäärän juuri päättyneestä layoutista. Se tyhjentää sitten välimuistitetut sivunäkymät, tallentaa kyseisen yhteismäärän XFA-sivumäärän ohituksena kohteen TPdf.PageCount taakse ja nostaa OnXfaPageCountChangedin vasta sen jälkeen. Silloin kun käsittelijäsi ajaa, NewCount ja FPdf.PageCount sopivat yhteen

PDFium Componentin dynaamisen XFA:n kaavio, jossa rivin lisääminen jakaa yksisivuisen lomakkeen kahdelle sivulle uudelleen ja FFI_PageEvent välittää arvon abs(new miinus old) deltan, joten vanha kääre ilmoitti TPdf.PageCount 1:n, kun taas v3.126.1 lukee FPDF_GetPageCountin ja ilmoittaa oikean yhteismäärän
Natiivi sivutapahtuma ilmoittaa lisätyn tai poistetun deltan, ei yhteismäärää, joten v3.126.1 ohittaa argumentin ja lukee päättyneen layoutin ennen kuin nostaa OnXfaPageCountChangedin
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount on päättyneen layoutin yhteismäärä, ei koskaan delta.
  // Tämä ajaa PDFiumin layout-callbackin sisällä: päivitä vain isännän UI-tila,
  // älä sulje asiakirjaa äläkä lataa sivuja uudelleen täältä
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Laukeaa jokaisen sivun uudelleenlatauksen jälkeen, mukaan lukien lykätty XFA-päivitys
  PageSpin.Value := PdfView1.PageNumber;
end;

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

Tapahtuma laukeaa vain Full XFA -lomakkeille, joiden layout muuttuu ajonaikaisesti. Staattiset XFA- ja AcroForm-asiakirjat eivät koskaan nosta sitä, joten katselin, joka käsittelee molemmat, voi jättää saman käsittelijän asetetuksi. Sen jättäminen asettamatta on myös turvallista; ohitus kohteen TPdf.PageCount takana sovelletaan joka tapauksessa, ja tapahtuma on olemassa, jotta isäntä voi päivittää sen, minkä on välimuistittanut

Miksi syötekenttä pysyy vanhalla sivulla, kun kenttä siirtyy?

Reunus siirtyi eikä editori, koska natiivi XFA-notifioija vertasi suorakulmiota itseensä. Kun layout muuttaa jo ladatun widgetin geometriaa, PDFiumin pitäisi huomata uusi suorakulmio ja kutsua funktiota PerformLayout widgetille, mikä sijoittaa tekstimuokkaimen ja sen osuma-alueen uudelleen. Tarkistus vertasi arvoa GetWidgetRect() arvoon RecacheWidgetRect(). Molemmat funktiot palauttavat const-viittauksen samaan jäseneen, ja uudelleenvälimuistitus ylikirjoittaa kyseisen jäsenen paikallaan, joten vertailu näki aina kaksi identtistä arvoa, ja ladatut widgetit ohittivat uudelleenasettelunsa

Oire nousi pintaan, kun testi muutti alilomakkeen korkeutta niin, että olemassa olevat kentät ylittivät seuraavalle sivulle. Molemmissa V8-arkkitehtuureissa kentän reunus piirrettiin uuteen paikkaansa, kun taas kirjoitettu teksti ja hiiren osuma-alue pysyivät edellisessä Y-koordinaatissa. Eksplisiittinen uudelleenasettelu ei korjannut sitä, eikä sivun uudelleenlatauskaan, koska widget yhä uskoi geometriansa olevan ajantasalla. v3.126.1:n mukana toimitetut Windows V8 -kirjastot kopioivat vanhan suorakulmion arvona ennen uudelleenvälimuistitusta ja vertaavat kyseistä kopiota, joten siirtyneet widgetit asettuvat uudelleen ja muokattu arvo ilmestyy täsmälleen sinne, missä reunus on. Kyseessä on natiivi korjaus: se kulkee DLL:ien mukana, joten Pascal-yksiköiden päivittäminen säilyttäen vanhempi pdfium.v8.dll jättää väärin sijoitetut osuma-alueet paikoilleen. Regression tarkistus, joka ajoi sen, muokkaa säilyvän rivin ei-oletusarvoon ensin ja vaatii sitten kyseisen arvon kentän uudessa paikassa, koska oletusarvoilla rakennettu rivi näyttäisi muuten läpäisyltä

PDFium Componentin widgetin uudelleenasettelun kaavio, joka asettaa vastakkain vanhan itsevertailun, jossa GetWidgetRect ja RecacheWidgetRect palauttivat yhden jaetun jäsenen, joten siirtyneet widgetit ohittivat PerformLayoutin, ja v3.126.1:n Windows V8:n arvon mukaisen kopion tarkistuksen, joka sijoittaa editorin ja hiiren osuma-alueen piirretyn reunuksen kohdalle
Suorakulmion vertaaminen itseensä ei koskaan epäonnistu, joten reunus siirtyi, kun taas kirjoitettu teksti ja klikkaukset jäivät jälkeen, kunnes tarkistus tallensi kopion arvona ensin

Miten TPdfView lataa sivuja uudelleen vetämättä kahvaa PDFiumin alta?

Versiosta v3.126.2 alkaen TPdfView lykkää XFA-layout-muutosta seuraavaa sivun uudelleenlatausta, kunnes natiivi kutsupino on purkautunut. Sivutapahtuma laukeaa yleensä silloin, kun PDFium käsittelee yhä syötettä: käyttäjä klikkasi Lisää rivi -painiketta, klikkaus ajoi skriptin, skripti muutti instanssimäärää, ja layout päättyi saman natiivikutsun sisällä. Sivukahvan sulkeminen ja uudelleenavaus tuolla hetkellä vapauttaisi objektin, jota kutsuja käyttää yhä. Ennen versiota v3.126.2 katselin vain mitätöi itsensä, joten näytetty sivukahva saattoi osoittaa yhä layoutia edeltävään tilaan, ja jos käyttäjä oli ollut viimeisellä sivulla, kun se katosi, valittu sivunumero oli alueen ulkopuolella

Lykätty päivitys toimii muutamassa pienessä vaiheessa, ja ne selittävät käytöksen, jonka näet isännältä:

  1. Sivutapahtuman callback merkitsee näkymän saaneen odottavan XFA-layout-päivityksen ja lähettää yksityisen ikkunaviestin; toistetut tapahtumat ennen viestin saapumista yhdistyvät yhdeksi päivitykseksi
  2. Näkymä ilman ikkunakahvaa yhä pitää odottavan flagin ja lähettää viestin funktiosta CreateWnd, kun taas asiakirjojen vaihtaminen, näkymän deaktivointi tai tuhoaminen tyhjentää flagin
  3. Kun viesti saapuu, näkymä tyhjentää tekstinvalinnan, hakukorostuksen ja kohdistetun kentän indeksin, koska kaikki kolme viittasivat vanhaan layoutiin
  4. Valittu sivu rajataan uuteen PageCountiin; muuttunut sivunumero kulkee tavallisen sivunvaihdon kautta, muuten nykyinen sivu ladataan uudelleen, ja sovitystila sovelletaan uudelleen
  5. Jos layout jättää ei yhtään sivua, näkymä purkaa vanhan sivukahvansa piirtämisen sijaan sivua, jota ei enää ole olemassa
PDFium Componentin TPdfView:n lykätyn XFA-päivityksen kaavio, jossa sivutapahtuma natiivin layout-kutsupinon sisällä vain merkitsee odottavan päivityksen ja lähettää ikkunaviestin, joka myöhemmin tyhjentää vanhentuneen valintatilan, rajaa sivun uuteen PageCountiin ja lataa sivukahvan uudelleen tai purkaa sen
Uudelleenlataus odottaa, kunnes natiivi kutsupino purkautuu: lähetetty viesti yhdistää toistetut tapahtumat, sitten näkymä rajaa sivun, lataa sen uudelleen ja nostaa OnPageChangen

Sama rajoite koskee omaa koodiasi. OnXfaPageCountChanged ajaa kyseisen natiivin layout-callbackin sisällä, joten kohtele sitä ilmoituksena: päivitä selitteet, spinner-alueet ja työkalupalkin tila siellä, ja jonoa kaikki raskaampi, kuten asiakirjan sulkeminen tai toisen avaaminen, lähetetyllä viestillä, jotta se ajaa callbackin paluun jälkeen. TPdfView.OnPageChange kertoo sitten, milloin näkymä on oikeasti ladannut sivun uudelleen, ja funktion PdfView1.PageNumber lukeminen tuolla hetkellä antaa rajatun arvon. Sarkainnäppäimen kulku ja FormType-tarkistukset, jotka lomakekatselin ajaa avatessaan, on käsitelty artikkelissa PDF-lomakekenttien navigointi PDFium Componentilla

Miksi Full XFA -kenttään klikkaaminen nostaa poikkeuksen "Cannot open text page"?

Full XFA -sivuilla ei ole PDF-tekstisivua, ja ennen versiota v3.126.2 katselimen oletuksena oleva tekstinvalinta ja linkkien tunnistus yrittivät silti ladata sellaisen. Kohteen TPdfView.AllowUserTextSelection ollessa oletusarvossaan True, osoittaminen kysyi tekstikerrokselta merkkiä hiiren alla, ja hiiren ylöspäästöklikkaus ajoi automaattisen URL-tutkan sivun tekstin yli. Full XFA -sivulla tekstisivua ei voi avata, joten tavallinen klikkaus kenttään saattoi päätyä poikkeukseen Cannot open text page. Versiosta v3.126.2 alkaen molemmat sisäiset polut palauttavat ei tulosta, kun TPdf.FormType on ftXfaFull ja XFA-ajonaikainen on saatavilla, joten oletusasetukset toimivat ja kenttäsyöte pysyy käytettävissä

Kohteen AllowUserTextSelection pois kytkeminen Full XFA -asiakirjoilta on yhä järkevä UI-valinta, koska sivutekstiä ei ole valittavana ja vetokäyntien ei pitäisi käynnistää valintatilaa. Se ei kuitenkaan korvaa päivitystä: aiemmissa versioissa klikkauksen URL-tutka ei riippunut kyseisestä ominaisuudesta, joten katselin saattoi osua samaan poikkeukseen valinnan ollessa pois käytöstä

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType lukee avoimen asiakirjan, joten kutsu tätä kohteen FPdf.Active := True jälkeen
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // PDF-tekstikerrosta ei ole olemassa Full XFA -sivuilla; kentät pysyvät muokattavina
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Kirjoittaminen tarvitsi oman korjauksensa versiossa v3.126.2. Natiivi XFA-tekstimuokkain ei korvaa valintaa, kun se vastaanottaa merkin: FORM_OnChar lisää tekstikohdistimen kohdalle, ja Backspace poistaa yhden merkin, joten arvon valitseminen ja sen ylikirjoittaminen tuotti vanhan ja uuden tekstin rinnakkain. PDFium Component muistaa nykyään, että klikkaus osui XFA-tekstikenttään, ja ohjaa kirjoitetut merkit, Backspacen ja Delen funktiolla FORM_ReplaceSelection aina, kun valinta on olemassa ja asiakirja myöntää lomakkeiden täyttö- tai muokkausluvan. Sitä, voiko vain luku -XFA-kenttää muuttaa, päättää yhä natiivi muokkain, joten lomakkeessa vain luku -merkitty kenttä säilyttää arvonsa myös asiakirjassa, joka muuten sallii täytön. Kohteen TPdfView.AllowFormEvents asettaminen arvoon False pysäyttää myös tämän näppäimistöohjauksen, mikä pitää vain luku -katselimen vain lukuna

Pikaopas: dynaaminen XFA Delphi-katselimessa

OireSyyKorjattu versiossa
Sivumäärä näyttää 1:n, kun lomake kasvaa kahdelle sivulleNatiivi sivutapahtuma välittää lisätyn tai poistetun deltan, ei yhteismäärääv3.126.1 (kääre)
Kentän reunus siirtyy, kirjoitettu teksti ja osuma-alue jäävät jälkeenLadattu widget ohitti uudelleenasettelun itsevertailun jälkeenv3.126.1 (Windows V8 -kirjastot)
Katselin piirtää tai ohjaa syötteen layoutia edeltävään sivutilaanSivukahvaa ei ladattu uudelleen uudelleensivutuksen jälkeenv3.126.2 (lykätty päivitys)
Kenttään klikkaaminen nostaa Cannot open text page -poikkeuksenTekstinvalinta ja URL-tutka sivuilla ilman tekstikerrostav3.126.2
Valitun arvon ylikirjoittaminen liittää tekstin korvaamisen sijaanNatiivi XFA-muokkain lisää tekstikohdistimen kohdallev3.126.2
  • Aseta EnableV8Engine arvoon True ennen kuin mikään asiakirja latautuu, ja käsittele OnXfaRuntimeMissing tapauksessa, jossa pelkkä kirjasto ladattiin ensin
  • Lue yhteismäärä kohteesta TPdf.PageCount tai kohteen OnXfaPageCountChanged NewCount-parametrista; älä koskaan laske tai vähennä sivumääriä itse
  • Pidä kohteen OnXfaPageCountChanged käsittelijä kevyenä, koska se ajaa natiivin layout-callbackin sisällä
  • Synkronoi nykyisen sivun indikaattori kohteessa TPdfView.OnPageChange, joka laukeaa sen jälkeen, kun lykätty uudelleenlataus on rajannut sivunumeron
  • Toimita v3.126.1:n tai uudemmat Windows V8 -DLL:t yksiköiden mukana; widgetin uudelleenasettelun korjaus asuu natiivikoodissa
  • Testaa lomakkeella, joka oikeasti muuttaa sivumääränsä ja siirtää muokatun kentän sivunvaihdon yli, koska kiinteäpituiset mallit piilottavat jokaisen tämän luettelon vikan

Dynaaminen XFA tekee sivumäärästä ja kenttien geometriasta eläviä arvoja, ja katselin pysyy oikeana vain, kun se ottaa ne päättyneestä layoutista ja lataa sivuja uudelleen turvallisella hetkellä. PDFium Component hoitaa molemmat kohteiden TPdf ja TPdfView sisällä, joten isännän tarvitsee vain kuunnella. Yksityiskohdat ja lataukset ovat PDFium Component for Delphi -tuotesivulla