TPdf.SetFocusedFormFieldText u proizvodu PDFium Component upisuje tekst u aktivni memorijski bafer trenutno fokusiranog polja obrasca, ali kod XFA obrasca taj bafer nikada ne stiže do paketa datasets koji se serijalizuje na disk — zato vrednost koju korisnik unese i koju vaš kod potvrdi kao prihvaćenu nečujno nestaje pri sledećem otvaranju datoteke. AcroForm polja nemaju taj problem: isti poziv upisuje vrednost u stavku /V čim polje izgubi fokus. Korisnik koji popuni XFA prijemni obrazac, sačuva ga i ponovo otvori da bi zatekao prazno polje za iznos ne nailazi na grešku prikaza — već na granicu onoga što sam PDFium mehanizam izlaže za upis podataka obrasca
Ovo je uže pitanje od samog otkrivanja XFA obrasca ili pokretanja njegovog JavaScript-a: nije pitanje „da li PDFium podržava XFA“ niti „kako da izvršim AcroForm skripte“, već šta se tačno događa sa vrednošću nakon što SetFocusedFormFieldText prijavi uspeh. Ukratko, AcroForm i XFA nisu dva dijalekta istog modela obrasca kada je reč o putanji upisa u PDFium-u — to su dva modela sa potpuno različitim odnosom između onoga što korisnik unese i onoga što čuvanje stvarno obuhvati, a mešanje ta dva modela pretvara poziv API-ja od jednog reda u prijavu podršci tri nedelje nakon početka pilot-primene kod korisnika. Članak o AcroForm JavaScript-u prikazuje poziv od jednog reda i ishod razlike između AcroForm-a i XFA-a navodi u komentaru koda; ovaj članak ostaje uz isti API i prolazi kroz interni put upisa, dokaz iz paketa datasets da se XFA izmena nikada ne upisuje, razlog zbog kojeg je praznina u samom PDFium-u a ne u Delphi povezivanju i zaobilazno rešenje sa sopstvenom izmenom XML-a za dokumente u kojima izmena mora preživeti čuvanje
Kako SetFocusedFormFieldText upisuje vrednost polja
TPdf.SetFocusedFormFieldText radi tako što oponaša izmenu na nivou pritiska tastera, a ne tako što direktno upisuje vrednost u model dokumenta. Interno poziva FORM_SelectAllText da izabere trenutni sadržaj fokusiranog polja, a zatim FORM_ReplaceSelection da izabrano zameni novim nizom — iste dve operacije koje bi izazvalo biranje svega i kucanje preko tastature. Pošto upis prolazi kroz PDFium-ovu interaktivnu putanju uređivanja teksta, a ne zaobilazi je, svaki skript vezan za pritisak tastera, formatiranje ili izračunavanje polja pokreće se upravo kao pri ručnom kucanju, zbog čega je API koristan za programsko popunjavanje obrazaca u pregledaču koji drži JavaScript aktivnim. Odgovarajuća metoda za čitanje je FocusedFormFieldText, zasnovana na FORM_GetFocusedText, i ona odražava isti živi bafer koji je upravo izmenio 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 vrednost, a XFA je gubi
AcroForm tekstualna polja i padajuće liste opstaju zato što PDFium-ovo okruženje za popunjavanje obrazaca samo potvrđuje izmenu: čim polje izgubi fokus, bafer se upisuje u stavku /V, isti ključ koji svaki usklađen PDF čitač koristi da sazna sačuvanu vrednost polja. TPdf.ClearFormFieldFocus — koji u pozadini poziva FORM_ForceToKillFocus — po zahtevu prisiljava tu potvrdu, pa kod koji programski postavi vrednost ne mora čekati da korisnik stvarno klikne negde u interfejsu. Ako odmah zatim sačuvate dokument, novi tekst je deo grafa objekata dokumenta pre nego što se uopšte pokrene TPdf.SaveAs, jer je /V stvarna stavka u stvarnom rečniku polja, a ne nešto naknadno dodato
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
Gde se izmena XFA polja zaista nalazi
XFA polja nemaju takvu vezu. Tekst koji korisnik unese završava u baferu CPWL_Edit sloja PDFium-a za XFA prikaz i interakciju, a taj sloj nema putanju koja bi bafer kopirala nazad u paket datasets sačuvan u PDF-u. TPdf.GetXfaDatasets jasno pokazuje prazninu: pozovite ga pre i posle izmene XFA polja i dobićete iste bajtove, jer metoda čita izvorni paket sa kojim je dokument otvoren, a nikada živo stanje vidžeta koji ste upravo izmenili. To nije greška keširanja niti pitanje trenutka osvežavanja — paket datasets na disku i bafer izmene u memoriji jednostavno su dva odvojena stanja koja javni API PDFium-a 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;
Da li je ovo greška PDFium Component-a ili ograničenje PDFium-a
Nedostajući deo nalazi se u samom PDFium-u, a ne u Delphi povezivanju iznad njega. Javni API PDFium-a nema FPDF_SetXFAPacket za ubacivanje izmenjenog paketa niti FPDF_SaveAsXFA za zahtev da XFA mehanizam pre čuvanja serijalizuje trenutni DOM nazad u XML paketa datasets. FPDF_SaveAsCopy — izvoz koji stoji iza metode TPdf.SaveAs — upisuje graf objekata dokumenta koji PDFium već ima; nema kuku kojom bi od XFA mehanizma zatražio da prvo isprazni svoje živo stanje, jer takva kuka uzvodno ne postoji. PDFium Component ne može dodati usklađivanje koje sam PDFium nikada nije implementirao, a sopstveni serijalizator DOM-a u XML koji nagađa o internom XFA stanju PDFium-a bio bi gori od otvorenog ograničenja: izgledalo bi da radi sve dok sledeća verzija PDFium-a ne promeni nešto što niko izvan projekta ne može videti
Ova granica se pojavila tokom iste revizije v2.13.2 u kojoj je prvobitno napravljen SetFocusedFormFieldText. FORM_ReplaceSelection je godinama bio vezan u tabeli DLL uvoza, a da se nijednom nije pozvao iz Pascal koda; dodavanje putanje upisa koja ga je konačno koristila učinilo je problem opstanka dovoljno konkretnim da se dokumentuje, a ne ostane teorijski. Ista revizija otkrila je još jednu nepovezanu, ali srodnu prazninu: AcroForm JavaScript bio je nečujno isključen od verzije v2.13.0 jer je JS platforma bila povezana samo unutar grane za inicijalizaciju XFA-a, pa obični AcroForm dokumenti sa app.alert ili izračunatim poljima uopšte nisu dobijali skriptni mehanizam. To je bilo rešivo — proširenjem JS platforme na svaki dokument bez obzira na XFA — i isporučeno je u istoj verziji; ovde opisano ograničenje opstanka nije bilo rešivo iz prethodno navedenih razloga. Ispravka JavaScript-a i događaji veta domaćina obrađeni su u članku o pokretanju AcroForm JavaScript-a sa PDFium Component-om
Šta treba uraditi u Delphi kodu
Kod AcroForm dokumenata rešenje je samo dobra navika: pozovite ClearFormFieldFocus ili na drugi način sklonite fokus pre SaveAs kada je vrednost postavljena programski, umesto da pretpostavite da će kasnija interakcija sa interfejsom potvrditi izmenu. Ako dokument može biti AcroForm ili XFA — što je uobičajeno u pregledaču opšte namene — proverite FormType ili logičku vrednost XFA pre nego što pozivaocu obećate da će čuvanje opstati, a zatim pročitajte članak o otkrivanju XFA obrazaca i izdvajanju XFA paketa za potpun skup provera, uključujući slučaj XFAF u kojem je XFA sadržaj postavljen preko inače običnih AcroForm vidžeta koji poštuju /V
Kod pravog dinamičkog XFA obrasca, u kojem izmenjene vrednosti moraju preživeti čuvanje, interaktivni bafer za izmenu uopšte nije pravi alat. Trajno rešenje je da GetXfaDatasets tretirate kao početnu osnovu, a ne kao rezultat: pročitajte ga jednom pri otvaranju dokumenta, vodite sopstvenu evidenciju o tome šta je korisnik promenio za svako polje — upravo vrednosti koje vaš interfejs već ima, jer ih PDFium naknadno neće vratiti — sami unesite te vrednosti u početni XML i generišite sopstveni izlaz. Upis kroz XML kojim upravlja vaš kod preživeće čuvanje koje bafer CPWL_Edit nikada ne bi mogao da sačuva
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 otkriti problem pre korisnika
TPdf.SaveAs vraća True bez obzira na to da li je vrednost XFA polja opstala, jer je iz perspektive PDFium-a čuvanje zaista uspelo — upisani su svi bajtovi koje je trebalo upisati. Zbog toga je ovo baš ona vrsta greške koja prođe pored brzog testa i stigne do korisnika: ništa ne prijavljuje izuzetak, ništa se ne upisuje u dnevnik, datoteka se normalno otvara, a pogrešna je samo konkretna vrednost. Test kružnog toka koji zaista ponovo otvara sačuvanu datoteku i poredi vrednost polja — ili poredi GetXfaDatasets pre i posle, prema ranijem primeru — pripada regresionom skupu za svaki pregledač koji korisnicima dopušta izmenu XFA sadržaja, a ne samo putanjama AcroForm-a koje podrazumevano rade
Ovo nije toliko greška za prijavu protiv PDFium Component-a koliko granica oko koje treba projektovati rešenje: SetFocusedFormFieldText radi upravo ono što mu ime kaže za oba modela obrazaca, a razlika u ishodu jasno proizlazi iz toga na šta AcroForm i XFA na strani PDFium-a povezuju taj bafer. API, primitive za fokus i čuvanje i čitači paketa pomenuti ovde deo su proizvoda PDFium Component za Delphi i C++Builder