Техническая статья

Экспорт и импорт XFDF в Delphi с PDFium Component

PDFium Component для Delphi и Lazarus обменивается данными форм и аннотациями через XFDF — формат обмена на XML, определённый в ISO 19444-1. TPdf.ExportXFDF сериализует значение каждого поля формы и каждую поддерживаемую аннотацию загруженного документа в файл XFDF или в TStream; TPdf.ImportXFDF читает документ XFDF обратно, заново создавая аннотации на их страницах и применяя значения полей. Один этот обмен покрывает два рабочих процесса, до которых рано или поздно доходит любое приложение для работы с документами: рецензент размечает PDF в Acrobat и присылает вам комментарии для слияния, либо правило комплаенса требует, чтобы данные формы жили в отдельном, сравнимом и проверяемом файле, а не были запечены в сам PDF

Схема обмена XFDF в Delphi: TPdf.ExportXFDF пишет поля форм и 18 подтипов аннотаций в небольшую полезную нагрузку XML, которую TPdf.ImportXFDF вливает обратно в любую копию PDF
ExportXFDF превращает весь документ в небольшую полезную нагрузку XML по ISO 19444-1, а ImportXFDF накладывает те же поля и аннотации обратно на любую копию PDF

Поддерживает ли PDFium импорт и экспорт XFDF?

Сама библиотека PDFium — нет. В нативном C API вообще нет функций для XFDF или FDF: ни один её публичный заголовок не читает и не пишет ни тот, ни другой формат, поэтому никакой обёрткой туда не добраться. Поэтому PDFium Component реализует весь движок XFDF на Pascal — в отдельном модуле, который строит и разбирает XML напрямую поверх существующей модели данных аннотаций и полей формы TPdf. Писатель выдаёт XML вручную, а читатель представляет собой написанный вручную парсер на конечном автомате, так что возможность не добавляет зависимости ни от Delphi XMLDoc, ни от модулей DOM в FPC и ведёт себя одинаково в Delphi и Lazarus

Этот выбор архитектуры важен, когда вы оцениваете альтернативы. Если вы вызываете сырые функции FPDF_* из Pascal, XFDF для вас стена: этой возможности попросту нет в DLL. Компонент проходит сквозь стену, трактуя XFDF как чистую задачу сериализации: собрать значения полей и записи аннотаций, которые обёртка и так умеет читать, выписать их стандартным XML и обратить процесс при импорте

Что содержит файл XFDF?

ISO 19444-1 определяет XFDF как XML-представление FDF, и его полезная нагрузка делится на два блока верхнего уровня. Элемент <fields> несёт иерархические имена полей формы и их значения — всё, что пользователь ввёл, выбрал или отметил в AcroForm. Элемент <annots> несёт аннотации: TPdf.ExportXFDF выдаёт 18 подтипов — text, highlight, underline, strikeout, squiggly, line, circle, square, caret, polygon, polyline, stamp, ink, freetext, fileattachment, sound, link и redact. Аннотации widget намеренно отсутствуют в <annots>, потому что их данные едут в <fields>, а внутренние для просмотрщика подтипы вроде Popup в словарь XFDF не входят

Анатомия файла XFDF в PDFium Component: иерархический блок fields со значениями AcroForm рядом с блоком annots, перечисляющим 18 поддерживаемых подтипов аннотаций
Блок fields несёт иерархические значения AcroForm, а блок annots перевозит 18 подтипов разметки и исключает widget и внутренние для просмотрщика типы

Чтобы обмен был верным, запись TPdfAnnotation была расширена метаданными, которых ждёт XFDF: Name (уникальный идентификатор NM), Subject, ModificationDate, CreationDate, Icon, Opacity, конечные точки линий, вершины многоугольников и ломаных и пути чернильных росчерков. Каждое новое поле идёт в паре с логическим сторожем Has*, поэтому код, строивший аннотации против прежних версий, продолжает компилироваться и продолжает выдавать те же словари — незаданное поле никогда не записывается. Если вы уже создаёте разметку программно, та же запись, которую вы используете для текстовых аннотаций разметки с quad points, теперь несёт всё, что нужно XFDF

Как экспортировать данные формы и аннотации в XFDF?

Один вызов делает весь документ. Загрузите PDF с включённым заполнением форм, вызовите ExportXFDF, и компонент обойдёт каждую страницу, соберёт значения полей и аннотации и запишет XML. Возвращаемое значение — число записанных байтов UTF-8, что даёт дешёвую проверку разумности в журналах

var
  Pdf: TPdf;
  Bytes: Integer;
begin
  Pdf := TPdf.Create(Self);
  Pdf.FormFill := True;            // нужно, чтобы значения полей были живыми
  Pdf.FileName := 'expense-report.pdf';
  Pdf.Active := True;
  Bytes := Pdf.ExportXFDF('expense-report.xfdf');
  ShowMessage(Format('%d bytes of XFDF written', [Bytes]));
end;

У обоих направлений есть перегрузка с TStream, поэтому временный файл на диске никому не навязывается. Экспорт в TMemoryStream — естественная форма, когда XFDF направляется в HTTP-ответ, в блоб базы данных или в запись подписанного архива

var
  Buffer: TMemoryStream;
begin
  Buffer := TMemoryStream.Create;
  try
    Pdf.ExportXFDF(Buffer);        // тот же XML, без всякого файла
    Buffer.Position := 0;
    // отдайте поток веб-ответу, колонке с блобом или записи zip
  finally
    Buffer.Free;
  end;
end;

Как ImportXFDF вливает комментарии обратно в документ?

Сторона импорта — то место, где замыкается рабочий процесс рецензирования. Коллега размечает договор в Acrobat, экспортирует комментарии в XFDF и присылает вам несколько килобайт XML вместо второй копии PDF. TPdf.ImportXFDF разбирает этот файл, создаёт каждую аннотацию на странице, названной её атрибутом page, и записывает значения полей в соответствующие аннотации widget. Функция возвращает суммарное число применённых полей и аннотаций, поэтому интерфейс может точно подтвердить, сколько всего пришло

var
  Applied: Integer;
begin
  Applied := Pdf.ImportXFDF('review-comments.xfdf');
  StatusBar.SimpleText :=
    Format('%d fields and annotations merged', [Applied]);
end;

Парсер — написанный вручную сканер тегов, сделанный ради терпимости, а не строгости: неизвестные элементы пропускаются, инструкции обработки, объявления DOCTYPE и комментарии игнорируются, нераспознанные имена флагов отбрасываются, а значения атрибутов разэкранируются по полному набору сущностей XML и числовых ссылок на символы, включая суррогатные пары UTF-16. XFDF, произведённый Acrobat, веб-просмотрщиком или другим инструментом, вливается без церемоний. Как только аннотации оказались в документе, цикл рецензирования продолжается механикой ответов и статусов, описанной в статье построение рабочего процесса рецензирования аннотаций с PDFium Component, а импортированные значения полей участвуют в обычной логике порядка табуляции, разобранной в статье навигация по полям формы

Почему для XFDF важны десятичные разделители?

Каждая аннотация в XFDF позиционируется координатными строками — rect="70.5,540,200,560" и им подобными, — а ISO 19444-1 требует точку в качестве десятичного разделителя. Форматирование чисел с плавающей точкой в Delphi по умолчанию следует локали Windows, поэтому на немецкой или французской системе наивный FloatToStr превращает 70.5 в 70,5, что портит разделённый запятыми список координат в мусор, который другие обработчики отвергают или читают неверно. PDFium Component нормализует каждое записываемое число: десятичный разделитель локали переписывается точкой, а хвостовые нули срезаются, поэтому экспортированный файл побайтово одинаков независимо от того, произведён он на машине en-US или de-DE. Если вам когда-нибудь доводилось отлаживать инструмент для PDF, который работал в офисе и падал у европейского клиента, вы встречали ровно этот класс ошибок — и полезно знать, что компонент разбирается с ним за вас

Каковы пределы обмена?

Две границы стоит назвать прямо. Во-первых, XFDF перевозит данные аннотаций, а не отрисованный вид: потоки внешнего вида в формат не входят, поэтому принимающий просмотрщик заново порождает облик каждой аннотации из её свойств. Выделение или квадрат отрисуются верно везде; штамп с пользовательским внешним видом откатится к тому, что целевой просмотрщик рисует для штампа с таким именем. Во-вторых, у нижележащего API PDFium есть сеттеры для строк, но нет ни для чисел аннотаций, ни для геометрии путей, поэтому Opacity, конечные точки линий, вершины и чернильные росчерки доступны только на экспорт: компонент честно сохраняет их при записи XFDF, но не может записать их обратно в PDF при импорте. Значения полей, содержимое, цвета, прямоугольники, quad points, даты и метаданные идентичности проходят обмен в обе стороны

Три уровня верности обмена XFDF в Delphi: свойства, ходящие в обе стороны, доступная только на экспорт геометрия вроде вершин и чернильных путей и потоки внешнего вида, которые файл никогда не покидают
Основные свойства ходят в обе стороны, геометрия путевого рода остаётся только на экспорт, а потоки внешнего вида заново порождает тот просмотрщик, который получит файл

Для практического старта пример XfdfLab поставляется в наборах демонстраций для Delphi, C++Builder и Lazarus и вешает оба вызова на кнопки поверх любого открытого вами PDF. Поддержка XFDF включена в текущий выпуск PDFium Component наряду с API аннотаций и форм, на которых она построена