Tehnički članak

Zašto izmjene XFA polja nestaju pri spremanju u PDFiumu za Delphi

TPdf.SetFocusedFormFieldText u komponenti PDFium Component upisuje u aktivni međuspremnik za uređivanje trenutačno fokusiranog polja obrasca, a kod XFA obrasca taj međuspremnik nikada ne dospijeva u paket datasets koji se serijalizira na disk — zato vrijednost koju korisnik upiše i koju vaš kod potvrdi kao prihvaćenu tiho nestane pri sljedećem otvaranju datoteke. AcroForm polja nemaju taj problem: isti poziv upisuje vrijednost u unos /V čim fokus prijeđe drugdje. Korisnik koji ispuni XFA ulazni obrazac, spremi ga i ponovno otvori da bi pronašao prazno polje iznosa ne nailazi na grešku iscrtavanja — nailazi na granicu onoga što sam mehanizam PDFium izlaže za zapis podataka obrasca

Ovo je uže pitanje od samog otkrivanja XFA obrasca ili pokretanja njegova JavaScripta: nije riječ o tome podržava li PDFium XFA ni kako izvršiti AcroForm skripte, nego konkretno o tome što se događa s vrijednošću nakon što SetFocusedFormFieldText prijavi uspjeh. Ukratko, AcroForm i XFA nisu dva dijalekta istog modela obrasca kada je riječ o putanji zapisivanja u PDFiumu — to su dva modela obrasca s potpuno različitim odnosom između onoga što korisnik upiše i onoga što spremanje stvarno obuhvati, a njihovo miješanje pretvara jednoredni API poziv u zahtjev korisničkoj podršci tri tjedna nakon što pilotna instalacija korisnika proradi. Članak o JavaScriptu za AcroForm prikazuje jednoredni poziv i ishod razlike AcroForm-a i XFA-a navodi u komentaru koda; ovaj članak ostaje na istom API-ju i prolazi kroz internu putanju zapisivanja, dokaz iz paketa datasets da se XFA upis nikada ne sprema, razlog zbog kojeg je nedostatak u samom PDFiumu, a ne u Delphi povezivanju, te zaobilazno rješenje s vlastitom izmjenom XML-a za dokumente u kojima izmjena mora preživjeti spremanje

Kako SetFocusedFormFieldText zapisuje vrijednost polja?

TPdf.SetFocusedFormFieldText radi simuliranjem izmjene na razini pritiska tipke, a ne izravnim upisivanjem vrijednosti u model dokumenta. Interno poziva FORM_SelectAllText kako bi odabrao trenutačni sadržaj fokusiranog polja, zatim FORM_ReplaceSelection kako bi odabrani sadržaj prebrisao novim nizom — iste dvije radnje koje bi pokrenuo odabir svega i upis pomoću tipkovnice. Budući da upis prolazi kroz PDFiumovu interaktivnu putanju uređivanja teksta, a ne zaobilazi je, svaka skripta za pritisak tipke, oblikovanje ili izračun povezana s poljem pokreće se upravo kao pri ručnom upisu, što API čini korisnim za programsko ispunjavanje obrazaca u pregledniku koji održava JavaScript aktivnim. Čitajuća protuteža je FocusedFormFieldText, oslonjena na FORM_GetFocusedText, i odražava isti aktivni međuspremnik koji je upravo upisao 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');

Zašto AcroForm zadržava vrijednost, a XFA je gubi?

Tekstualna i kombinirana AcroForm polja ostaju spremljena jer PDFiumovo okruženje za ispunjavanje obrazaca umjesto vas potvrđuje međuspremnik za uređivanje: čim polje izgubi fokus, međuspremnik se upisuje u unos /V polja, isti ključ koji svaki usklađeni PDF čitač provjerava kako bi saznao pohranjenu vrijednost polja. TPdf.ClearFormFieldFocus — koji u pozadini poziva FORM_ForceToKillFocus — na zahtjev prisiljava tu potvrdu, pa kod koji programski postavlja vrijednost ne mora čekati stvarni klik mišem drugdje u sučelju. Spremite odmah nakon toga i novi tekst već je dio grafa objekata dokumenta prije nego što se TPdf.SaveAs uopće izvrši jer je /V stvarni unos u stvarnom rječniku polja, a ne nešto naknadno dodano

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

Gdje se izmjena XFA polja zapravo nalazi?

XFA polja nemaju takvu poveznicu. Tekst koji korisnik upiše završava u međuspremniku CPWL_Edit koji pripada PDFiumovu sloju za iscrtavanje i interakciju XFA-a, a taj sloj nema putanju koda koja bi međuspremnik kopirala natrag u paket datasets spremljen u PDF-u. TPdf.GetXfaDatasets čini nedostatak vidljivim: pozovite ga prije i nakon izmjene XFA polja i vraćeni bajtovi bit će identični jer metoda čita izvorni paket s kojim je dokument otvoren, a nikada aktivno stanje widgeta koji ste upravo izmijenili. To nije greška predmemoriranja ni problem vremena osvježavanja — paket datasets na disku i međuspremnik za uređivanje u memoriji jednostavno su dva različita dijela stanja koja PDFiumov javni API nikada ne povezuje

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;

Je li ovo greška PDFium Componenta ili ograničenje PDFiuma?

Nedostajući dio nalazi se u samom PDFiumu, a ne u Delphi povezivanju iznad njega. PDFiumov javni API nema FPDF_SetXFAPacket za umetanje ažuriranog paketa ni FPDF_SaveAsXFA za zahtjev XFA mehanizmu da prije spremanja serijalizira trenutačni DOM natrag u XML datasets. FPDF_SaveAsCopy — izvoz iza TPdf.SaveAs — zapisuje graf objekata dokumenta koji PDFium već ima; nema kuku kojom bi od XFA mehanizma zatražio da najprije isprazni svoje aktivno stanje jer takva kuka ne postoji uzvodno. PDFium Component ne može dodati usklađivanje koje sam PDFium nikada nije implementirao, a isporuka vlastitog serijalizatora DOM-a u XML koji nagađa o internom stanju XFA-a bila bi gora od pošteno priznatog nedostatka: izgledala bi kao da radi sve dok sljedeća verzija PDFiuma ne promijeni nešto što nitko izvan projekta ne može vidjeti

Ova se granica pojavila tijekom iste revizije v2.13.2 koja je uopće izgradila SetFocusedFormFieldText. FORM_ReplaceSelection bio je povezan u uvoznoj tablici DLL-a za verzije, ali se nikada nije pozivao iz Pascal koda, a dodavanje putanje zapisivanja koja ga je napokon upotrijebila učinilo je nedostatak trajnosti dovoljno konkretnim za dokumentiranje, a ne samo teorijskim. Ista je revizija otkrila nepovezan, ali sličan nedostatak: JavaScript za AcroForm bio je tiho onemogućen od v2.13.0 jer je JS platforma bila povezana samo unutar grane inicijalizacije XFA-a, pa obični AcroForm dokumenti s app.alert ili izračunatim poljima uopće nisu dobivali mehanizam za skripte. To je bilo popravljivo — proširenjem JS platforme na svaki dokument neovisno o XFA-u — i isporučeno je u istoj verziji; nedostatak trajnosti opisan ovdje nije popravljiv iz prethodno navedenih razloga. Ispravak JavaScripta i događaji zabrane domaćina oko njega obrađeni su u članku pokretanje JavaScripta za AcroForm pomoću PDFium Component

Što biste trebali učiniti u Delphiju?

Za AcroForm dokumente ispravak je samo dobra navika: pozovite ClearFormFieldFocus ili na drugi način premjestite fokus prije SaveAs kad god je vrijednost postavljena programski, umjesto da pretpostavite kako će kasnija interakcija sa sučeljem sama pokrenuti potvrdu. Za dokument koji može biti AcroForm ili XFA — što je uobičajen slučaj u pregledniku opće namjene — provjerite FormType ili booleovsku vrijednost XFA prije nego što pozivatelju obećate da će spremanje trajno sačuvati vrijednost, a za potpuni skup provjera pročitajte otkrivanje XFA obrazaca i izdvajanje XFA paketa, uključujući slučaj XFAF u kojem je sadržaj XFA-a slojevit preko inače uobičajenih AcroForm widgeta koji poštuju /V

Za pravi dinamički XFA obrazac u kojem izmijenjene vrijednosti moraju preživjeti spremanje interaktivni međuspremnik za uređivanje uopće nije pravi alat. Trajno rješenje jest tretirati GetXfaDatasets kao osnovu, a ne kao rezultat: pročitajte ga jednom pri otvaranju dokumenta, vodite vlastitu evidenciju onoga što je korisnik promijenio po poljima — upravo vrijednosti koje vaše sučelje već ima jer ih PDFium naknadno neće vratiti — sami ih unesite u osnovni XML i upravljajte vlastitim izlazom. Upis kroz XML kojim upravlja vaš kod preživjet će spremanje koje međuspremnik CPWL_Edit nikada ne bi mogao

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;

Otkrivanje nedostatka prije korisnika

TPdf.SaveAs vraća True neovisno o tome je li vrijednost XFA polja preživjela jer je sa stajališta PDFiuma spremanje doista uspjelo — zapisao je svaki bajt koji je dobio kao zahtjev. Zbog toga je ovo upravo vrsta nedostatka koja prođe smoke test i stigne do korisnika: ništa ne baca iznimku, ništa se ne zapisuje u dnevnik, datoteka se uredno otvara, a pogrešna je samo određena vrijednost. Test povratnog kruga koji stvarno ponovno otvara spremljenu datoteku i uspoređuje vrijednost polja — ili uspoređuje GetXfaDatasets prije i nakon, prema ranijem primjeru — pripada regresijskom skupu za svaki preglednik koji korisnicima dopušta uređivanje XFA sadržaja, a ne samo putanjama AcroForma koje slučajno rade po zadanim postavkama

Ništa od ovoga nije toliko nedostatak koji treba prijaviti za PDFium Component koliko granica oko koje treba oblikovati rješenje: SetFocusedFormFieldText radi upravo ono što mu ime kaže za oba modela obrazaca, a razlika u ishodu jasno proizlazi iz toga na što AcroForm i XFA povezuju taj međuspremnik na strani PDFiuma. Ovdje navedeni API, primitive za fokus i spremanje te čitači paketa dio su komponente PDFium Component za Delphi i C++Builder