Технічна стаття

Експорт та імпорт XFDF у Delphi за допомогою PDFium Component

Компонент PDFium Component для Delphi та Lazarus здійснює обмін даними форм та анотаціями через XFDF — XML-формат обміну, визначений стандартом ISO 19444-1. Метод TPdf.ExportXFDF серіалізує кожне значення поля форми та кожну підтримувану анотацію в завантаженому документі у файл XFDF або потік TStream; метод TPdf.ImportXFDF зчитує документ XFDF назад, відтворюючи анотації на сторінках та застосовуючи значення полів. Цей двосторонній процес охоплює два робочі процеси, які рано чи пізно потрібні кожному додатку для роботи з документами: рецензент розмічає PDF в Acrobat і надсилає вам коментарі для об'єднання, або правило відповідності вимагає, щоб дані форм зберігалися в окремому файлі для перевірки та аудиту, а не всередині самого PDF-файлу

Чи підтримує PDFium імпорт та експорт XFDF?

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

Цей вибір архітектури є важливим при оцінці альтернатив. Якщо ви викликали сирі функції FPDF_* з Pascal, XFDF стане для вас перешкодою: у DLL просто немає такої можливості. Компонент долає цю перешкоду, розглядаючи XFDF як чисте завдання серіалізації — зібрати значення полів та записи анотацій, які обгортка вже вміє читати, записати їх як стандартний XML, а при імпорті виконати зворотний процес

Що містить файл XFDF?

Стандарт ISO 19444-1 визначає XFDF як представлення FDF в XML, і його дані розділені на два блоки верхнього рівня. Елемент <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

Щоб забезпечити точність двостороннього перенесення даних, запис TPdfAnnotation було розширено метаданими, які очікує XFDF: Name (унікальний ідентифікатор NM), Subject, ModificationDate, CreationDate, Icon, Opacity, кінцеві точки ліній, вершини полігонів та ламаних ліній, а також траєкторії малювання пером. Кожне нове поле супроводжується логічним прапорцем Has*, тому код, написаний під попередні версії, продовжує компілюватися та створювати ті самі словники — незаповнене поле ніколи не записується. Якщо ви вже створюєте розмітку програмно, той самий запис, який ви використовуєте для текстових анотацій із квадроточками, тепер містить все необхідне для XFDF

Як експортувати дані форм та анотації в XFDF?

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

var
  Pdf: TPdf;
  Bytes: Integer;
begin
  Pdf := TPdf.Create(Self);
  Pdf.FormFill := True;            // needed so field values are live
  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-відповідь, blob-поле бази даних або підписаний архів

var
  Buffer: TMemoryStream;
begin
  Buffer := TMemoryStream.Create;
  try
    Pdf.ExportXFDF(Buffer);        // same XML, no file involved
    Buffer.Position := 0;
    // hand the stream to a web response, blob column, or zip entry
  finally
    Buffer.Free;
  end;
end;

Як ImportXFDF об'єднує коментарі назад із документом?

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

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, а імпортовані значення полів беруть участь у звичайній логіці переходу по клавіші Tab, яка описана в навігації по полях форми

Чому десяткові роздільники важливі для 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 передає дані анотацій, а не їхній зовнішній вигляд (appearance streams) — графічне представлення не входить до формату, тому переглядач на приймальній стороні самостійно відтворює вигляд кожної анотації за її властивостями. Виділення маркером або прямокутник відображатимуться правильно скрізь; штамп із користувацьким виглядом повернеться до того вигляду, який малює цільовий переглядач для цієї назви штампа. По-друге, базове API PDFium містить методи встановлення для текстових рядків, але не має методів для зміни чисел анотацій чи геометрії шляхів, тому властивості Opacity, кінцеві точки ліній, вершини та мазки пера працюють лише на експорт: компонент надійно зберігає їх при записі XFDF, але не може записати їх назад у PDF при імпорті. Значення полів, вміст, кольори, прямокутники (rects), квадроточки, дати та метадані ідентифікації повністю підтримуються в обох напрямках

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