HotPDF гоняет значения мультиселект-листбокса через FDF и XFDF, удерживая значение поля массивом от начала до конца. Начиная с версии 2.755.0 ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF и ExportLoadedFormToXFDF пишут каждую выбранную опцию собственной FDF-строкой или XFDF-элементом <value>, а парные методы импорта сверяют каждое значение с опциями поля и перестраивают индексы выбора /I, прежде чем что-то менять. Ничего по пути не склеивается в одну строку
Сбой, который это чинит, легко воспроизвести. Возьмите форму заказа с мультиселект-листбоксом опций продукта, дайте пользователю выбрать две из них, экспортируйте данные формы для бэк-офисной системы, затем импортируйте отредактированный файл обратно в PDF. До этого изменения листбокс возвращался пустым или неверным. Причина в том, что одна из экспортных значений содержала перевод строки, а старый путь сплющил выбор в одну строку с разделителями-переводами строк. Достать несколько выборов обратно из такой строки никогда не было надёжно, а с экспортным значением, которое само содержит перевод строки, это не работает в принципе
Почему склейка значений мультиселекта переводами строк ломает round trip?
Склейка выборов в одну строку выбрасывает границы между значениями, а значение может содержать разделитель, так что ни один импортёр не сможет правильно разрезать строку обратно. ISO 32000-1 §12.7.4.4 разрешает записи /V choice-поля быть либо единственной текстовой строкой, либо массивом текстовых строк, а листбокс с флагом MultiSelect (бит 22 в /Ff) использует массив, как только выбрано больше одной опции. Та же секция определяет /I как массив индексов опций с нуля в возрастающем порядке, которым вьюверы различают две опции, случайно разделившие экспортное значение. В HotPDF скалярный геттер GetFormFieldValue читает только строковую форму, так что прогон массива через него вырождал экспорт в пустую строку, а старый XFDF-импорт склеивал повторные элементы <value> через LF. Представьте опцию, экспортированную как Deep, перевод строки, Blue: после склейки Deep\nBlue\nRed — это два выбора или три, и файл не даёт способа узнать. Починка состояла в том, чтобы вовсе перестать использовать скаляр в середине round trip
Что содержат экспортированные файлы FDF и XFDF?
HotPDF пишет значение мультиселекта типизированным массивом в 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), так что значение, которое нельзя экспортировать, — например массив с чем-то кроме строк — поднимает исключение, не обрезав существующий файл. Второе: пустой выбор на листбоксе, который вдобавок предлагает экспортное значение в виде пустой строки, двусмыслен в 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
// Мультиселект-листбоксы пишутся как /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Пустой выбор плюс пустая экспортная опция: XFDF не различит
// их, а существующий .xfdf останется нетронутым
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
Как HotPDF валидирует значение мультиселекта при импорте?
HotPDF принимает импортированный массив, только когда цель — choice-поле с выставленным флагом MultiSelect и каждое значение в массиве совпадает с экспортным значением из массива /Opt поля. Каждый слот опции расходуется один раз, поэтому список с двумя опциями, разделившими экспортное значение b, принимает [<62> <62>] как два различных выбора и отвергает третий b. Перестроенный /I следует порядку /Opt, а не порядку входящих значений, поскольку §12.7.4.4 требует возрастающих индексов. HotPDF строит новые /V и /I как отсоединённые объекты и присваивает их только после того, как каждое значение прошло валидацию, так что отвергнутое значение никогда не оставит полмассива или протухших индексов. Копия пишется в импортируемое поле, а не в общий родительский массив, hex-написания, прибывшие из FDF, остаются hex насквозь до сохранения, а поля, чьи вычисления зависят от листбокса, помечаются к пересчёту. Если нужно задать лишь одно значение, задание одного значения поля формы в загруженном PDF идёт через скалярный путь, который по дизайну не умеет несколько выборов
Некоторые другие инструменты пишут простые ASCII экспортные значения hex-строками без метки порядка байтов, например <416272>, а затем экспортируют XFDF, выписывая эти hex-цифры текстом. Строгое литеральное сравнение на обратном пути проваливается, и импорт прерывается. Версия 2.755.1 добавляет один ретрай: когда значение не совпало ни с одной опцией, HPDFHexSpellingText декодирует текст как hex-нагрузку и сравнивает результат снова. Ретрай применяется только к входу, который иначе поднял бы исключение, так что уже совпавшее значение он никогда не меняет. Тот же релиз заставил скалярный и массивный пути пользоваться одним Unicode-декодером, понимающим PDFDocEncoding, UTF-16 с любой меткой порядка байтов и 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 или немультиселектная цель поднимет исключение
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-колбэки, не ломая существующих вызывающих
Поддержка массивов в низкоуровневом XFDF-юните живёт в отдельной записи THPDFXFDFArrayAccess и в новых оверлоудах HPDFXFDFExportFields и HPDFXFDFImportFields, а не в дополнительных полях, дописанных в конец существующей записи THPDFXFDFAccess. Причина — бинарная совместимость. Код, заполняющий THPDFXFDFAccess как локальную переменную, часто выставляет только известные ему слоты и никогда не чистит остальные, так что новый указатель на функцию, дописанный в ту запись, содержал бы мусор со стека, и библиотека приняла бы его за настоящий колбэк. С отдельной записью старые вызывающие сохраняют прежнюю раскладку и прежние оверлоуды, а те оверлоуды внутри передают полностью нулевую запись массива. Исходный скалярный оверлоуд импорта по-прежнему склеивает повторные значения через LF ради совместимости, и только array-aware оверлоуд держит их раздельно. Привязывая собственное хранилище данных, стартуйте с Default(THPDFXFDFArrayAccess). Возвращайте True из GetFormFieldValueArray для любого поля со списочным значением, включая и то, где ничего не выбрано, и False для отката к скалярному колбэку
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;
Обмен мультиселектом работает на листбоксах, которые уже существуют и держат бит MultiSelect в /Ff. О том, как choice-поля и их флаговые биты создаются в принципе, — в статье о добавлении ListBox и прочих полей AcroForm в загруженный PDF. О разметке комментариев, идущей через дерево <annots> XFDF, — в статье о импорте и экспорте аннотаций XFDF в HotPDF. Полный справочник API и пробная версия — на странице HotPDF Delphi PDF component