HotPDF protahuje hodnoty multi-select list boxů FDF a XFDF tak, že hodnotu pole drží jako pole od začátku do konce. Od verze 2.755.0 zapisují ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF a ExportLoadedFormToXFDF každou vybranou volbu jako vlastní FDF řetězec nebo XFDF element <value> a odpovídající importní metody kontrolují každou hodnotu proti volbám pole a staví /I selection indexy znovu, než cokoli změní. Nic se cestou neslepuje do jednoho řetězce
Selhání, které tohle opravuje, se snadno zreprodukuje. Vezměte objednávkový formulář s multi-select list boxem produktových voleb, nechte uživatele vybrat dvě z nich, exportujte data formuláře pro back-office systém a pak naimportujte editovaný soubor zpátky do PDF. Před touhle změnou se list box vrátil prázdný nebo špatně. Důvodem je, že jedna z exportních hodnot obsahovala line break a stará cesta zploštila výběry do jediného řetězce odděleného řádky. Dostat z takového řetězce vícenásobné výběry zpátky nebylo nikdy spolehlivé a s exportní hodnotou, která sama obsahuje line break, to nemůže fungovat vůbec
Proč slepení multi-select hodnot line breaky rozbije round trip?
Slepení výběrů do jednoho řetězce zahodí hranice mezi hodnotami a hodnota může obsahovat oddělovač, takže žádný importer nedokáže řetězec správně rozdělit zpátky. ISO 32000-1 §12.7.4.4 dovoluje, aby položka /V choice pole byla buď jediný textový řetězec, nebo pole textových řetězců, a list box s příznakem MultiSelect (bit 22 z /Ff) používá formu pole, jakmile je vybrána víc než jedna volba. Tatáž sekce definuje /I jako pole 0-based indexů voleb ve vzestupném pořadí, které prohlížeče používají k rozlišení dvou voleb, které náhodou sdílejí exportní hodnotu. V HotPDF čte skalární getter GetFormFieldValue jen formu řetězce, takže puštění pole přes něj degradovalo export na prázdný řetězec a starý XFDF import slepoval opakované elementy <value> přes LF. Představte si volbu exportovanou jako Deep, line feed, Blue: po slepení může být Deep\nBlue\nRed dva výběry nebo tři a soubor nedává žádnou šanci zjistit které. Opravou bylo přestat uprostřed round tripu úplně používat skalár
Co obsahují exportované FDF a XFDF soubory?
HotPDF zapisuje multi-select hodnotu jako typované pole ve FDF a jako jeden element <value> na výběr v XFDF, takže hranice zůstávají viditelné na disku. Ve FDF si každá položka drží pravopis, jaký měla ve zdrojovém PDF: hexadecimální řetězce jdou ven jako hex a literal řetězce escapuje jediný helper, který převádí CR a LF na \r a \n. V XFDF nese kořen xml:space="preserve", jak vyžaduje ISO 19444-1, což znamená, že jakékoli bílé znaky uvnitř textového elementu se počítají jako data. HotPDF proto zapisuje start tag, escapovaný text a end tag každého <value> v kuse, drží odsazení mimo element a kóduje CR, LF a TAB jako character reference, takže XML parser aplikující normalizaci konců řádků nemůže změnit původní bajty
<!-- FDF: jeden typovaný array na formulářové pole -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF: jeden <value> na výběr -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
Dva exportní edge case stojí za znalost, než napíšete volající kód. Za prvé ExportLoadedFormToFDF staví kompletní FDF tělo v paměti, než vytvoří cílový soubor (opraveno v 2.755.1), takže hodnota, kterou nelze exportovat, jako array držící něco jiného než řetězce, vyhodí výjimku bez useknutí existujícího souboru. Za druhé je prázdný výběr na list boxu, který nabízí i exportní hodnotu prázdného řetězce, v XFDF nejednoznačný, protože <value/> může znamenat, že není vybráno nic, nebo že je vybrána prázdná volba. ExportLoadedFormToXFDF v tom případě vyhodí výjimku místo hádání a hází ji před otevřením cílového souboru. FDF takovou nejednoznačnost nemá, protože /V [] a /V [()] jsou odlišné. Oba FDF exportéři taky přeskočí widget-only terminály bez /T jména, stejně jako XFDF exportér, protože žádný importer by ty položky nikdy nespároval zpátky s polem
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// Multi-select list boxy se zapisují jako /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Prázdný výběr plus prázdná exportní volba: XFDF to nedokáže
// rozlišit a existující soubor .xfdf zůstává nedotčen
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
Jak validuje HotPDF multi-select hodnotu při importu?
HotPDF přijme importované pole jen když je cíl choice pole s nastaveným příznakem MultiSelect a každá hodnota v poli sedí na exportní hodnotu v /Opt poli toho pole. Každý slot volby se smí použít jednou, takže seznam se dvěma volbami sdílejícími exportní hodnotu b přijme [<62> <62>] jako dva odlišné výběry a odmítne třetí b. Znovu postavené /I následuje pořadí /Opt, ne pořadí příchozích hodnot, protože §12.7.4.4 vyžaduje vzestupné indexy. HotPDF staví nové /V a /I jako odpojené objekty a přiřazuje je až po tom, co každá hodnota prošla validací, takže odmítnutá hodnota nikdy nezanechá půlku pole ani zastaralé indexy. Kopie se zapisuje do pole, které se importuje, místo do sdíleného nadřazeného pole, hex pravopisy příchozí z FDF zůstávají hex i přes save a pole, jejichž výpočty závisí na list boxu, se označí pro přepočet. Pokud potřebujete jen nastavit jedinou hodnotu, nastavování jedné hodnoty formulářového pole v načteném PDF jde skalární cestou, která z principu neumí vícenásobné výběry
Některé jiné nástroje zapisují obyčejné ASCII exportní hodnoty jako hex řetězce bez byte order mark, například <416272>, a pak exportují XFDF tak, že ty hex číslice vypíšou jako text. Přísné literální srovnání na cestě zpátky selže a import se přeruší. Verze 2.755.1 přidává jeden retry: když hodnota nesedí na žádnou volbu, HPDFHexSpellingText dekóduje text jako hex payload a výsledek porovná znovu. Retry se vztahuje jen na vstup, který by jinak vyhodil výjimku, takže nikdy nezmění hodnotu, která už seděla. Týž release taky pořídil, aby skalární i array cesta používaly stejný Unicode dekodér, který rozumí PDFDocEncoding, UTF-16 s oběma byte order mark a UTF-8. Předtím mohla jedna logická hodnota sedět na jedné cestě a selhat na druhé v dokumentech míchajících kódování
Proč může validní FDF soubor během parsování přijít o pole?
FDF skener, který netrackuje hexadecimální řetězce, může rozpůlit slovník pole, když hex hodnota skončí hned vedle terminátoru slovníku. V << /T (region) /V <416273>> zavírá první > hex řetězec, ale naivní skener ho přečte spolu s dalším > jako konec slovníku a potichu zahodí pole. FDF importer na úrovni souboru už si vedl, jestli je uvnitř hex řetězce, a v 2.755.1 dělají totéž array a slovníkové skenery za ImportLoadedInterchangeFromFDF. Druhý problém se týká nepřímých referencí. Soubor FDF je malý dokument v PDF syntaxi s vlastním číslováním objektů (ISO 32000-1 §12.7.7), takže hodnota jako /V [11 0 R] odkazuje na objekt 11 souboru FDF, ne na objekt 11 PDF, které vyplňujete. Zjednodušený FDF parser v HotPDF neresolvuje reference uvnitř souboru, takže takové pole odmítne, místo aby četla cokoli, čeho je objekt 11 zrovna v cílovém dokumentu
Import ze souboru, streamu a XFDF hlásí chyby jinak
Všechny tři importní cesty validují stejně, ale selhání hlásí různě a stojí za to si jednu vybrat záměrně. ImportLoadedFormFromFDF přeskočí každé pole, které neprojde validací, a vrátí počet polí, která skutečně aplikovala, takže nižší počet, než čekáte, je jediným znakem problému. ImportLoadedInterchangeFromFDF a ImportLoadedFormFromXFDF vyhazují na prvním odmítnutém poli. Každé pole se commituje samostatně, takže pole zpracovaná před výjimkou si podrží nové hodnoty. Neber žádnou z nich jako transakci nad celým výměnným souborem: pokud potřebujete chování všechno-nebo-nic, zahoďte načtený dokument, když výjimka nastane, místo jeho uložení
var
Pdf: THotPDF;
Source: TMemoryStream;
Status: AnsiString;
Info: THPDFFDFInterchangeInfo;
begin
Pdf := THotPDF.Create(nil);
Source := TMemoryStream.Create;
try
Source.LoadFromFile('order-form-reviewed.fdf');
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
try
// Jen pole; hodnota mimo /Opt nebo cíl bez multi-select vyhodí výjimku
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
Pdf.SaveLoadedDocument('order-form-filled.pdf');
except
on E: Exception do
ShowMessage('Import rejected, nothing saved: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
Rozšiřování XFDF callbacků bez rozbíjení stávajících volajících
Podpora polí v nižší XFDF unitě bydlí v samostatném záznamu THPDFXFDFArrayAccess a v nových overloadech HPDFXFDFExportFields a HPDFXFDFImportFields, ne v dalších polích přidaných na konec stávajícího záznamu THPDFXFDFAccess. Důvodem je binární kompatibilita. Kód plnící THPDFXFDFAccess jako lokální proměnnou často nastaví jen sloty, o kterých ví, a zbytek nikdy nevynuluje, takže nový function pointer přidaný do toho záznamu by obsahoval stackový smetí a knihovna by ho vzala za skutečný callback. Se samostatným záznamem si starí volající drží starý layout a staré overloady a tyhle overloady předávají interně all-nil array záznam. Původní skalární importní overload pořád slepuje opakované hodnoty přes LF kvůli kompatibilitě a jen array-aware overload je drží rozebrané. Když bindujete vlastní datové úložiště, začněte od Default(THPDFXFDFArrayAccess). Z GetFormFieldValueArray vraťte True pro jakékoli pole s hodnotou seznamu, včetně takového s ničím nevybráným, a False pro spadnutí zpátky na skalární callback
uses HPDFXFDF;
// Holý function pointer, ne "of object": Context nese vaše vlastní úložiště
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
out Values: THPDFXFDFValueArray): Boolean;
begin
Result := TFormStore(Context).IsListField(FieldIndex);
if Result then
Values := TFormStore(Context).Selections(FieldIndex);
end;
procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
Access: THPDFXFDFAccess;
ArrayAccess: THPDFXFDFArrayAccess;
begin
Access := MakeStoreAccess(Store); // vaše stávající skalární bindingy
ArrayAccess := Default(THPDFXFDFArrayAccess); // každý nepoužitý slot je nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
Multi-select výměna funguje na list boxech, které už existují a mají nastavený bit MultiSelect v /Ff. Jak se choice pole a jejich bitové příznaky vůbec vytvářejí, viz přidávání ListBox a dalších AcroForm polí do načteného PDF. Komentář markup, který jde stromem <annots> XFDF, viz import a export XFDF anotací v HotPDF. Kompletní API reference a trial download jsou na stránce HotPDF Delphi PDF component