Exit code: 0 Wall time: 14 seconds Output: Urejanja polj XFA izginejo pri shranjevanju v PDFium za Delphi

Tehnični članak

Zakaj urejanja polj XFA izginejo pri shranjevanju v PDFium za Delphi

TPdf.SetFocusedFormFieldText v PDFium Component zapiše besedilo v trenutni živi medpomnilnik za urejanje osredotočenega polja obrazca, pri obrazcu XFA pa ta medpomnilnik nikoli ne doseže paketa datasets, ki se serializira na disk — zato vrednost, ki jo uporabnik vtipka in za katero vaša koda potrdi, da je bila sprejeta, ob naslednjem odprtju datoteke tiho izgine. Polja AcroForm te težave nimajo: isti klic se zapiše v vnos /V polja takoj, ko se fokus premakne drugam. Uporabnik, ki izpolni obrazec XFA, ga shrani in ob ponovnem odprtju znova najde prazno polje zneska, ne naleti na napako izrisovanja — naleti na mejo tega, kar mehanizem PDFium sam izpostavlja za zapis podatkov obrazca

To je ožje vprašanje od začetnega zaznavanja obrazca XFA ali zagona njegovega JavaScripta: ne gre za vprašanje »ali PDFium podpira XFA« in ne za vprašanje »kako izvedem skripte AcroForm«, temveč posebej za to, kaj se zgodi z vrednostjo, ko SetFocusedFormFieldText poroča o uspehu. Na kratko: AcroForm in XFA z vidika zapisovalne poti PDFium nista dve narečji istega modela obrazca — sta dva modela obrazcev s povsem različnim odnosom med tem, kar uporabnik vtipka, in tem, kar shranjevanje dejansko zajame, mešanje obeh pa enovrstični klic API spremeni v prijavo podpori tri tedne po začetku pilotne uporabe pri stranki. Članek o JavaScriptu AcroForm prikazuje enovrstični klic in izid primerjave AcroForm z XFA navaja v komentarju kode; ta članek ostaja pri istem API-ju ter predstavi notranjo zapisovalno pot, dokaz s paketom datasets, da se zapis XFA nikoli ne shrani, zakaj je vrzel v samem PDFiumu in ne v vezavi za Delphi, ter rešitev z lastnim popravljanjem XML za dokumente, pri katerih mora urejanje preživeti shranjevanje

Kako SetFocusedFormFieldText zapiše vrednost polja

TPdf.SetFocusedFormFieldText deluje tako, da simulira urejanje na ravni pritiska tipke, ne pa tako, da bi vrednost neposredno vstavil v model dokumenta. Interno pokliče FORM_SelectAllText, da izbere trenutno vsebino osredotočenega polja, nato pa FORM_ReplaceSelection, da izbor prepiše z novim nizom — to sta isti operaciji, ki bi ju sprožilo izbiranje vsega in tipkanje s tipkovnico. Ker zapis poteka po interaktivni poti urejanja besedila PDFiuma in ne mimo nje, se vsaka skripta za pritisk tipke, oblikovanje ali izračun, vezana na polje, sproži natanko tako kot pri človeškem tipkanju, zato je API uporaben za programsko izpolnjevanje obrazcev v pregledovalniku z aktivnim JavaScriptom. Ustreznik na bralni strani je FocusedFormFieldText, ki temelji na FORM_GetFocusedText in odraža isti živi medpomnilnik, ki ga je pravkar zapisala funkcija SetFocusedFormFieldText

if Pdf.FocusedFormFieldIndex >= 0 then
begin
  if Pdf.SetFocusedFormFieldText('1284.50') then
    Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
  else
    Log('No field is focused, or it does not accept text');
end
else
  Log('Focus a field first - FocusFormField or a real click');

Zakaj AcroForm ohrani vrednost, XFA pa jo izgubi

Besedilna in kombinirana polja AcroForm se ohranijo, ker lastno okolje PDFiuma za izpolnjevanje obrazcev samo potrdi spremembe v medpomnilniku: takoj ko polje izgubi fokus, se medpomnilnik zapiše v vnos /V polja, isti ključ, ki ga vsak skladen bralnik PDF preveri, da ugotovi shranjeno vrednost polja. TPdf.ClearFormFieldFocus — ki v ozadju pokliče FORM_ForceToKillFocus — na zahtevo prisili to potrditev, zato kodi, ki vrednost nastavi programsko, ni treba čakati na pravi klik miške drugje v uporabniškem vmesniku. Če nato takoj shranite, je novo besedilo del objektnega grafa dokumenta, še preden se izvede TPdf.SaveAs, saj je /V dejanski vnos v pravem slovarju polja in ne nekaj, kar bi bilo dodano naknadno

Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus;              // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');

// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50');   // passes

Kje dejansko živi urejanje polja XFA

Polja XFA nimajo takšne povezave. Besedilo, ki ga uporabnik vtipka, pristane v medpomnilniku CPWL_Edit, ki pripada sloju PDFiuma za izris in interakcijo XFA, ta sloj pa nima poti kode, ki bi medpomnilnik kopirala nazaj v paket datasets, shranjen v PDF-ju. TPdf.GetXfaDatasets vrzel jasno pokaže: pokličite ga pred urejanjem polja XFA in po njem, pa bodo vrnjeni bajti enaki, ker metoda bere izvirni paket, s katerim je bil dokument odprt, in nikoli živega stanja pravkar urejenega gradnika. To ni napaka predpomnjenja ali težava s časom osveževanja — paket datasets na disku in medpomnilnik za urejanje v pomnilniku sta preprosto ločena dela stanja, ki ju javni API PDFiuma nikoli ne poveže

var
  Before, After: TBytes;
begin
  Before := Pdf.GetXfaDatasets;
  Pdf.FocusFormField(FieldIndex);
  Pdf.SetFocusedFormFieldText('1284.50');
  After := Pdf.GetXfaDatasets;
  // Before and After are byte-for-byte identical on an XFA document -
  // the edit never touched the packet GetXfaDatasets reads from
end;

Ali je to napaka PDFium Component ali omejitev PDFiuma

Manjkajoči del je v samem PDFiumu, ne v vezavi za Delphi nad njim. Javni API PDFiuma nima funkcije FPDF_SetXFAPacket za vstavljanje posodobljenega paketa in nima funkcije FPDF_SaveAsXFA, s katero bi mehanizmu XFA naročili, naj pred shranjevanjem serializira trenutni DOM nazaj v XML datasets. FPDF_SaveAsCopy — izvoz, na katerem temelji TPdf.SaveAs — zapiše objektni graf dokumenta, ki ga PDFium že ima; nima kljuke, s katero bi mehanizem XFA prosili, naj najprej izprazni svoje živo stanje, ker takšna kljuka v nadrejeni kodi ne obstaja. PDFium Component ne more dodati usklajevanja, ki ga PDFium sam nikoli ni implementiral, domači serializator iz DOM v XML, ki ugiba o notranjem stanju XFA PDFiuma, pa bi bil slabši od odkrite vrzeli: deloval bi le do naslednje različice PDFiuma, ki bi spremenila nekaj, česar zunaj projekta nihče ne more videti

Ta meja se je pokazala med isto revizijo v2.13.2, v kateri je bil prvotno zgrajen SetFocusedFormFieldText. FORM_ReplaceSelection je bil v uvozni tabeli DLL vezan že več različic, ne da bi ga kdaj poklicala koda Pascal, dodajanje zapisovalne poti, ki ga je končno uporabila, pa je vrzel pri obstojnosti naredilo dovolj konkretno za dokumentiranje in ne več le teoretično. Ista revizija je razkrila še drugo, po duhu povezano vrzel: JavaScript AcroForm je bil od v2.13.0 tiho onemogočen, ker je bila platforma JS povezana samo znotraj veje za inicializacijo XFA, zato običajni dokumenti AcroForm z app.alert ali izračunanimi polji sploh niso dobili skriptnega mehanizma. To je bilo mogoče popraviti — platformo JS je bilo treba razširiti na vsak dokument ne glede na XFA — in popravek je bil izdan v isti različici; vrzeli pri obstojnosti, obravnavani tukaj, iz zgoraj navedenih razlogov ni bilo mogoče odpraviti. Popravek JavaScripta in dogodki host-veto so opisani v članku zagon JavaScripta AcroForm s PDFium Component

Kaj storiti glede tega v Delphiju

Pri dokumentih AcroForm je rešitev le dobra navada: kadar je bila vrednost nastavljena programsko, pred SaveAs pokličite ClearFormFieldFocus (ali kako drugače premaknite fokus), namesto da bi predpostavili, da bo poznejša interakcija z uporabniškim vmesnikom sama sprožila potrditev. Pri dokumentu, ki je lahko AcroForm ali XFA — kar je v splošnem pregledovalniku običajen primer — preverite FormType ali logično vrednost XFA, preden klicatelju obljubite, da bo shranjevanje obstalo, za celoten nabor preverjanj pa preberite članek zaznavanje obrazcev XFA in pridobivanje paketov XFA, vključno s primerom XFAF, pri katerem je vsebina XFA naložena čez običajne gradnike AcroForm, ki upoštevajo /V

Pri pravem dinamičnem obrazcu XFA, pri katerem morajo urejene vrednosti preživeti shranjevanje, interaktivni medpomnilnik za urejanje sploh ni pravo orodje. Obstojna pot je, da GetXfaDatasets obravnavate kot izhodišče in ne kot rezultat: ob odprtju dokumenta ga preberite enkrat, sami beležite, kaj je uporabnik spremenil po posameznih poljih — natančno vrednosti, ki jih vaš uporabniški vmesnik že ima, saj vam jih PDFium pozneje ne bo vrnil — te vrednosti sami vnesite v izhodiščni XML in ustvarite lasten izhod. Zapis, ki poteka prek XML-ja pod nadzorom vaše kode, preživi shranjevanje, ki ga medpomnilnik CPWL_Edit nikoli ne bi mogel

function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
  NewValue: string): TBytes;
var
  DatasetsXml: string;
begin
  // GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
  // your own helper over your own XML library, nothing PDFium provides
  DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
  DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
  Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;

Kako vrzel ujeti, preden jo opazi stranka

TPdf.SaveAs vrne True, ne glede na to, ali je vrednost polja XFA preživela, saj je z vidika PDFiuma shranjevanje res uspelo — zapisal je vsak bajt, za katerega je bil zaprošen. Zato je to natanko takšna napaka, ki uide preizkusu osnovnega delovanja in doseže stranko: nič ne vrže izjeme, nič se ne zapiše v dnevnik, datoteka se brez težav odpre, napačna je le določena vrednost. Preizkus povratnega kroga, ki shranjeno datoteko dejansko znova odpre in primerja vrednost polja — ali po prejšnjem primeru primerja GetXfaDatasets pred urejanjem in po njem — sodi v regresijski nabor vsakega pregledovalnika, ki uporabnikom omogoča urejanje vsebine XFA, ne le v poti AcroForm, ki privzeto deluje

Vse to ni toliko napaka, ki bi jo bilo treba prijaviti za PDFium Component, kot meja, okoli katere je treba zasnovati rešitev: SetFocusedFormFieldText za oba modela obrazcev naredi natanko to, kar pove njegovo ime, razlika v rezultatu pa je neposredno povezana s tem, na kaj AcroForm in XFA na strani PDFiuma vsak pripneta ta medpomnilnik. Tukaj navedeni API, primitive za fokus in shranjevanje ter bralniki paketov so del komponente PDFium Component za Delphi in C++Builder