HotPDF проганяє значення multi-select list box через FDF і XFDF, тримаючи значення поля масивом від початку до кінця. З версії 2.755.0 ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF і ExportLoadedFormToXFDF пишуть кожен вибраний варіант як власний FDF-рядок чи XFDF-елемент <value>, а відповідні методи імпорту звіряють кожне значення з опціями поля і перебудовують індекси вибору /I, перш ніж що-небудь змінити. Нічого не склеюється в один рядок по дорозі
Збій, який це виправляє, легко відтворити. Візьміть форму замовлення з multi-select list box-ом продуктових опцій, дайте користувачеві вибрати дві з них, експортуйте дані форми для бекофісної системи, а потім імпортуйте відредагований файл назад у PDF. До цієї зміни list box повертався порожнім або неправильним. Причина в тому, що одне з export-значень містило розрив рядка, а старий шлях сплощив вибір в один рядок із розділенням по рядках. Витягнути кілька виборів назад із того рядка ніколи не було надійно, а з export-значенням, яке саме містить розрив рядка, це не може працювати взагалі
Чому склеювання multi-select значень розривами рядків ламає round trip?
Склеювання виборів в один рядок викидує межі між значеннями, а значення може містити розділювач, тож жоден імпортер не може правильно розрізати рядок назад. ISO 32000-1 §12.7.4.4 дозволяє запису /V choice-поля бути або одним текстовим рядком, або масивом текстових рядків, а list box із прапорцем MultiSelect (біт 22 у /Ff) використовує масивну форму, щойно вибрано більше одного варіанта. Той самий розділ визначає /I як масив індексів опцій з нуля в зростаючому порядку, яким переглядачі відрізняють два варіанти, що випадково поділяють export-значення. У HotPDF скалярний гетер GetFormFieldValue читає лише рядкову форму, тож прогнати масив крізь нього — значило звести експорт до порожнього рядка, а старий XFDF-імпорт склеював повторені елементи <value> через LF. Уявіть опцію, експортовану як Deep, розрив рядка, Blue: після склеювання Deep\nBlue\nRed може бути двома виборами чи трьома, і файл не дає способу дізнатися, котрого. Виправлення полягало в тому, щоб узагалі перестати використовувати скаляр посередині round trip
Що містять експортовані файли FDF і XFDF?
HotPDF пише multi-select значення як типізований масив у FDF і як один елемент <value> на вибір у XFDF, тож межі лишаються видимими на диску. У FDF кожен елемент тримає написання, яке мав у вихідному PDF: шістнадцяткові рядки виходять як hex, а літеральні рядки екрануються одним хелпером, який перетворює CR і LF на \r і \n. У XFDF корінь несе xml:space="preserve", як вимагає ISO 19444-1, що означає: будь-який пробіл усередині текстового елемента рахується даними. Тому HotPDF пише відкривальний тег, екранований текст і закривальний тег кожного <value> одним шматком, тримає відступи поза елементом і кодує CR, LF і TAB як символьні посилання, щоб XML-парсер, що застосовує нормалізацію кінців рядків, не міг змінити оригінальні байти
<!-- FDF: один типізований масив на поле -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF: один <value> на вибір -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
Дві граничні ситуації експорту варто знати, перш ніж писати код виклику. Перше: ExportLoadedFormToFDF будує повне FDF-тіло в пам'яті, перш ніж створити цільовий файл (виправлено в 2.755.1), тож значення, яке неможливо експортувати, як-от масив, що тримає щось крім рядків, підніме виняток, не обрізавши наявний файл. Друге: порожній вибір на list box-і, який ще й пропонує export-значення-порожній-рядок, двозначний у XFDF, бо <value/> може означати і те, що нічого не вибрано, і те, що вибрано порожню опцію. ExportLoadedFormToXFDF у тому випадку піднімає виняток, а не вгадує, і піднімає його до того, як цільовий файл відкрито. FDF такої двозначності не має, бо /V [] і /V [()] розрізняються. Обидва FDF-експортери також пропускають віджето-лише термінали без імені /T, як і XFDF-експортер, бо жоден імпортер ніколи не зміг би співставити ті записи з полем
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// Multi-select list box-и пишуться як /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Порожній вибір плюс порожня export-опція: XFDF не може відрізнити
// їх, а наявний .xfdf-файл лишається недоторканим
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
Як HotPDF валідує multi-select значення при імпорті?
HotPDF приймає імпортований масив лише коли ціль — choice-поле з установленим прапорцем MultiSelect і кожне значення в масиві збігається з export-значенням у масиві /Opt поля. Кожен слот опції можна використати один раз, тож список із двома опціями, що поділяють export-значення b, приймає [<62> <62>] як два різні вибори і відкидає третій b. Перебудований /I йде за порядком /Opt, а не за порядком вхідних значень, бо §12.7.4.4 вимагає зростаючих індексів. HotPDF будує нові /V і /I як від'єднані об'єкти і присвоює їх лише після того, як кожне значення пройшло валідацію, тож відкинуте значення ніколи не лишає по собі половини масиву чи застарілих індексів. Копія пишеться в поле, яке імпортується, а не в спільний батьківський масив, hex-написання, що приїжджають із FDF, лишаються hex крізь збереження, а поля, чиї обчислення залежать від list box-а, позначаються на перерахунок. Якщо вам треба лише задати одне значення, задання одного значення поля форми в завантаженому PDF іде скалярним шляхом, який за задумом не обробляє множинні вибори
Деякі інші інструменти пишуть звичайні ASCII export-значення як hex-рядки без byte order mark, наприклад <416272>, а потім експортують XFDF, викидаючи ті hex-цифри як текст. Суворе літеральне порівняння на зворотному шляху фейлиться, і імпорт переривається. Версія 2.755.1 додає один ретрай: коли значення не збігається з жодною опцією, HPDFHexSpellingText декодує текст як hex-payload і порівнює результат ще раз. Ретрай застосовується лише до входу, який інакше підняв би помилку, тож він ніколи не змінює значення, що вже збіглося. Той самий реліз також змусив скалярний і масивний шляхи використовувати той самий Unicode-декодер, який розуміє PDFDocEncoding, UTF-16 з будь-яким byte order mark і UTF-8. До того один логічний вал міг збігтися на одному шляху і провалитися на іншому в документах, що змішували кодування
Чому валідний FDF-файл досі може губити поля при розборі?
FDF-сканер, який не трекає шістнадцяткові рядки, може розрізати словник поля навпіл, коли hex-значення закінчується впритул до термінатора словника. У << /T (region) /V <416273>>> перший > закриває hex-рядок, але наївний сканер читає його разом із наступним > як кінець словника і тихо губить поле. Файловий FDF-імпортер уже трекав, чи він усередині hex-рядка, і в 2.755.1 сканери масивів і словників позаду ImportLoadedInterchangeFromFDF роблять те саме. Друге питання стосується непрямих посилань. FDF-файл — це маленький документ PDF-синтаксису з власною нумерацією об'єктів (ISO 32000-1 §12.7.7), тож значення на кшталт /V [11 0 R] посилається на об'єкт 11 FDF-файлу, а не на об'єкт 11 PDF, який ви заповнюєте. Спрощений FDF-парсер у HotPDF не розв'язує посилання всередині файлу, тож він відкидає такий масив замість читати те, чим об'єкт 11 опиниться в цільовому документі
Імпорти з файлу, потоку та XFDF звітують помилки по-різному
Три імпортні маршрути валідують однаково, але звітують збої по-різному, і варто свідомо вибрати один. ImportLoadedFormFromFDF пропускає будь-яке поле, що провалило валідацію, і повертає кількість полів, які справді застосував, тож число менше за очікуване — єдина ознака проблеми. ImportLoadedInterchangeFromFDF і ImportLoadedFormFromXFDF піднімають виняток на першому відкинутому полі. Кожне поле комітиться самостійно, тож поля, оброблені до винятку, тримають свої нові значення. Не трактуйте жоден із них як транзакцію над усім обмінним файлом: якщо вам потрібна поведінка все-або-нічого, викиньте завантажений документ, коли станеться виняток, замість зберігати його
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
// Лише поля; значення поза /Opt чи не-multi-select ціль піднімає помилку
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;
Розширення XFDF callback-ів без ламання наявних викликачів
Підтримка масивів у нижчому XFDF-юніті живе в окремому записі THPDFXFDFArrayAccess і в нових перевантаженнях HPDFXFDFExportFields і HPDFXFDFImportFields, а не в додаткових полях, доданих у кінець наявного запису THPDFXFDFAccess. Причина — бінарна сумісність. Код, що заповнює THPDFXFDFAccess як локальну змінну, часто ставить лише слоти, про які знає, і ніколи не очищає решту, тож новий покажчик на функцію, доданий у той запис, містив би сміття зі стеку, і бібліотека прийняла б його за справжній callback. З окремим записом старі викликачі тримають старе розташування і старі перевантаження, а ті перевантаження всередині передають запис масиву, весь у nil. Оригінальне скалярне перевантаження імпорту досі склеює повторені значення через LF заради сумісності, і лише масивно-обізнане перевантаження тримає їх окремо. Коли прив'язуєте власне сховище даних, стартуйте з Default(THPDFXFDFArrayAccess). Повертайте True з GetFormFieldValueArray для будь-якого поля зі значенням-списком, включно з полем, де нічого не вибрано, і False — щоб упасти назад у скалярний callback
uses HPDFXFDF;
// Звичайний покажчик на функцію, не "of object": Context несе ваше сховище
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); // ваші наявні скалярні прив'язки
ArrayAccess := Default(THPDFXFDFArrayAccess); // кожен невикористаний слот — nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
Multi-select-обмін працює на list box-ах, які вже існують і мають біт MultiSelect у /Ff. Про те, як choice-поля та їхні біти прапорців створюються в першу чергу, читайте в додаванні ListBox та інших полів AcroForm до завантаженого PDF. Про розмітку коментарів, що йде крізь дерево <annots> XFDF, — у імпорті-експорті анотацій XFDF у HotPDF. Повна довідка API і завантаження пробної версії — на сторінці HotPDF Delphi PDF component