TPdf.SetFocusedFormFieldText v PDFium Component zapisuje do živého editačného bufferu aktuálne zaostreného poľa formulára a pri formulári XFA sa tento buffer nikdy nedostane do balíka datasets, ktorý sa serializuje na disk – takže hodnota, ktorú používateľ napísal a váš kód potvrdil ako prijatú, po ďalšom otvorení súboru ticho zmizne. Polia AcroForm tento problém nemajú: to isté volanie zapíše hodnotu do položky /V daného poľa v okamihu, keď fokus prejde inam. Používateľ, ktorý vyplní vstupný formulár XFA, uloží ho a po opätovnom otvorení zistí, že pole so sumou je opäť prázdne, nenaráža na chybu vykresľovania – naráža na hranicu toho, čo samotný engine PDFium vôbec vystavuje pre zápis dát formulára
Toto je užšia otázka než len rozpoznanie formulára XFA alebo spustenie jeho JavaScriptu: nejde o to, „či PDFium podporuje XFA“, ani o to, „ako spustiť skripty AcroForm“, ale konkrétne o to, čo sa stane s hodnotou po tom, čo SetFocusedFormFieldText ohlási úspech. Skrátene povedané, AcroForm a XFA nie sú z pohľadu zápisovej cesty PDFia dva dialekty toho istého modelu formulárov – sú to dva modely formulárov s úplne odlišným vzťahom medzi tým, čo používateľ napíše, a tým, čo uloženie skutočne zachytí, a práve zamieňanie oboch mení jednoriadkové volanie API na požiadavku podpory tri týždne po spustení pilotného nasadenia u zákazníka. Článok o JavaScripte v AcroForm ukazuje toto jednoriadkové volanie a rozdiel medzi AcroForm a XFA uvádza len v komentári kódu; tento článok zostáva pri tom istom API a prechádza internou zápisovou cestou, dôkazom cez balík datasets, že zápis XFA sa nikdy nepretaví, dôvodom, prečo táto medzera sedí v samotnom PDFiu a nie vo väzbe pre Delphi, a riešením formou vlastnej úpravy XML pre dokumenty, ktoré potrebujú, aby úprava prežila uloženie
Ako SetFocusedFormFieldText zapisuje hodnotu poľa?
TPdf.SetFocusedFormFieldText funguje tak, že simuluje úpravu na úrovni jednotlivých stlačení klávesov, nie priamym vložením hodnoty do modelu dokumentu. Interne zavolá FORM_SelectAllText, aby vybral aktuálny obsah zaostreného poľa, a potom FORM_ReplaceSelection, aby výber prepísal novým reťazcom – rovnaké dve operácie, aké by spustil výber všetkého a napísanie textu z klávesnice. Keďže zápis prechádza interaktívnou textovou úpravovou cestou PDFia a nie okolo nej, akýkoľvek skript viazaný na pole pre stlačenie klávesu, formátovanie alebo výpočet sa spustí presne tak, ako keby písal človek, čo robí toto API užitočné pre programové vypĺňanie formulárov v prehliadači, ktorý udržiava JavaScript živý. Čítacím náprotivkom je FocusedFormFieldText, podložený FORM_GetFocusedText, a odráža ten istý živý buffer, do ktorého práve zapísal 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');
Prečo AcroForm hodnotu uchová a XFA ju stratí?
Textové a comboboxové polia AcroForm pretrvávajú preto, lebo samotné prostredie vypĺňania formulárov v PDFiu za vás zapíše editačný buffer: v okamihu, keď pole stratí fokus, sa buffer zapíše do jeho položky /V – toho istého kľúča, na ktorý sa pozerá každý štandardu vyhovujúci čítač PDF, aby zistil uloženú hodnotu poľa. TPdf.ClearFormFieldFocus – ktorá pod kapotou volá FORM_ForceToKillFocus – vynúti toto zapísanie na požiadanie, takže kód, ktorý nastavuje hodnotu programovo, nemusí čakať na skutočné kliknutie myšou niekde inde v používateľskom rozhraní. Uložte súbor hneď potom a nový text je súčasťou grafu objektov dokumentu ešte predtým, než sa vôbec spustí TPdf.SaveAs, pretože /V je skutočná položka v skutočnom slovníku poľa, nie niečo dodatočne priskrutkované
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
Kde skutočne žije úprava poľa XFA?
Polia XFA takéto prepojenie nemajú. Text, ktorý používateľ napíše, skončí v bufferi CPWL_Edit, ktorý patrí vykresľovacej a interakčnej vrstve PDFia pre XFA, a táto vrstva nemá žiadnu cestu kódu, ktorá by buffer skopírovala späť do balíka datasets uloženého v PDF. TPdf.GetXfaDatasets túto medzeru zviditeľňuje: zavolajte ju pred úpravou poľa XFA a po nej a bajty, ktoré vráti, budú identické, pretože metóda číta pôvodný balík, s ktorým bol dokument otvorený, nikdy živý stav widgetu, ktorý ste práve upravili. Nič z toho nie je chyba vyrovnávacej pamäte ani problém s časovaním obnovenia – balík datasets na disku a editačný buffer v pamäti sú jednoducho dva odlišné kusy stavu, ktoré verejné API PDFia nikdy nepreplo
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;
Ide o chybu PDFium Component, alebo o obmedzenie samotného PDFia?
Chýbajúci diel sedí v samotnom PDFiu, nie vo väzbe pre Delphi nad ním. Verejné API PDFia nemá FPDF_SetXFAPacket na vloženie aktualizovaného balíka ani FPDF_SaveAsXFA, ktorý by požiadal engine XFA, aby pred uložením serializoval svoj aktuálny DOM späť do XML v datasets. FPDF_SaveAsCopy – export, na ktorom stojí TPdf.SaveAs – zapisuje graf objektov dokumentu, ktorý už PDFium má; nemá žiaden hák na to, aby požiadal engine XFA najprv vypláchnuť svoj živý stav, pretože taký hák jednoducho neexistuje na strane samotného upstreamu. PDFium Component nemôže dodať zosúladenie, ktoré samotné PDFium nikdy neimplementovalo, a nasadenie vlastného, domácky vyrobeného serializátora DOM do XML, ktorý by hádal interný stav XFA v PDFiu, by bolo horšie než čestne priznaná medzera: pôsobilo by to, že to funguje, až kým ďalšia verzia PDFia niečo nezmení spôsobom, ktorý mimo projektu nikto nevidí
Táto hranica sa ukázala počas toho istého auditu k verzii v2.13.2, ktorý vôbec vytvoril SetFocusedFormFieldText. FORM_ReplaceSelection bola vo väzbovej tabuľke DLL zaviazaná už niekoľko verzií bez toho, aby ju kód v Pascale niekedy zavolal, a práve pridanie zápisovej cesty, ktorá ju konečne využila, urobilo túto medzeru pretrvávania dostatočne konkrétnou na zdokumentovanie namiesto teoretického rizika. Ten istý audit odhalil aj nesúvisiacu, no príbuznú medzeru: JavaScript v AcroForm bol tichy vypnutý od verzie v2.13.0, pretože platforma JS bola zapojená len vnútri inicializačnej vetvy pre XFA, takže bežné dokumenty AcroForm s app.alert alebo počítanými poľami nikdy nedostali skriptovací engine vôbec. Táto chyba sa dala opraviť – rozšírením platformy JS na každý dokument bez ohľadu na XFA – a vyšla v tej istej verzii; medzera v pretrvávaní opísaná tu opraviteľná nebola, z vyššie uvedených dôvodov. Oprava JavaScriptu a udalosti hostiteľského veta okolo nej sú pokryté v článku spúšťanie JavaScriptu AcroForm s PDFium Component
Čo by ste s tým mali robiť v Delphi?
Pri dokumentoch AcroForm je oprava len otázkou dobrého zvyku: zavolajte ClearFormFieldFocus (alebo inak presuňte fokus inam) pred SaveAs vždy, keď bola hodnota nastavená programovo, namiesto toho, aby ste predpokladali, že neskoršia interakcia s používateľským rozhraním za vás zapísanie vyvolá. Pri dokumente, ktorý môže byť buď AcroForm, alebo XFA – čo je bežný prípad vo všeobecnom prehliadači – skontrolujte FormType alebo booleovskú hodnotu XFA skôr, než volajúcemu sľúbite, že sa uloženie „chytí“, a prečítajte si rozpoznávanie formulárov XFA a extrakciu balíkov XFA pre kompletnú sadu skúšok vrátane prípadu XFAF, keď je obsah XFA navrstvený nad inak bežnými widgetmi AcroForm, ktoré /V rešpektujú
Pre skutočný dynamický formulár XFA, kde upravené hodnoty musia prežiť uloženie, interaktívny editačný buffer nie je vôbec ten správny nástroj. Trvácny postup je považovať GetXfaDatasets za svoj základ, nie za výsledok: prečítať ho raz pri otvorení dokumentu, viesť si vlastný záznam o tom, čo používateľ zmenil pole po poli – presne tie hodnoty, ktoré vaše rozhranie už má, keďže PDFium vám ich naspäť nevráti – zapísať tieto zmeny do základu XML sami a riadiť si vlastný výstup. Zápis, ktorý prechádza cez XML pod kontrolou vášho vlastného kódu, prežije uloženie, ktoré buffer CPWL_Edit nikdy neprežije
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;
Odhalenie medzery skôr, ako na ňu narazí zákazník
TPdf.SaveAs vráti True bez ohľadu na to, či hodnota poľa XFA prežila, pretože z pohľadu PDFia sa uloženie skutočne podarilo – zapísalo každý bajt, o ktorý bolo požiadané. Práve preto ide presne o taký typ chyby, ktorý prekĺzne cez dymový test a dostane sa až k zákazníkovi: nič nevyhodí výnimku, nič sa nezaloguje, súbor sa v poriadku otvorí, mýli sa len konkrétna hodnota. Test opätovného otvorenia, ktorý skutočne znovu otvorí uložený súbor a porovná hodnotu poľa – alebo porovná GetXfaDatasets pred úpravou a po nej, podľa predchádzajúceho príkladu – patrí do regresnej sady pre každý prehliadač, ktorý umožňuje používateľom upravovať obsah XFA, nielen do ciest pre AcroForm, ktoré fungujú predvolene
Nič z toho nie je chyba, ktorú treba nahlásiť voči PDFium Component, skôr ide o hranicu, okolo ktorej treba navrhovať riešenie: SetFocusedFormFieldText robí presne to, čo hovorí jeho názov, pre oba modely formulárov, a rozdiel vo výsledku sa dá jasne vystopovať k tomu, na čo je tento buffer napojený v PDFiu pre AcroForm a na čo pre XFA. API, primitíva pre fokus a uloženie a čítače balíkov spomenuté v tomto článku sú súčasťou komponentu PDFium Component pre Delphi a C++Builder