Technical Article

Обмен данными форм FDF, XFDF и XFA в Delphi

Библиотека losLab PDF Library импортирует и экспортирует данные форм из PDF тремя различными способами: форматы FDF и XFDF для значений полей AcroForm, те же два формата для комментариев аннотаций и запись целого пакета XFA XDP непосредственно в форму. Точками входа являются методы ExportFormDataToXFDF, ImportFormDataFromXFDF, ImportAnnotationsFromFDF, ExportAnnotationsToXFDF и SetXFAFromString, и для каждого из них предусмотрены файловый и строковый варианты

Причина наличия столь большого числа методов заключается в том, что данные форм PDF неоднородны. Заполненная форма AcroForm содержит значения полей, она также может нести комментарии рецензирования, а устаревшая форма XFA внедряет полное XML-описание приложения, не имеющее отношения ни к тому, ни к другому. losLab PDF Library намеренно разделяет эти три задачи по различным API, так как их объединение привело бы к навязыванию неверной модели данных как минимум для двух из них. Выбор нужного семейства методов — это первое архитектурное решение, и обычно оно принимается сразу же, как только вы понимаете, какой формат фактически ожидает принимающая система на другой стороне

В чем разница между данными форм FDF, XFDF и XFA?

Как заполнить форму PDF из FDF или XFDF в Delphi?

Загрузите документ, вызовите один из методов импорта и сохраните файл. ImportFormDataFromXFDF и ImportFormDataFromFDF анализируют входящие данные формы, сопоставляют каждую запись с полем AcroForm по его полному имени, задают значение и возвращают количество фактически обновленных полей. Оба метода обновляют только те поля, которые уже существуют в целевом PDF; они не создают новые поля для имен, не определенных в форме, что предотвращает несанкционированное расширение вашей формы сторонними или вредоносными файлами данных

var
  Lib: TPDFlib;
  FieldsSet: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('application-blank.pdf', '');
    // XFDF produced by a case-management system, already UTF-8 on disk
    FieldsSet := Lib.ImportFormDataFromXFDF('applicant-1042.xfdf');
    if FieldsSet > 0 then
      Lib.SaveToFile('application-filled.pdf');
  finally
    Lib.Free;
  end;
end;

Две детали определяют, сохранятся ли нетривиальные данные при переносе. Первая — это экранирование: FDF сохраняет значения как строковые литералы PDF, поэтому значение, содержащее круглую скобку, обратный слэш или непечатный байт, поступает обернутым в восьмеричное экранирование согласно ISO 32000-2 §7.3.4.2, и библиотека losLab PDF Library декодирует это экранирование при импорте, чтобы скобки и слэши вернулись в виде литеральных символов, введенных пользователем. Вторая деталь — кодировка: файловые методы считывают и записывают XFDF и FDF в формате UTF-8, что соответствует объявлению XFDF и требуется для корректного переноса не-ASCII значений полей (имен с диакритическими знаками, символов валют, CJK-адресов) без искажения. Если вы собираете XFDF самостоятельно, объявляйте и записывайте данные в UTF-8, чтобы процесс импорта прошел корректно

Иерархические поля, множественный выбор и форматированный текст

Иерархические имена полей — первое место, где ломается простейший экспортер. AcroForm обращается к вложенному полю через точечное имя, например, Applicant.FullName, однако XFDF не помещает эту строку с точкой в единый атрибут имени; стандарт ISO 19444 предусматривает вложенность в виде <field name="Applicant"><field name="FullName">. losLab PDF Library разделяет точечные имена на вложенные элементы <field> при экспорте и собирает их обратно в полное точечное имя при импорте, чтобы оба направления оставались симметричными. При экспорте библиотека также пропускает промежуточные родительские поля, поскольку родительский узел в AcroForm несет только имя и не имеет собственного значения; его экспорт привел бы к генерации пустого элемента <value></value>, не соответствующего реальной форме. Списки с множественным выбором — вторая ловушка: поле выбора может содержать несколько выбранных значений одновременно, что в XFDF выражается повторяющимися элементами <value>, а в FDF — массивом /V, и losLab PDF Library разделяет поле на несколько значений только тогда, когда это действительно список с множественным выбором, чтобы обычное многострочное текстовое поле сохраняло свои разрывы строк, а не разбивалось на фиктивные значения

Форматированный текст (rich text) — третья проблема, в которой ошибаются чаще всего. Поле с форматированием сохраняет разметку в записи RV как поддерево XHTML, а XFDF переносит ее в <value-richtext>. losLab PDF Library записывает это поддерево как активный фрагмент XML, а не экранирует его, благодаря чему сторонние инструменты считывают настоящий форматированный текст вместо строки с видимыми тегами; при импорте библиотека сохраняет исходное поддерево RV и по-прежнему применяет обычный текст <value> к V. Если поле предлагает оба варианта, обычное значение имеет приоритет для V, а форматированный текст передается рядом в RV, что соответствует правилу совместимости, предотвращающему перезапись значения, уже установленного другим инструментом, при импорте форматированного текста. Если форматированный текст служит для передачи структуры документа вспомогательным технологиям, обрабатывайте его так же, как и порядок чтения, описанный в статье о структуре доступности и tagged PDF

var
  Lib: TPDFlib;
const
  XFDF =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">' +
    '<fields>' +
    '  <field name="Applicant">' +
    '    <field name="FullName"><value>Alice Example</value></field>' +
    '  </field>' +
    '  <field name="Skills">' +
    '    <value>Delphi</value><value>PDF</value>' +
    '  </field>' +
    '  <field name="Notes">' +
    '    <value>See attachment</value>' +
    '    <value-richtext><body><p>See <b>attachment</b></p></body></value-richtext>' +
    '  </field>' +
    '</fields></xfdf>';
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('intake.pdf', '');
    // Applicant.FullName reassembles; Skills fills the /V + /I arrays;
    // Notes gets V from <value> and RV from <value-richtext>
    Lib.ImportFormDataFromXFDFString(XFDF);
    Lib.SaveToFile('intake-filled.pdf');
  finally
    Lib.Free;
  end;
end;

Как передавать комментарии аннотаций в обе стороны через FDF или XFDF?

Комментарии передаются в семействе аннотаций, и поддерживаемое подмножество намеренно невелико. Методы ExportAnnotationsToXFDF и ImportAnnotationsFromXFDF, а также их аналоги для FDF, переносят текстовую разметку для тех полей, которые корректно передаются в обе стороны: подтип аннотации, ее прямоугольник, ее индекс страницы (начиная с 0), автора в T, тему в Subj, простой текст Contents и цвет, который сопоставляет тройку DeviceRGB в PDF /C с атрибутом XFDF #RRGGBB и обратно. Импорт принимает только известные имена элементов и пропускает любую запись, страница которой выходит за рамки диапазона или прямоугольник которой отсутствует, поэтому измененный вручную или сторонний файл XFDF не сможет добавить некорректную аннотацию в документ. Стоит прямо сказать о том, что данное подмножество пока не переносит: форматированное содержимое (RC), всплывающие окна, квадроточки (quadpoints), списки линий от руки (ink lists), вершины и запеченные потоки внешнего вида (appearance streams) остаются вне сферы применения, поэтому геометрия выделения и пользовательский вид штампов не сохранятся при таком переносе. Чтобы подтвердить, что именно было перенесено, обойдите аннотации страницы или более широкое дерево элементов, описанное в статье о поиске текста и перечислении элементов страницы

var
  Src, Dest: TPDFlib;
  Xfdf: WideString;
begin
  Src := TPDFlib.Create;
  Dest := TPDFlib.Create;
  try
    Src.LoadFromFile('reviewed.pdf', '');
    Xfdf := Src.ExportAnnotationsToXFDFString;   // <annots> subset only
    
    Dest.LoadFromFile('clean-copy.pdf', '');     // pages must line up by index
    Dest.ImportAnnotationsFromXFDFString(Xfdf);
    Dest.SaveToFile('clean-copy-commented.pdf');
  finally
    Dest.Free;
    Src.Free;
  end;
end;

Запись полного пакета XFA XDP с помощью SetXFAFromString

Формат XFA стоит особняком, и метод SetXFAFromString позволяет записать его целиком за один раз. Этот метод принимает полный XML Data Package (документ <xdp:xdp> со своими пакетами шаблонов и наборов данных) и сохраняет его как поток, на который указывает ссылка из /AcroForm/XFA. losLab PDF Library создает контейнер AcroForm по запросу, если в документе его нет, поэтому вам не нужно добавлять фиктивное поле только для того, чтобы привязать XFA; кроме того, библиотека сбрасывает любое кэшированное состояние XFA, поэтому последующее чтение отразит именно тот пакет, который вы только что записали, а не устаревшие данные. Поскольку пакет XFA является XML-документом и не обязательно сохранен в UTF-8, библиотека автоматически обнаруживает маркер последовательности байтов UTF-16 и корректно декодирует пакет перед анализом, что важно для пакетов, созданных инструментами, использующими UTF-16 по умолчанию

После того как пакет размещен, методы GetXFAFormFieldValue и SetXFAFormFieldValue обращаются к отдельным полям по их пути Scripting Object Model. losLab PDF Library принимает как стандартные корни SOM, так и простые относительные пути, поэтому form1.FullName, $data.form1.FullName и xfa.datasets.data.form1.FullName разрешаются в один и тот же узел данных, а сторона шаблона аналогичным образом принимает $template и xfa.template. Эта гибкость важна, когда пути SOM генерируются другой системой, которая всегда выводит полное имя

var
  Lib: TPDFlib;
const
  Xdp =
    '<?xml version="1.0" encoding="UTF-8"?>' +
    '<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/">' +
    '  <template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">' +
    '    <subform name="form1">' +
    '      <field name="FullName"><ui><textEdit/></ui></field>' +
    '    </subform>' +
    '  </template>' +
    '  <xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/">' +
    '    <xfa:data><form1><FullName>Alice Example</FullName></form1></xfa:data>' +
    '  </xfa:datasets>' +
    '</xdp:xdp>';
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetXFAFromString(Xdp, 0);      // creates the AcroForm container if none exists
    // read back through the data DOM with an SOM path
    if Lib.GetXFAFormFieldValue('form1.FullName') = 'Alice Example' then
      Lib.SetXFAFormFieldValue('form1.FullName', 'Bob Example');
    Lib.SaveToFile('xfa-packet.pdf');
  finally
    Lib.Free;
  end;
end;

В итоге значения полей полностью переносятся в обе стороны через FDF и XFDF, включая иерархию, множественный выбор и форматированный текст; комментарии аннотаций переносятся как определенное подмножество текстовой разметки с явными ограничениями; а XFA записывается и считывается целиком в виде пакета с доступом к отдельным полям по SOM. Подбирайте формат в соответствии с требованиями получателя, объявляйте кодировку UTF-8 для всего, что создаете вручную, и помните, что методы импорта работают только с теми полями и аннотациями, которые уже соответствуют структуре документа. Описанные здесь методы работы с данными форм, аннотациями и XFA входят в состав библиотеки losLab PDF Library для Delphi и C++Builder, справочное руководство к которой содержит полный список параметров для каждой точки экспорта и импорта