PDFium Component ukládá upravené hodnoty formulářů XFA přesně, skrz uložení a opětovné otevření, když běží Windows V8 runtime pdfium.v8.dll dodávaný od v3.125.2 výš. Starší runtime přidávaly k hodnotám polí znaky nového řádku, stříhaly emoji na nesouvisející znak BMP, potichu vynechávaly ukládání single-stream XFA a mohly spolknout selhaný závěrečný zápis. Jeden příznak při opětovném otevření není vůbec vadou knihovny: dynamický formulář, jehož kořenový subform postrádá restoreState="auto", si znovu staví layout ze šablony
Bug reporty k tomu všemu vypadaly stejně. Zákazník vyplní XFA formulář hlášení v prohlížeči v Delphi, uloží, znovu otevře a něco je mírně vedle. Prázdný box poznámek teď drží prázdný řádek a po druhém uložení drží dva. Jméno napsané s emoji se vrací s glyfem z private use area. Nikdo nedostane chybu, a přesně proto jsou tyhle bugy drahé: drift se vynoří za pár týdnů v exportu někoho jiného
Co se kazí, když se formulář XFA uloží a znovu otevře?
Čtyři oddělené vady v nativní cestě ukládání XFA způsobovaly drift hodnot a každá se skrývala za uložením, které vypadalo úspěšně. Dvě přišly ze serializace, jedna z úložného layoutu single-stream a jedna ze samotného PDF zapisovače. Tabulka mapuje každý příznak na příčinu a na vydání, kde ho PDFium Component opravilo
| Příznak po znovuotevření | Příčina | Opraveno ve |
|---|---|---|
| Prázdné pole drží znak nového řádku; hodnoty přibírají LF na každé uložení | Oba XFA zapisovače vkládaly layoutové nové řádky za start tagy | v3.125.2, pdfium.v8.dll |
| U+1F642 se vrací jako U+F642, nebo emoji zmizí z form paketu | 16bitové zkracování wchar_t při dekódování; filtrování surrogateů ve form serializeru | v3.125.2, pdfium.v8.dll |
| Úpravy v dokumentu single-stream XFA prostě zmizí | Nativní uložení odmítlo streamový layout, ale návratová hodnota se ignorovala | v3.125.2; komentáře a processing instructions se udrží od v3.126.0 |
| Useknutý soubor, i když uložení ohlásilo úspěch | Závěrečný bufferovaný zápis selhal poté, co zapisovač už vrátil úspěch | v3.125.2 V8 runtime; v3.125.3 obyčejné pdfium.dll |
| Třístránkový dynamický formulář se otevře jako dvoustránkový | Kořenový subform nežádá restoreState="auto" | Tvorba formuláře, ne vada knihovny |
Dřívější rozepisy došly k závěru, že úpravy polí XFA se s PDFium uchovat nedají, což pro runtime té doby sedělo. Novější V8 runtime ukládá hodnoty XFA nativně, takže úprava udělaná v živém formuláři dorazí do uloženého datasets paketu bez packet chirurgie na vaší straně
Který PDFium runtime ukládá hodnoty XFA?
Věrnost ukládání XFA závisí na nativním DLL, ne na wrapperu Delphi, takže první kontrola je, který runtime váš proces doopravdy načetl. PDFium Component dodává dva Windows buildy na architekturu: obyčejné pdfium.dll, postavené bez V8 a XFA, a pdfium.v8.dll, které nese JavaScript engine a XFA form runtime. Jen pdfium.v8.dll dokáže spustit formulář XFA, takže všechny zde popsané opravy XFA bydlí tam, počínaje znovu postavenými knihovnami Win32 a Win64 V8 ve v3.125.2
Oprava závěrečného zápisu je obecný kód PDF zapisovače, takže má smysl i pro obyčejné dokumenty. v3.125.3 znovu postavila obyčejné knihovny pdfium.dll, aby nesly tutéž záplatu. Sdílený zdroják není důkazem sdíleného chování: dokud se binárka znovu nepostaví, staré DLL drží starý bug
Druhá past seděla v loaderu. Před v3.125.2 donutilo nastavení EnableV8Engine na True vazbu, aby vybralo výchozí název pdfium.v8.dll a ignorovalo plnou cestu v LibraryName. Aplikace mířící na čerstvě nasazený runtime mohla dál načítat starší kopii z jiné složky. Od v3.125.2 vybere LibraryName obsahující adresář přesně tenhle soubor v obou engine módech a chybějící cesta selže místo pádu na jinou přibalenou knihovnu
uses
System.SysUtils, PDFium;
procedure SelectXfaRuntime;
begin
// Adresář v LibraryName přibije přesně tento soubor (v3.125.2 a výš);
// chybí-li soubor, načítání hodí výjimku místo pádu na zálohu
{$IFDEF WIN64}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win64\pdfium.v8.dll';
{$ELSE}
PDFium.LibraryName := ExtractFilePath(ParamStr(0)) + 'DLLs\Win32\pdfium.v8.dll';
{$ENDIF}
PDFium.EnableV8Engine := True;
PDFium.LoadLibrary; // selhej při startu, ne při prvním uložení
end;
Po otevření dokumentu vám TPdf.XFA řekne, že soubor obsahuje XFA, a TPdf.XfaRuntimeAvailable, že načtené DLL ho dokáže doopravdy vykonat. Potřebujete-li navíc rozeznat statické a dynamické formuláře, TPdf.FormType vrací ftXfaFull nebo ftXfaForeground; článek rozpoznávání formulářů XFA a extrakce XFA paketů v Delphi pokrývá tohle sondování detailně
Proč uložená pole XFA přibírají znaky nového řádku?
Uložená pole XFA přibírala znaky nového řádku, protože oba nativní XFA zapisovače, obecný zapisovač XML elementů i serializer form paketu, tiskly svůj výstup s novým řádkem za start tagy jako pretty-print. Ve většině XML je ten bílý prostor kosmetika. V datech XFA ne: až se datasets paket znovu parsuje, je text mezi <Comments> a </Comments> hodnotou pole, nový řádek včetně. Prázdné pole se proto otevřelo s jediným LF a každý další cyklus uložení a otevření mohl přidat další
Zjevná záplata, seřezávat hodnoty při načtení, by byla špatně. Uživatelé píší do polí XFA úvodní mezery, koncové mezery a záměrně víceřádkový text a blok adresy nebo kód s pevnou šířkou musí přežít bajt po bajtu. Oprava ve v3.125.2 proto odstraňuje jen bílý prostor, který kolem tagů syntetizoval sám serializer. Uživatelské hodnoty, stávající textové uzly a CDATA sekce projdou nedotčeny, takže " odsazené" zůstane odsazené a úmyslně prázdné pole zůstane prázdné
Proč se emoji vrací jako jiný znak?
Emoji se vracelo špatně, protože Windows wchar_t je široký 16 bitů a dvě dekódující cesty ukládaly plnou Unicode skalární hodnotu do jediného wchar_t. Dělali to dekodér streamu UTF-8 i parser číselných znakových referencí jako 🙂. U+1F642, lehce se usmívající obličej, se do 16 bitů nevejde, takže vysoké bity spadly a místo něj se objevilo U+F642: kódový bod v Private Use Area, který většina fontů vykreslí jako obdélníček nebo vůbec nic
Form serializer měl opačný problém. Filtroval znaky po jednom wchar_t, viděl dvě surrogate kódové jednotky, které jsou izolovaně neplatné, a obe zahodil, takže emoji z form paketu zmizelo úplně. Ve v3.125.2 konzumuje dekodér každou skalární hodnotu celou a vypouští korektní surrogate pár. Zbývá-li jen jeden výstupní slot, drží nízký surrogate v čekání a nehlásí konec streamu, dokud je ta jednotka v bufferu. Sekvence UTF-8 rozsekaná mezi čtecí bloky se přenáší do dalšího čtení místo aby se zahodila. Form exporter teď drží platné surrogate páry pohromadě a číselné znakové reference produkují správné páry taky
Testovací data Latin-1 tohle nikdy neukážou, takže každý round-trip test XFA potřebuje aspoň jeden znak z doplňkové roviny
Single-stream XFA a chyby ukládání, které nikdo neviděl
Dokument single-stream XFA ztrácel úpravy, protože nativní pomocník ukládání odmítl ten úložný layout a jeho volající selhání ignoroval. ISO 32000-1 §12.7.8 dovoluje, aby položka /XFA slovníku interaktivního formuláře byla buď polem názvů paketů a streamů, nebo jediným streamem držícím celý dokument XDP. Pole paketů je běžný případ, ale jediný stream je úplně legální a PDF uložení doběhlo, jako by se nic nestalo, zatímco data formuláře zůstala na starých hodnotách
Od v3.125.2 obsluhuje V8 runtime podporovanou podmnožinu single-stream. Nejdřív vyexportuje oba živé pakety, datasets a form, do přípravné oblasti (staging) a zvaliduje je a teprve pak vymění odpovídající pakety v původním XDP. Ostatní pakety a deklarace kořenového namespace zůstávají. Selže-li staging, perzistentní XFA stream se nikdy nedotkne a dokument si drží značku změny
XML komentáře a processing instructions si žádaly zvýšenou péči, protože interní XML DOM je zahazuje. Ve v3.125.2 znamenala jejich přítomnost, že ukládání selže rovnou, místo aby potichu ztratilo obsah. v3.126.0 je uchovává: před parsováním se každý komentář nebo processing instruction vymění za marker postavený z prefixu, který se v původním textu nevyskytuje nikde. Poté, co se živé pakety vymění, se musí každý marker objevit přesně jednou, dřív než se obnoví původní token a stream zapíše. Tokeny mimo vyměněné pakety si tak drží text i pořadí, včetně tokenů v prologu, šabloně a dalších paketech
Některé vstupy se pořád odmítají záměrně a každé odmítnutí je explicitní selhání uložení:
- Komentáře nebo processing instructions uvnitř živých paketů
datasetsneboform, protože jejich původní pozice se nedají namapovat do čerstvě exportovaného obsahu - Deklarace DTD a podpisy XMLDSig, protože přepis XDP nedokáže udržet XML podpis platný
- Neplatné kódování UTF-8 nebo UTF-16, nedokončené tagy, neplatné znakové reference, neznámé entity a deformované processing instructions, které se odmítají místo tiché opravy
Výstup single-stream je UTF-8 a zachovává XML obsahový model, nikoli původní bajtové rozložení ani deklaraci kódování
Poslední vada seděla pod XFA. Nativní souborový zapisovač bufferuje výstup v blocích po 32 KB a vypláchl závěrečný neúplný blok jen ve svém destruktoru, poté, co dokumentový zapisovač už ohlásil úspěch. Chyba plného disku nebo I/O na tom posledním bloku byla volajícímu neviditelná. Od v3.125.2 ve V8 runtime a v3.125.3 v obyčejném runtime je tohle závěrečné vypláchnutí součástí výsledku ukládání a značka změny XFA se maže jen po skutečném úspěchu. Ze strany Delphi zapisuje TPdf.SaveAs(const FileName: string; Option: TSaveOption = saNone; PdfVersion: TPdfVersion = pvUnknown): Boolean do dočasného souboru vedle cíle a přemístí ho na místo jen tehdy, když uložení vrátí True, takže selhané uložení nechá předchozí soubor v celku
Proč se dynamický formulář XFA otevře s méně stránkami?
Dynamický formulář XFA se otevře s méně stránkami, když jeho kořenový subform nedeklaruje restoreState="auto", a to je rozhodnutí při tvorbě formuláře, nikoli vada PDFium Component. V XFA 3.3 má restoreState na kořenovém subformu výchozí hodnotu manual. Pod manual obnovuje XFA procesor jen omezený stav z uloženého form paketu a zbytek nechává autorovým skriptům. Uložené hodnoty polí a počty instancí opakovaných subformů se vrátí, ale geometrické vlastnosti nastavené za běhu ne
Případ, který to vystavil na odiv, byl třístránkový formulář, jehož skript zvětšil subform na h="450pt". Uložený form paket držel novou výšku, hodnoty i počty instancí. Při opětovném otevření se ale layout znovu postavil z výšek šablony a formulář se přelil na dvě stránky. Měl runtime pravdu: šablona nikdy nežádala automatické obnovení. Deklarovat ho na kořenovém subformu opraví znovuotevření:
<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form1" layout="tb" restoreState="auto">
<pageSet>
<pageArea name="Page1">
<contentArea x="0.25in" y="0.25in" w="8in" h="10.5in"/>
<medium stock="letter"/>
</pageArea>
</pageSet>
<subform name="Details" layout="tb" w="7.5in">
<!-- pole; skripty mohou za běhu měnit h nebo přidávat instance -->
</subform>
</subform>
</template>
Nevlastníte-li šablonu, nezáplatovávejte ji v prohlížeči: formulář, který spoléká na mód manual, čeká, že stav znovu postaví jeho vlastní skripty. Živé přestránkování, zatímco uživatel píše, je zvláštní téma, pokryté v jak PDFium Component sleduje počty stránek a přemístěná pole dynamického XFA
Jak ověříte uložení XFA v Delphi?
Jediná spolehlivá kontrola uložení XFA je znovu otevřít uložený soubor v čerstvé instanci TPdf a přečíst uložená data zpět. TPdf.GetXfaDatasets vrací datasets paket tak, jak je v dokumentu uložený, ne živý datový model XFA, takže volání před uložením ukazuje staré hodnoty. Po znovuotevření ukazuje přesně to, co se zapsalo. Dokument single-stream nemá samostatně pojmenované pakety: PDFium hlásí celé XDP jako jeden paket s prázdným názvem, takže GetXfaPacketByName('datasets') i GetXfaDatasets nevrací nic a záloha čte celý stream přes GetXfaFormPackets
uses
System.SysUtils, PDFium, FPdfXfa;
function ReadSavedXfaData(const FileName: string): string;
var
Pdf: TPdf;
Packets: TXfaPacketList;
Bytes: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Bytes := Pdf.GetXfaDatasets; // layout s polem paketů
if Length(Bytes) = 0 then
begin
Packets := Pdf.GetXfaFormPackets; // single stream: jeden nepojmenovaný paket
if Length(Packets) = 1 then
begin
SetLength(Bytes, Length(Packets[0].Content));
if Length(Bytes) > 0 then
Move(Packets[0].Content[0], Bytes[0], Length(Bytes));
end;
end;
Result := TEncoding.UTF8.GetString(Bytes); // uložený výstup XDP je UTF-8
finally
Pdf.Free;
end;
end;
Ukládací rutina pak potvrdí nevyřízenou úpravu, zkontroluje výsledek SaveAs a porovná znovu otevřenou hodnotu. TPdf.ClearFormFieldFocus zabije focus formuláře, což je okamžik, kdy PDFium potvrzuje edit buffer zaměřeného pole. TPdf.SetFocusedFormFieldText(const Value: WString): Boolean naplní zaměřené pole programově, ale spoléká se na focus, který wrapper sleduje přes FocusFormField, jenž projde anotacemi widgetů. Dynamická stránka XFA normálně žádné nemá, takže tam text obvykle přichází klávesnicí přes TPdfView a funkce vrací False, když nemá sledované pole focus
function XmlText(const S: string): string;
begin
Result := StringReplace(S, '&', '&', [rfReplaceAll]);
Result := StringReplace(Result, '<', '<', [rfReplaceAll]);
end;
procedure SaveXfaAndVerify(Pdf: TPdf; const FileName, FieldTag,
Expected: string);
var
Saved: string;
begin
// Volitelné skriptované vyplnění; False znamená, že žádné sledované pole nemá focus
if (Pdf.FocusedFormFieldIndex >= 0) and
not Pdf.SetFocusedFormFieldText(Expected) then
raise EPdfError.Create('Could not write the focused field');
Pdf.ClearFormFieldFocus; // potvrdí edit buffer
if not Pdf.SaveAs(FileName) then // včetně závěrečného flush (v3.125.2+)
raise EPdfError.CreateFmt('Saving %s failed', [FileName]);
Saved := ReadSavedXfaData(FileName);
if Pos('<' + FieldTag + '>' + XmlText(Expected) + '</' + FieldTag + '>',
Saved) = 0 then
raise EPdfError.CreateFmt('%s did not survive the round trip', [FieldTag]);
end;
Test podřetězcem berte jako kouřový test. Prázdný element se může serializovat jako <Tag/>, atributy se můžou objevit na datových elementech a escapování nad rámec & a < je volba serializeru. Pro produkční kontroly načtěte znovu otevřené XML skutečným XML parserem a porovnejte textový uzel vázaného datového elementu. Pouštějte kontrolu i dvakrát za sebou, protože vada nového řádku ukázala svůj plný tvar až ve druhé generaci
Rychlý přehled: kontrolní seznam věrnosti ukládání XFA
- Nasazujte
pdfium.v8.dllz v3.125.2 a výš pro formuláře XFA a v3.125.3 a výš pro obyčejnépdfium.dll, aby oprava závěrečného zápisu byla v obou - Miřte
LibraryNamena plnou cestu a nastavteEnableV8Enginena True; chybějící cesta selže místo načtení jiné kopie - Po otevření dokumentu potvrďte
TPdf.XFAaTPdf.XfaRuntimeAvailable - Volejte
ClearFormFieldFocuspředSaveAs, aby se zaměřené pole potvrdilo - Nikdy neignorujte Boolean výsledek
SaveAs; výsledek False nechává předchozí soubor na místě - Ověřujte znovuotevřením v novém
TPdfa čtenímGetXfaDatasets, se zálohou naGetXfaFormPacketspro single-stream XFA - Testujte s prázdnými hodnotami, úvodními mezerami, víceřádkovým textem,
&a znakem z doplňkové roviny, napříč dvěma generacemi ukládání - Čekejte explicitní selhání uložení pro DTD, XMLDSig a komentáře uvnitř živých paketů single-stream XFA
- Ztrácí-li dynamický formulář za běhu získanou geometrii při opětovném otevření, zkontrolujte na kořenovém subformu
restoreState="auto", dřív než začnete podezírat knihovnu
Strukturu callbacků, kterou XFA runtime od hostitelské aplikace čeká, popisuje FPDF_FORMFILLINFO verze 2 a XFA ABI v Delphi. V8 runtime, wrapper Delphi a C++Builder i ovládací prvek prohlížeče jsou součástí PDFium Component for Delphi and C++Builder, který zahrnuje oba Windows runtime pro Win32 i Win64