Экспортное значение листбокса из Deep, перевода строки и Blue — совершенно легальный PDF, и второй вариант под именем Red тоже. Выберите оба, сплющите выбор в одну LF-разделённую строку для файла обмена, и дальняя сторона получит Deep\nBlue\nRed: два выбора или три — байты об этом не говорят. Значения мультиселекта возвращаются из FDF или XFDF целыми, только пока они всю дорогу остаются массивами: с момента выхода из PDF до момента записи обратно
Почему склейка переводом строки никогда не донесёт мультиселект-значение?
Склеенная строка теряет границы между значениями, а поскольку значение имеет право содержать разделитель, ни один импортёр не соберёт их обратно. ISO 32000-1 §12.7.4.4 позволяет записи /V поля выбора хранить либо одну текстовую строку, либо массив текстовых строк; листбокс с флагом MultiSelect (бит 22 в /Ff) переходит на форму массива, как только выбрано больше одного варианта. Та же секция определяет /I — массив нулевых индексов вариантов по возрастанию, по которому просмотрщик различает два варианта с общим экспортным значением. Ни одна из этих структур не пролезает через строковый API. GetFormFieldValue в HotPDF возвращает AnsiString и читает строковую форму /V, поэтому любой экспорт поверх неё может записать лишь один скаляр на поле
У импорта проблема зеркальная. XFDF пишет каждый выбор собственным элементом <value>, и импортёру, скармливающему их скалярному сеттеру, сперва приходится склеивать их — обычно через LF. Для варианта Deep/Blue выше этот клей неотличим от данных. Поэтому HotPDF держит скаляры подальше от середины round trip: ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF и ExportLoadedFormToXFDF читают массив прямо из поля, а парные методы импорта строят новый массив, ни разу не пройдя через склеенную строку
Что содержат экспортированные файлы FDF и XFDF
На диске границы остаются видимыми. В FDF мультиселект-поле становится типизированным массивом, и каждый элемент сохраняет написание из исходного PDF: hex-строки выходят как hex, литеральные строки — как литералы с экранированными CR и LF в виде \r и \n. В файле, записанном ExportLoadedFormToFDF, rich-text /RV или ненулевой локальный /Ff поля едут в том же словаре поля. В XFDF корневой элемент объявляет xml:space="preserve", что требует ISO 19444-1 и что превращает каждый пробельный символ внутри текстового элемента в данные. Это ловушка для красивой печати. HotPDF пишет открывающий тег, экранированный текст и закрывающий тег каждого <value> в одну строку, держит отступы вне элемента и кодирует CR, LF и TAB как character references, чтобы XML-парсер, применяющий нормализацию концов строк, не смог переписать пару CR LF в одинокий LF
<!-- 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 в памяти и лишь затем создаёт целевой файл, поэтому значение, которое нельзя экспортировать (массив с чем-то кроме строк, например), поднимает исключение, пока предыдущий .fdf на диске ещё цел. Второй случай — настоящая дыра самого XFDF: пустой выбор на листбоксе, который к тому же предлагает экспортное значение пустой строки. <value/> может означать и «ничего не выбрано», и «выбрана пустая опция», и третьего написания у формата нет. ExportLoadedFormToXFDF в такой ситуации поднимает исключение до открытия целевого файла. У FDF такой проблемы нет, потому что /V [] и /V [()] — разные объекты. Все три экспортёра пропускают widget-only терминалы без имени /T, ведь ни один импортёр всё равно не смог бы сопоставить такую запись с полем
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;}}
Как входящий выбор сверяется с /Opt?
HotPDF принимает входящий массив, только если цель — поле выбора с выставленным флагом MultiSelect и каждое значение совпадает с экспортным значением из массива /Opt поля (для пары опций из двух элементов — первый элемент). Каждый слот опции можно потратить один раз. Список с двумя вариантами, экспортирующими b, поэтому примет [<62> <62>] как два различных выбора и отвергнет третий b: свободного слота для него не осталось. Перестроенный /I следует порядку /Opt независимо от порядка прихода значений, что держит индексы восходящими, как требует §12.7.4.4. Новые /V и /I строятся как отсоединённые объекты и присваиваются лишь после того, как все значения прошли, поэтому отвергнутое значение не может оставить на поле полмассива или протухшие индексы
После присваивания поле получает свежий appearance stream. Этот шаг легко забыть в самодельном импортёре, и симптом сбивает с толку: просмотрщики рисуют сохранённый /AP, а не /V, так что старый выбор остаётся на экране и на печати, хотя данные изменились. Листбокс также попадает в набор изменённых полей, а значит вызов RecalculateLoadedFormFieldsIncremental без параметров перезапустит ровно те вычисления, которые его читают. Hex-написания, пришедшие из FDF, остаются hex до сохранения. Если нужно выставить лишь одно скалярное значение, установка одного значения поля формы в загруженном PDF описывает этот путь, который по дизайну не занимается множественными выборами
Один входной паттерн заслуживает особой обработки. PDF может хранить простое ASCII экспортное значение в виде hex-строки без маркера порядка байтов, <416272> для Abr, и экспортёр, копирующий тело PDF-строки дословно, выдаёт XFDF, чей текст элемента — сырые цифры 416272, а не декодированный текст. XFDF, несущий hex-закодированное экспортное значение сырыми цифрами, не может свериться с /Opt по написанию: строгое сравнение не находит опции с именем 416272, и импорт проваливается. Получив такой файл, XFDF-импортёр HotPDF один раз повторяет попытку для несовпавшего значения, декодируя его как hex-нагрузку. Повтор намеренно узкий. Он запускается только для значения, вообще не имеющего точного совпадения; каждое точное совпадение резервируется первым, поэтому спасённое написание никогда не займёт слот опции, который понадобится позднему значению с точным написанием; а в сохранённом /V лежит собственная PDF-строка опции, так что /V и /I описывают один и тот же выбор. Это правильная доза снисходительности: срабатывает лишь там, где импорт иначе поднял бы исключение, и совпавшее значение никогда не перетолковывается. Само сопоставление идёт через один Unicode-декодер, понимающий PDFDocEncoding, UTF-16 с любым маркером порядка байтов и UTF-8, поэтому один и тот же логический текст совпадает с одной и той же опцией даже в документах со смешанными кодировками
Две ловушки разбора FDF: hex-терминаторы и чужие номера объектов
Первая ловушка лексическая. В << /T (region) /V <416273>>> первый > закрывает hex-строку, а следующие два — словарь. Сканер, ищущий только пары >>, закончит словарь на символ раньше и потеряет поле. FDF-импортёр HotPDF файлового уровня отслеживает, внутри ли он hex-строки или литерала, пока нарезает массив /Fields, а потоковый ридер за ImportLoadedInterchangeFromFDF токенизирует весь файл как синтаксис PDF, так что оба видят три закрывающие скобки там, где наивный скан видит две
Вторая ловушка касается ссылок. Файл FDF — маленький документ синтаксиса PDF с собственной нумерацией объектов (ISO 32000-1 §12.7.7), поэтому /V [11 0 R] означает объект 11 файла FDF, а не объекта 11 заполняемого PDF. Разрешение его против целевого документа тихо втащит тот объект, который в нём носит этот номер. В сборках Delphi 2009 и позже interchange-ридер декодирует FDF как полный граф объектов, идёт по ссылке внутри этого графа, валидирует полученные строки как любой встроенный массив и поднимает исключение, если объекта 11 в FDF нет. Файловый ImportLoadedFormFromFDF читает каждый словарь поля сам по себе, без объектной таблицы FDF; массив ссылок проваливает его проверку «только строки», и поле пропускается
Выбор пути импорта: пропустить, поднять или откатить
Все три пути импорта валидируют мультиселект-значения одинаково. Различаются они в том, что происходит при отказе значения. ImportLoadedFormFromFDF пропускает любое провалившее поле и возвращает число применённых полей, так что счёт меньше числа отправленных записей — единственный признак беды. ImportLoadedFormFromXFDF и ImportLoadedInterchangeFromFDF поднимают исключение на первом отвергнутом поле, и каждый вызов идёт одной транзакцией по загруженному документу: когда исключение покидает метод, каждое поле (а для interchange-пути каждая аннотация), которое вызов уже успел изменить, откатывается. Interchange-импорт, вернувший False, тоже откатывается, со сбросом Status и информационной записи
Для данных, которые человек просмотрел и отправил обратно, используйте один из поднимающих путей. Полуприменённая форма заказа хуже отвергнутой: в сохранённом PDF ничто не покажет, какие листбоксы пришли из просмотренного файла, а какие сохранили старые значения. Поведение «пропусти и посчитай» у ImportLoadedFormFromFDF подходит для другой работы: заполнение по мере возможностей из FDF, который могли сгенерировать для чуть другой ревизии формы
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
begin
// Перезапустить только вычисления, читающие импортированные поля
Pdf.RecalculateLoadedFormFieldsIncremental;
Pdf.SaveLoadedDocument('order-form-filled.pdf');
end;
except
on E: Exception do
ShowMessage('Import rejected, document unchanged: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
Подключение собственного хранилища к XFDF-коллбэкам массива
Модуль HPDFXFDF, конвертирующий между XFDF и любым хранилищем полей, описываемым коллбэками, несёт поддержку массива в отдельной записи THPDFXFDFArrayAccess и в дополнительных перегрузках HPDFXFDFExportFields и HPDFXFDFImportFields. Дописать новые слоты в существующую запись THPDFXFDFAccess было бы короче и неверно. Вызывающие часто объявляют ту запись локальной переменной и заполняют лишь знакомые слоты, так что новый указатель функции в конце держал бы мусор стека, который библиотека приняла бы за коллбэк. С отдельной записью существующий код сохраняет раскладку и перегрузки, а те перегрузки внутренне передают запись массива со всеми слотами nil
Две импортные перегрузки ведут себя по-разному при повторных элементах <value>. Исходная скалярная перегрузка по-прежнему склеивает их через LF и вызывает SetFormFieldValue, потому что существующие вызывающие зависят от этой конвенции. Перегрузка с поддержкой массива никогда не склеивает: она отдаёт список SetFormFieldValueArray, падает к скалярному сеттеру лишь для одиночного значения, от которого ваш коллбэк отказался, и поднимает исключение, когда коллбэк отказал списку из двух и более. При экспорте стартуйте с Default(THPDFXFDFArrayAccess), чтобы неназначенные слоты (включая опциональный SetFormFieldRV) остались nil. Возвращайте True из GetFormFieldValueArray для любого поля со списочным значением, включая пустой выбор, и False, чтобы скалярный коллбэк разобрался с полем сам. Пустой массив уходит единственным пустым <value/>, поэтому хранилище, предлагающее опцию пустой строки, должно отвергнуть этот случай внутри коллбэка — так же, как делает ExportLoadedFormToXFDF
uses HPDFXFDF;
// Обычный указатель на функцию, не "of object": Context несёт ваше хранилище.
// TFormStore и его методы — ваш код, не часть HotPDF
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. Создание полей выбора и установка их флаговых бит разобраны в добавлении ListBox и других полей AcroForm в загруженный PDF, а разметка комментариев, путешествующая через часть <annots> XFDF — в импорте и экспорте аннотаций XFDF в HotPDF. Справочник методов по каждому показанному здесь вызову плюс пробная сборка, чтобы прогнать их на своих формах, — на странице HotPDF Delphi PDF component