Technický článek

Proč úpravy polí XFA v PDFium pro Delphi zmizí při uložení

TPdf.SetFocusedFormFieldText v PDFium Component zapisuje do živého editačního bufferu aktuálně zaostřeného pole formuláře, a u formuláře XFA se tento buffer nikdy nedostane do balíčku datasets, který se serializuje na disk — takže hodnota, kterou uživatel napíše, a váš kód potvrdí, že byla přijata, tiše zmizí při příštím otevření souboru. Pole AcroForm tento problém nemají: stejné volání se zapíše do záznamu /V pole v okamžiku, kdy fokus odejde jinam. Uživatel, který vyplní příjmový formulář XFA, uloží a znovu otevře, aby zjistil, že je pole s částkou zase prázdné, nenaráží na vykreslovací glitch — naráží na okraj toho, co samotný engine PDFium vystavuje pro zápis dat formuláře

To je užší otázka, než detekce formuláře XFA na prvním místě, nebo rozběhnutí jeho JavaScriptu: ne „podporuje PDFium XFA" a ne „jak spustím skripty AcroForm", ale konkrétně co se stane s hodnotou poté, co SetFocusedFormFieldText ohlásí úspěch. Krátká verze je, že AcroForm a XFA nejsou dva dialekty stejného modelu formuláře, pokud jde o zápisovou cestu PDFium — jsou to dva modely formuláře se dvěma úplně odlišnými vztahy mezi tím, co uživatel napíše, a tím, co uložení skutečně zachytí, a zaměňovat je je to, co promění jednořádkové volání API na lístek podpory tři týdny poté, co pilotní nasazení zákazníka jde do provozu. Článek o JavaScriptu AcroForm ukazuje jednořádkové volání a stav výsledku AcroForm-versus-XFA uvádí v komentáři kódu; tento zůstává u stejného API a prochází interní zápisovou cestu, důkaz balíčkem datasets, že zápis XFA nikdy nepřistane, proč mezera sedí v samotném PDFium místo v bindingu Delphi, a obejití „opravte si vlastní XML" pro dokumenty, kde úprava musí přežít uložení

Jak SetFocusedFormFieldText zapisuje hodnotu pole?

TPdf.SetFocusedFormFieldText funguje simulací úpravy na úrovni stisku kláves, ne přímým vpíchnutím hodnoty do modelu dokumentu. Interně volá FORM_SelectAllText, aby vybral aktuální obsah zaostřeného pole, pak FORM_ReplaceSelection, aby přepsal výběr novým řetězcem — stejné dvě operace, jaké by vyvolalo vybrat-vše-a-napsat řízené klávesnicí. Protože zápis prochází přes interaktivní cestu úpravy textu PDFium místo kolem ní, jakýkoli skript stisku klávesy, formátu, nebo výpočtu vázaný na pole se spustí přesně tak, jako by se spustil pro člověka píšícího, což je to, co dělá toto API užitečné pro programové vyplňování formuláře v prohlížeči, který drží JavaScript živý. Protějšek pro čtení je FocusedFormFieldText, podložený FORM_GetFocusedText, a odráží stejný živý buffer, který SetFocusedFormFieldText právě zapsal

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');

Proč AcroForm hodnotu podrží a XFA ji ztratí?

Text AcroForm a comboboxová pole přetrvávají, protože vlastní prostředí vyplňování formuláře PDFium za vás potvrdí editační buffer: v okamžiku, kdy pole ztratí fokus, se buffer zapíše do záznamu /V pole, stejného klíče, na který se dívá každý konformní čtenář PDF, aby zjistil uloženou hodnotu pole. TPdf.ClearFormFieldFocus — který interně volá FORM_ForceToKillFocus — toto potvrzení vynutí na požádání, takže kód, který nastavuje hodnotu programově, nemusí čekat na skutečný klik myší jinde v UI. Uložte hned poté, a nový text je součástí grafu objektů dokumentu ještě předtím, než vůbec proběhne TPdf.SaveAs, protože /V je skutečný záznam ve skutečném slovníku pole, ne něco přišroubovaného dodatečně

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 skutečně žije úprava pole XFA?

Pole XFA nemají žádné takové propojení. Text, který uživatel napíše, skončí v bufferu CPWL_Edit, který patří vrstvě vykreslování a interakce XFA v PDFium, a tato vrstva nemá žádnou cestu kódu, která by buffer zkopírovala zpátky do balíčku datasets uloženého v PDF. TPdf.GetXfaDatasets tuto mezeru zviditelňuje: zavolejte ji před a po úpravě pole XFA, a bajty, které vrátí, jsou identické, protože metoda čte původní balíček, se kterým byl dokument otevřen, nikdy živý stav widgetu, který jste právě upravili. Nic z toho není chyba cachování ani problém s časováním obnovy — balíček datasets na disku a editační buffer v paměti jsou prostě dva různé kusy stavu, které veřejné API PDFium nikdy nepropojuje

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 to chyba PDFium Component, nebo omezení PDFium?

Chybějící kus sedí v samotném PDFium, ne v bindingu Delphi navrch. Veřejné API PDFium nemá žádné FPDF_SetXFAPacket pro vložení aktualizovaného balíčku a žádné FPDF_SaveAsXFA, které by požádalo engine XFA, aby před uložením serializoval svůj aktuální DOM zpátky do XML datasets. FPDF_SaveAsCopy — export, který stojí za TPdf.SaveAs — zapisuje graf objektů dokumentu, který už PDFium má; nemá žádný hák, který by požádal engine XFA, aby nejdřív vyprázdnil svůj živý stav, protože tento hák neexistuje proti proudu. PDFium Component nedokáže přidat sesouhlasení, které samotný PDFium nikdy neimplementoval, a dodat domácí serializátor DOM-na-XML, který by hádal interní stav XFA v PDFium, by bylo horší než poctivá mezera: vypadalo by to, že to funguje, dokud další verze PDFium něco nezmění, co nikdo mimo projekt nevidí

Tato hranice se vynořila během stejného auditu v2.13.2, který vůbec postavil SetFocusedFormFieldText. FORM_ReplaceSelection byla svázaná v tabulce importů DLL po verze, aniž by kdy byla volaná z kódu Pascalu, a přidání zápisové cesty, která ji konečně použila, je to, co mezeru přetrvání udělalo konkrétní na to, aby se zdokumentovala, ne teoretickou. Stejné kolo auditu odhalilo nesouvisející, ale duchem příbuznou mezeru: JavaScript AcroForm byl tiše vypnutý od v2.13.0, protože platforma JS byla zapojená jen uvnitř inicializační větve XFA, takže obyčejné dokumenty AcroForm s app.alert nebo počítanými poli vůbec nedostaly engine skriptu. Ta byla opravitelná — rozšířit platformu JS na každý dokument bez ohledu na XFA — a dodala se ve stejné verzi; mezera v přetrvávání popsaná zde opravitelná nebyla, z důvodů výše. Oprava JavaScriptu a hostitelské veto-události kolem ní popisuje spouštění JavaScriptu AcroForm s PDFium Component

Co byste s tím měli dělat v Delphi?

Pro dokumenty AcroForm je oprava jen dobrý zvyk: zavolejte ClearFormFieldFocus (nebo jinak přesuňte fokus jinam) před SaveAs, kdykoli byla hodnota nastavena programově, místo předpokladu, že pozdější interakce UI za vás potvrzení vyvolá. Pro dokument, který může být buď AcroForm nebo XFA — což je běžný případ v prohlížeči obecného účelu — zkontrolujte FormType nebo booleovskou hodnotu XFA dřív, než volajícímu slíbíte, že uložení vydrží, a přečtěte si detekci formulářů XFA a extrakci balíčků XFA pro plnou sadu sond, včetně případu XFAF, kde je obsah XFA vrstvený přes jinak obyčejné widgety AcroForm, které /V respektují

Pro skutečně dynamický formulář XFA, kde upravené hodnoty musí přežít uložení, není interaktivní editační buffer vůbec ten správný nástroj. Trvanlivá cesta je brát GetXfaDatasets jako svou základní linii, ne svůj výsledek: přečtěte ji jednou při otevření dokumentu, veďte si vlastní záznam toho, co uživatel změnil pole po poli — přesně ty hodnoty, které vaše UI už má, protože PDFium vám je dodatečně nevrátí — tyto sami zaplátujte do základního XML, a řiďte vlastní výstup. Zápis, který prochází přes XML, které řídí váš vlastní kód, přežije uložení, které buffer CPWL_Edit nikdy nepřež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;

Odhalení mezery dřív, než ji odhalí zákazník

TPdf.SaveAs vrátí True bez ohledu na to, zda hodnota pole XFA přežila, protože z pohledu PDFium se uložení skutečně podařilo — zapsalo každý bajt, o který bylo požádáno. To dělá z tohoto přesně ten druh defektu, který proklouzne kolem kouřového testu a dosáhne zákazníka: nic nevyvolá výjimku, nic se nezaloguje, soubor se otevře v pořádku, jen konkrétní hodnota je špatně. Test obousměrného převodu, který skutečně znovu otevře uložený soubor a porovná hodnotu pole — nebo porovná GetXfaDatasets před a po, podle dřívějšího příkladu — patří do regresní sady pro každý prohlížeč, který nechává uživatele upravovat obsah XFA, ne jen cesty AcroForm, které ve výchozím stavu náhodou fungují

Nic z tohoto není defekt k nahlášení proti PDFium Component tak, jako hranice, kolem které se má navrhovat: SetFocusedFormFieldText dělá přesně to, co jeho jméno říká pro oba modely formuláře, a rozdíl ve výsledku se čistě vysleduje k tomu, na co každý z AcroForm a XFA tento buffer na straně PDFium zapojuje. API, primitiva fokusu a uložení, a čtečky balíčků odkazované zde jsou součástí komponenty PDFium pro Delphi a C++Builder