Techninis straipsnis

PDFium Component Dynamic XFA: puslapių skaičius – tai delta

Kai dinaminė XFA forma Delphi peržiūrovoje prideda arba pašalina puslapius, PDFium Component nuo v3.126.1 naująją sumą praneša per TPdf.PageCount ir TPdf.OnXfaPageCountChanged, nes savoji puslapių įvykio žinutė neša pridėta/pašalinta deltą, o ne sumą. v3.126.1 Windows V8 bibliotekos taip pat perkelia įvesties sritis kartu su perkeltais laukais, o v3.126.2 po išdėstymo atgalinio kvietimo grąžos perskaito pasenusius puslapių rankenėles. Už tai stovėjusi klaidos ataskaita – išlaidų deklaracijos forma: dukart spauskite Pridėti eilutę, forma paauga iki dviejų puslapių, o puslapių indikatorius didingai rodo 1 iš 1. Rašykite lauke, persikėlusiame į 2 puslapį, ir klavišų paspaudimai nusileidžia kažkur nematomoje vietoje. Nė vienas iš šių dalykų nepasirodydavo ant fiksuoto ilgio pavyzdinių formų, kuriomis visi pirmiausia išbando, o priežastys, kodėl taip, verta žinoti, jei įterpiate formų peržiūrovą

Kas nutinka, kai dinaminė XFA forma persiskirsto į puslapius?

Dinaminė XFA forma neturi fiksuoto puslapių sąrašo, tad jos puslapių skaičius yra išdėstymo išvestis ir gali keistis kas kartą, kai naudotojas redaguoja duomenis. XFA 3.3 formą aprašo kaip poformių medį; kartojantįjį poformį valdo instanceManager, o toks scenarijus kaip _Row.addInstance() paklonuoja dar vieną eilutę. Išdėstymo procesorius tada turinį vėl supila į puslapių sritis, o tai gali pridėti puslapį, nuimti puslapį arba perkelti esamus laukus ant kito puslapio. ISO 32000-1 §12.7.8 apibrėžia tik tai, kaip XFA paketai važiuoja PDF viduje; visa, kas toliau, priklauso XFA varikliui, kuris PDFium Component atveju yra pačio PDFium XFA išdėstymas, sukantis hosto procese. Delphi peržiūrova todėl turi reikalą su dokumentu, kurio puslapių skaičius, puslapių dydžiai ir valdiklių pozicijos – visos gyvos būsenos. Trys dalykai suyra, kai hostas mano kitaip:

  • Puslapių skaičius, kurį hostas sukaupia navigacijai, slinkimo ruožams ir puslapių sukams, sensta, ar dar geriau – atsinaujina neteisingu skaičiumi
  • Persikeliantys laukai rodo savo rėmą naujoje pozicijoje, kol redaktorius ir pelės sritis lieka senosiose koordinatėse
  • Peržiūrova laiko puslapio rankenėlę, kurią išdėstymas pakeitė, tad paspaudimai ir piešimai keliauja į puslapį, kurio toje formoje nebėra

Eilučių redagavimų išlikimas per išsaugojimą ir pakartotinį atvėrimą – atskira problema su savais dėsniais; šis straipsnis lieka prie to, kas vyksta vykdymo metu peržiūrovos viduje

Kurio PDFium vykdymo aplinkos reikia dinaminiam XFA?

Dinaminis XFA PDFium Component reikalauja savosios bibliotekos V8/XFA varianto, renkamo globalios kintamojo EnableV8Engine iš PDFium modulio prieš pirmąjį dokumentą. Procesas prisiriša prie vienos DLL pirmąkart, kai bet koks TPdf pakrauna biblioteką, o paprastas PDFium variantas XFA variklio apskritai nepaleidžia. Atverdamas dokumentą, TPdf į failą žvilgteli ar nėra XFA ženklų ir pereina į V8 variantą automatiškai, bet tik jei paprasta biblioteka tame procese dar nepakrauta. Kai ryšys jau įėjęs netinkama kryptimi, TPdf.OnXfaRuntimeMissing suveikia kartą, kad hostas galėtų paprašyti naudotojo perkrauti. Vėliavėlę nustačius aiškiai starto metu, spėlionės nelieka. FPDF_FORMFILLINFO atgalinių kvietimų struktūra, nešanti XFA įvykius, irgi turi sutapti su DLL; fonas – FPDF_FORMFILLINFO versija 2 ir XFA atgalinių kvietimų ABI, o XFA formų aptikimas ir jų paketų skaitymas dengia formų tipų atskyrimą dar prieš atveriant peržiūrovą

uses
  PDFium;

procedure TClaimForm.FormCreate(Sender: TObject);
begin
  // Nuspręskite prieš pirmajam TPdf pakraunant savąją biblioteką:
  // procesas negali vėliau pereiti nuo pdfium.dll į 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;

Kodėl PageCount dviejų puslapių formai pranešė 1?

Iki v3.126.1 PDFium Component savosios puslapių įvykio žinutės argumentą page_count laikė dokumento suma, o tas argumentas iš tikrųjų yra absoliutus naujo ir seno puslapių skaičių skirtumas. PDFium kelia FFI_PageEvent po išdėstymo praėjimo su puslapio pridėjimo ar pašalinimo įvykio tipu; viduje jis pirmiau atnaujina savą saugomą puslapių skaičių, o tada perduoda abs(new - old). Pradiniame išdėstyme senasis skaičius nulinis, tad delta lygi sumai, ir trijų puslapių statinis pavyzdys praneša tris puslapius, kaip ir tikėtasi. Būtent todėl fiksuoto ilgio bandomosios formos niekada neatskleidė klaidos. Pirmąkart, kai dinaminė forma paauga nuo vieno puslapio iki dviejų, delta yra 1, ir aplinkas nustatė ir TPdf.PageCount, ir OnXfaPageCountChanged parametrą NewCount į 1. Eilutės pašalinimas iš trijų puslapių formos duodavo tos pačios rūšies nesąmonę kitoje kryptimi

Kaupti deltos ant ankstesnės reikšmės – irgi nesaugus sutvarkymas. Inicijavimo ir išdėstymo atgalinių kvietimų tvarka reiškia, kad aplinkas ne visada gali pasitikėti ankstesniu skaičiumi kaip atskaita, tad besikaupianti suma gali nuklysti. Nuo v3.126.1 atgalinis kvietimas argumento kaip skaičiaus nebeklausia ir dokumentui kviečia FPDF_GetPageCount, kuris sumą skaito iš ką tik baigto išdėstymo. Tada jis išvalo sukaupptas puslapių scenas, tą sumą užrašo kaip XFA puslapių skaičiaus persižengėją už TPdf.PageCount ir tik po to kelia OnXfaPageCountChanged. Kai jūsų doroklė jau lekia, NewCount ir FPdf.PageCount sutampa

PDFium Component dinaminio XFA diagrama, kur eilutės pridėjimas vieno puslapio formą persiskirsto į du puslapius, o FFI_PageEvent perduoda abs(new minus old) kaip deltą, tad senasis aplinkas pranešė TPdf.PageCount 1, kai v3.126.1 skaito FPDF_GetPageCount ir praneša teisingą sumą
Savoji puslapių įvykio žinutė praneša pridėtos ar pašalintos deltą, o ne sumą, tad v3.126.1 argumento neklausia ir perskaito baigtąjį išdėstymą, dar prieš keldamas OnXfaPageCountChanged
procedure TClaimForm.PdfXfaPageCountChanged(Sender: TObject; NewCount: Integer);
begin
  // v3.126.1+: NewCount – baigto išdėstymo suma, niekada ne delta.
  // Tai lekia PDFium išdėstymo atgalinio kvietimo viduje: atnaujinkite tik
  // hosto UI būseną, dokumento neišverskite ir puslapių iš čia neperskaitinėkite
  UpdatePageRange(NewCount);
end;

procedure TClaimForm.PdfViewPageChange(Sender: TObject);
begin
  // Suveikia po kiekvieno puslapių perskaitymo, įskaitant atidėtąjį XFA atnaujinimą
  PageSpin.Value := PdfView1.PageNumber;
end;

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

Įvykis suveikia tik pilnoms XFA formoms, kurių išdėstymas keičiasi vykdymo metu. Statinės XFA ir AcroForm dokumentai jo niekada nekelia, tad peržiūrova, tvarkanti abu, gali palikti tą pačią doroklę priskirtą. Palikti jos nepriskirtos irgi saugu; persižengėjas už TPdf.PageCount taikomas bet kokiu atveju, o įvykis yra tam, kad hostas atnaujintų tai, ką sukaupęs

Kodėl įvesties laukas lieka sename puslapyje, kai laukas persikelia?

Rėmas persikėlė, o redaktorius ne, nes savoji XFA pranešėja lygino stačiakampį su pačiu savimi. Kai išdėstymas keičia jau pakrauto valdiklio geometriją, PDFium turėtų pastebėti naująjį stačiakampį ir valdikliui kviesti PerformLayout, kuris perstato teksto redaktorių ir jo sritį. Patikra lygino GetWidgetRect() su RecacheWidgetRect(). Abi funkcijos grąžina const nuorodą į tą patį narį, o perkrovimas tą narį perrašo vietoje, tad palyginimas visada matydavo dvi identiškas reikšmes, ir pakrautieji valdikliai praleisdavo savą pertvarkymą

Simptomas iškilo, kai testas pakeitė poformio aukštį taip, kad esami laukai persikėlė ant kito puslapio. Abiejose V8 architektūrose lauko rėmas būdavo piešiamas naujoje pozicijoje, kol įrašytas tekstas ir pelės sritis likdavo ankstesnėje Y koordinatėje. Aiškus pakartotinis išdėstymas nepadėdavo, puslapio perskaitymas irgi, nes valdiklis vis dar tikėjo savo geometriją esant aktualią. Su v3.126.1 keliaujančios Windows V8 bibliotekos prieš perkraudamos nukopijuoja senąjį stačiakampį pagal reikšmę ir lygina tą kopiją, tad persikėlę valdikliai išsiderankuoja iš naujo, ir redaguota reikšmė atsiranda tiksliai ten, kur rėmas. Tai savoji pataisa: ji keliauja su DLL, tad Pascal modulių atnaujinimas, paliekant senesnę pdfium.v8.dll, netinkamas sritis palieka vietoje. Regresijos patikra, privertusi tai padaryti, išgyvenusią eilutę pirmiausia paredaguoja į nenumatytąją reikšmę, o tada reikalauja tos reikšmės naujoje lauko vietoje, nes su numatytosiomis reikšmėmis atkurta eilutė kitaip atrodytų kaip sėkmė

PDFium Component valdiklio pertvarkymo diagrama, kontrastuojanti senąjį savęs palyginimą, kai GetWidgetRect ir RecacheWidgetRect grąžindavo vieną bendrą narį, tad persikėlę valdikliai praleisdavo PerformLayout, su v3.126.1 Windows V8 kopijavimo pagal reikšmę patikra, perstatančia redaktorių ir pelės sritį ant naujai nupiešto rėmo
Stačiakampio lyginimas su pačiu savimi niekada nežlunga, tad rėmas persikeldavo, kol įrašytas tekstas ir paspaudimai likdavo atsilikę – kol patikra pirmiausia neišsaugodavo kopijos pagal reikšmę

Kaip TPdfView perskaito puslapius neištraukęs rankenėlės iš po PDFium kojų?

Nuo v3.126.2 TPdfView puslapių perskaitymą, sekinantį XFA išdėstymo pakeitimą, atideda, kol savoji kvietimų krūva išsivynioja. Puslapių įvykis paprastai suveikia, kol PDFium vis dar apdoroja įvestį: naudotojas paspaudė Pridėti eilutės mygtuką, paspaudimas paleido scenarijų, scenarijus pakeitė egzempliorių skaičių, ir išdėstymas pasibaigė tame pačiame savajame kvietime. Puslapio rankenėlės užvėrimas ir atvėrimas tą akimirką atlaisvintų objektą, kurį kvientėjas vis dar naudoja. Iki v3.126.2 peržiūrova tik save paskelbdavo pasenusia, tad rodomoji puslapio rankenėlė galėdavo toliau rodyti į iki išdėstymo buvusią būseną, o jei naudotojas buvo paskutiniame puslapyje, kai šis dingo, pažymėto puslapio numeris išeidavo už ribų

Atidėtasis atnaujinimas dirba keliais smulkiais žingsniais, ir jie paaiškina elgseną, kurią iš hosto matote:

  1. Puslapių įvykio atgalinis kvietimas peržiūrovą pažymi turinčią laukiantį XFA išdėstymo atnaujinimą ir išsiunčia privatų lango pranešimą; pakartotiniai įvykiai, kol pranešimas neatkeliauja, suliejami į vieną atnaujinimą
  2. Peržiūrova be lango rankenėlės laiko laukiančią vėliavėlę, o pranešimą išsiunčia iš CreateWnd; dokumentų keitimas, peržiūrovos išjungimas ar sunaikinimas vėliavėlę nuima
  3. Kai pranešimas atkeliauja, peržiūrova išvalo teksto žymėjimą, paieškos paryškinimą ir sufokusuoto lauko indeksą – visi treji rodė į senąjį išdėstymą
  4. Pažymėtasis puslapis sugniaužiamas iki naujojo PageCount; pasikeitęs puslapio numeris eina per įprastą puslapių perjungimą, kitaip dabartinis puslapis perskaitomas, o pritaikymo (fit) veiksena taikoma dar kartą
  5. Jei išdėstymas palieka apskritai be puslapių, peržiūrova senąją puslapio rankenėlę iškrauna vietoj puslapio, kurio nebėra, piešimo
PDFium Component TPdfView atidėto XFA atnaujinimo diagrama, kur puslapių įvykis savosios išdėstymo kvietimų krūvos viduje tik pažymi laukiantį atnaujinimą ir išsiunčia lango pranešimą, kuris vėliau išvalo pasenusią žymėjimo būseną, sugniaužia puslapį iki naujojo PageCount ir perskaito arba iškrauna puslapio rankenėlę
Perskaitymas laukia, kol savosios kvietimų krūva išsivynioja: išsiųstas pranešimas sulieja pakartotinius įvykius, tada peržiūrova sugniaužia puslapį, perskaito jį ir kelia OnPageChange

Tas pats apribojimas taikomas ir jūsų kodui. OnXfaPageCountChanged lekia savosios išdėstymo atgalinio kvietimo viduje, tad elkitės su juo kaip su pranešimu: ten atnaujinkite užrašus, sukų ruožus ir įrankių juostos būseną, o ką sunkesnį – dokumento užvėrimą ar kito atvėrimą – eikite per išsiųstą pranešimą, kad atsileistų po atgalinio kvietimo grąžos. TPdfView.OnPageChange tada pasako, kada peržiūrova puslapį jau tikrai perskaitė, o PdfView1.PageNumber skaitymas tą akimirką duoda sugniaužtąją reikšmę. Tab klavišų vaikščiojimas ir FormType patikros, kurias formų peržiūrova atlieka atvėrdama, dengia PDF formų laukų navigacija su PDFium Component

Kodėl paspaudus pilno XFA lauką iškyla „Cannot open text page“?

Pilni XFA puslapiai neturi PDF teksto puslapio, o iki v3.126.2 peržiūrovos numatytasis teksto žymėjimas ir nuorodų atpažinimas vis tiek bandydavo vieną pakrauti. Su TPdfView.AllowUserTextSelection ant numatytosios True, užvedimas klaupdavo teksto sluoksnį dėl simbolio po pele, o pelės atleidimo paspaudimas paleisdavo automatinę URL žvalgybą per puslapio tekstą. Pilno XFA puslapyje teksto puslapio atverti negalima, tad paprastas paspaudimas į lauką galėjo pasibaigti Cannot open text page išimtimi. Nuo v3.126.2 abu vidiniai keliai, kai TPdf.FormType yra ftXfaFull ir XFA vykdymo aplinka prieinama, negrąžina rezultato, tad numatytosios nuostatos veikia, o laukų įvestis lieka prieinama

AllowUserTextSelection išjungimas pilniems XFA dokumentams lieka pagrįstu sąsajos pasirinkimu, nes nėra puslapio teksto, kurį žymėti, o tempimo gestai neturėtų paleisti žymėjimo veiksenos. Bet tai nėra atnaujinimo pakaitalas: ankstesnėse versijose URL žvalgyba paspaudimo metu nuo tos savybės nepriklausė, tad peržiūrova tą pačią išimtį galėjo gauti ir su išjungtu žymėjimu

procedure TClaimForm.ConfigureViewerForForm;
begin
  // FormType skaito atvertąjį dokumentą, tad kviesti po FPdf.Active := True
  if FPdf.XFA and (FPdf.FormType = ftXfaFull) and FPdf.XfaRuntimeAvailable then
  begin
    // Pilnų XFA puslapių PDF teksto sluoksnio nėra; laukai lieka redaguojami
    PdfView1.AllowUserTextSelection := False;
    StatusBar1.SimpleText := Format('Dynamic XFA form, %d page(s)',
      [FPdf.PageCount]);
  end
  else
    PdfView1.AllowUserTextSelection := True;
end;

Rašymas v3.126.2 sulaukė savos atskiros pataisos. Savasis XFA teksto redaktorius gavęs simbolį žymės nepakeičia: FORM_OnChar įterpia prie žymeklio, o Backspace šalina po vieną simbolį, tad reikšmės pažymėjimas ir rašymas ant jos duodavo seną ir naują tekstą šalia vienas kito. PDFium Component dabar prisimena, kad paspaudimas nusileido ant XFA teksto lauko, ir įrašytus simbolius, Backspace ir Delete nukreipia per FORM_ReplaceSelection, kai tik žymė yra, o dokumentas suteikia formų užpildymo ar keitimo teisę. Ar tik skaitomas XFA laukas gali keistis, vis dar sprendžia savasis redaktorius, tad laukas, pažymėtas tik skaitymui formoje, išlaiko reikšmę net dokumente, kuris kitaip leistų pildyti. TPdfView.AllowFormEvents nustatymas į False irgi sustabdo šį klaviatūros nukreipimą – tai išlaiko tik skaitymui skirtą peržiūrovą tik skaitymui

Trumpa atmintinė: dinaminis XFA Delphi peržiūrovoje

SimptomasPriežastisSutvarkyta
Puslapių skaičius rodo 1, kai forma paauga iki dviejų puslapiųSavoji puslapių įvykio žinutė perduoda pridėtos/pašalintos deltą, o ne sumąv3.126.1 (aplinkas)
Lauko rėmas persikelia, įrašytas tekstas ir sritis lieka atsilikęPakrautasis valdiklis po savęs palyginimo praleido pertvarkymąv3.126.1 (Windows V8 bibliotekos)
Peržiūrova piešia ar nukreipia įvestį į iki išdėstymo puslapio būsenąPuslapio rankenėlė po persiskirstymo neperskaitytav3.126.2 (atidėtasis atnaujinimas)
Paspaudimas į lauką iškelia Cannot open text pageTeksto žymėjimas ir URL žvalgyba puslapiuose be teksto sluoksniov3.126.2
Rašymas ant pažymėtos reikšmės prikimšo vietoj pakeitimoSavasis XFA redaktorius įterpia prie žymekliov3.126.2
  • Nustatykite EnableV8Engine į True prieš bet kokiam dokumentui keliaujant, ir apdorokite OnXfaRuntimeMissing atvejui, kai paprasta biblioteka pakrauta pirma
  • Sumą skaitykite iš TPdf.PageCount arba OnXfaPageCountChanged parametro NewCount; puslapių skaičių patys niekada nesudėkite ir neatimkite
  • OnXfaPageCountChanged doroklę laikykite lengvą, nes ji lekia savosios išdėstymo atgalinio kvietimo viduje
  • Dabartinio puslapio indikatorių sinchronizuokite TPdfView.OnPageChange, suveikiančiame po atidėtojo perskaitymo, kuris sugniaužia puslapio numerį
  • Išdėstykite v3.126.1 ar naujesnes Windows V8 DLL kartu su moduliais; valdiklių pertvarkymo pataisa gyvena savajame kode
  • Bandykite su forma, kuri tikrai keičia puslapių skaičių ir perkelia redaguotą lauką per puslapių ribą, nes fiksuoto ilgio pavyzdžiai šiame sąraše pridengia kiekvieną klaidą

Dinaminis XFA puslapių skaičių ir laukų geometriją paverčia gyvomis reikšmėmis, ir peržiūrova lieka teisinga tik tada, kai jas ima iš baigtojo išdėstymo, o puslapius perskaito saugiu momentu. PDFium Component abi smulkmenas tvarko TPdf ir TPdfView viduje, tad hostui lieka tik klausytis. Detalės ir atsiuntimai yra PDFium Component for Delphi produkto puslapyje