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

Виявлення та вилучення форм XFA в Delphi за допомогою PDFium

Так, PDFium підтримує форми XFA, але з одним важливим застереженням. Компонент PDFium може виявляти форми XFA та вилучати їхні пакети template, datasets та config як вихідний XML за допомогою стандартної бібліотеки DLL без будь-яких зовнішніх залежностей. Проте запуск динамічної форми XFA (тобто формування інтерактивних сторінок на основі її XML-опису) — це окреме завдання, яке додатково потребує збірки з підтримкою двигуна V8. У спільноті часто запитують, чи підтримує PDFium XFA, наче це одна проста функція, але правильна відповідь полягає в тому, що читання даних XFA та візуалізація форми XFA — це абсолютно різні завдання з різними вимогами до системних залежностей.

У цій статті детально розглянуто обидва підходи з використанням компонента PDFium Component — нативного VCL-обгортки навколо PDFium для Delphi та C++Builder. Спочатку ми опишемо процес виявлення та статичного вилучення (властивості FormType, XFA та методи сімейства GetXfaPacket*), потім перейдемо до динамічного запуску з обмеженнями V8, і на завершення розглянемо перевірку можливостей бібліотеки для безпечної обробки ситуацій, коли встановлена DLL не підтримує потрібний двигун.

Чи підтримує PDFium форми XFA?

Сприймайте це як дві окремі можливості. Статичне вилучення XFA доступне завжди: низькорівневі функції FPDF_GetXFAPacket*, що лежать в основі методів GetXfaPacketCount, GetXfaPacketName та GetXfaPacketContent, скомпільовані в кожній сучасній версії pdfium.dll, навіть якщо підтримка XFA була вимкнена при збірці. Саме тому отримання сирого XML-вмісту пакетів template, datasets та config не потребує зовнішніх залежностей — це просто читання структури без запуску коду. Динамічна XFA — це зовсім інша можливість: запуск самої форми, виконання обчислень та верстка сторінок двигуном XFA. Цей шлях доступний лише у збірках PDFium, скомпільованих із підтримкою XFA поверх двигуна V8 JavaScript, і саме про це зазвичай йдеться в обговореннях підтримки XFA в PDFium.

Стандарти чітко описують це розділення. Формат XFA визначено поза базовою структурою PDF в Adobe XFA 3.3 (2012), а специфікація PDF 1.7 §8.6.7 описує спосіб збереження форми XFA всередині PDF: пакети форми знаходяться в словнику AcroForm під ключем /XFA, а документ, який має бути зверстаний процесором XFA для коректного відображення, встановлює прапорець /NeedsRendering true у каталозі. Саме ці два маркери шукає компонент PDFium, і на них можна повністю покладатися при аналізі раніше невідомих файлів.

Чим XFA відрізняється від AcroForm?

AcroForm — це класична форма PDF: інтерактивні елементи (текстові поля, прапорці, перемикачі) розміщуються за фіксованими координатами на звичайних сторінках PDF. Сторінка, яку ви бачите, збережена у файлі, а поля знаходяться поверх неї. XFA використовує інший підхід. Інтерактивна форма повністю описується мовою XML, а в повному (динамічному) документі XFA сторінки PDF виступають лише плейсхолдерами. Реальний вміст створюється під час відкриття файлу двигуном XFA, який читає XML-шаблон і верстає його сторінки, розширюючи або стискаючи їх відповідно до обсягу даних. Саме про це повідомляє прапорець /NeedsRendering true: без процесора XFA статичні сторінки можуть бути порожніми або містити лише повідомлення «будь ласка, відкрийте цей файл у сумісному переглядачі».

Вміст XFA — це набір іменованих пакетів, три з яких містять практично всю необхідну інформацію. Пакет template визначає структуру форми: поля, макет, обчислення та скрипти. Пакет datasets містить поточні дані форми (заповнені значення полів у вигляді дерева XML). Пакет config зберігає інструкції для двигуна XFA. Існують також інші пакети (localeSet, connectionSet, обгортка xdp), але для перевірки форми або експорту даних зазвичай достатньо пакетів template та datasets. Оскільки ці дані є звичайним XML-кодом у потоці, їх читання не потребує виконання будь-якого коду, що і дозволяє виконувати статичне вилучення без додаткових залежностей.

Виявлення форми XFA в Delphi

Виявлення починається стандартно для компонента PDFium: вкажіть FileName, встановіть Active := True та перевірте, чи відкрився файл. Встановлення Active := True не генерує винятків — відсутність файлу, неправильний пароль або відсутність DLL просто залишать властивість Active у значенні False, тому перевірка є обов'язковою. Після відкриття документа властивість FormType вкаже модель форми. Два значення з переліку TPdfFormType відносяться до XFA: ftXfaFull означає повну, динамічну форму XFA, яка потребує рендерингу, а ftXfaForeground позначає XFAF (гібридний варіант, де вміст XFA малюється поверх звичайних сторінок AcroForm, завдяки чому статичні сторінки залишаються інформативними). Булева властивість XFA є зручним прапорцем, що означає «цей документ містить форму XFA будь-якого типу». Якщо ви також працюєте з елементами керування на сторінках XFAF або AcroForm, детальніше це описано в довіднику з навігації по полях форм PDFium.

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'application-form.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                        // damaged, encrypted, or DLL missing
    case Pdf.FormType of
      ftXfaFull:
        Log('Full (dynamic) XFA form');
      ftXfaForeground:
        Log('XFAF: XFA layered over static AcroForm pages');
      ftAcroForm:
        Log('Classic AcroForm');
    else
      Log('No interactive form');
    end;
    if Pdf.XFA then
      Log('Document carries an XFA packet payload');
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Вилучення пакетів XFA у вигляді сирого XML

Вилучення — це корисна можливість, яка не потребує додаткових бібліотек. Перевірте можливість читання за допомогою властивості XfaStaticReadable (вона підтверджує наявність потрібних функцій у завантаженій DLL), а потім обійдіть пакети за індексом. Метод GetXfaPacketCount повертає загальну кількість пакетів форми, GetXfaPacketName повертає ім'я кожного з них, а GetXfaPacketContent повертає сирі байти (які для відомих пакетів є XML-текстом у кодуванні UTF-8). Якщо ви знаєте назву потрібного пакета, спеціальні методи GetXfaTemplate, GetXfaDatasets та GetXfaConfig дозволяють отримати дані типу TBytes напряму. Для задач міграції даних найбільше значення має метод GetXfaDatasets: він повертає введені користувачем значення полів у вигляді XML-документа, який можна розібрати та імпортувати у власну базу даних. Це аналогічно завданням із вилучення тексту з PDF.

procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
  I, Count: Integer;
  PacketName: string;
  Content, Datasets: TBytes;
begin
  if not Pdf.XfaStaticReadable then
    Exit;                          // packet reader exports not present
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // raw UTF-8 XML
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Or jump straight to the filled-in field values by name:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

Чому динамічний рендеринг XFA потребує двигуна V8?

Короткий висновок: тому що динамічна форма XFA — це маленька програма, а не статичний малюнок. Пакет шаблону містить обчислення FormCalc та JavaScript, правила перевірки та скрипти обробки подій, які двигун XFA має виконати для побудови зовнішнього вигляду сторінок. PDFium реалізує цей двигун лише у збірках, скомпільованих із підтримкою XFA поверх V8 — двигуна JavaScript від Google. Саме тому бібліотека DLL з підтримкою V8 (наприклад, pdfium32v8.dll або pdfium64v8.dll) значно більша за стандартну версію. Без двигуна V8 скрипти не виконуватимуться, і сторінки не будуть відрендерені — ви зможете отримати лише сирий XML-код статично.

Вибір збірки бібліотеки має жорстке обмеження: це рішення приймається один раз для всього процесу. Один процес не може завантажити одночасно і стандартну бібліотеку pdfium.dll, і версію з підтримкою V8 (вони експортують однакові функції, а V8 зберігає глобальний стан ізолятів). Тому компонент PDFium має вибрати збірку V8 до найпершого завантаження бібліотеки в пам'ять. Щоб автоматизувати цей процес, метод LoadDocument попередньо сканує файл на наявність маркерів /XFA та /NeedsRendering true (ця ж перевірка доступна як окрема функція PdfFileLooksXfa) і встановлює глобальний прапорець EnableV8Engine := True при їх виявленні. Якщо стандартна бібліотека pdfium.dll вже завантажена в процес, ця зміна не матиме сили, про що і повідомляє система діагностики.

begin
  // Peek for XFA before PDFium is loaded for the first time, so the
  // V8-enabled build can still be selected. PdfFileLooksXfa scans for
  // /XFA and /NeedsRendering true without parsing the whole file.
  if PdfFileLooksXfa('dynamic-form.pdf') then
    EnableV8Engine := True;

  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'dynamic-form.pdf';
    Pdf.Active := True;
    if Pdf.Active and Pdf.XFA then
    begin
      if Pdf.XfaRuntimeAvailable then
        RenderXfaPages(Pdf)          // engine is live: dynamic layout
      else
        ExtractXfaData(Pdf, 'C:\out'); // fall back to static packets
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Безпечна обробка відсутності рантайму XFA

Три перевірки дозволяють точно оцінити рівень підтримки XFA, і надійний код має перевіряти їх замість припущень. Властивість XfaFeaturesAvailable глобально перевіряє наявність функцій XFA/V8 у завантаженій DLL. Властивість XfaStaticReadable підтверджує можливість читання пакетів конкретного документа. XfaRuntimeAvailable — це найважливіша перевірка: вона повертає true, лише якщо для цього документа активовано двигун XFA. Це означає, що документ містить XFA, завантажено версію з V8 та виклик FPDF_LoadXFA завершився успішно. Документ може повертати XFA = True, але при цьому XfaRuntimeAvailable = False, якщо розгорнута DLL не підтримує XFA або якщо в процесі вже була завантажена стандартна бібліотека.

Якщо документ XFA відкрито, але двигун не може бути запущений, компонент PDFium перемикає форму в безпечний статичний режим замість спроб динамічного виконання. Він один раз генерує подію OnXfaRuntimeMissing, даючи програмі можливість відреагувати — наприклад, запропонувати користувачеві перезапустити програму із версією бібліотеки з підтримкою V8, одночасно дозволяючи працювати зі статичними XML-даними. Налаштування цього обробника дозволяє уникнути ситуації, коли форма просто тихо відображає порожні сторінки.

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Fires once when an XFA document opens but the engine cannot run:
  // a plain pdfium.dll was already loaded, or the build lacks XFA/V8.
  ShowMessage('This is a dynamic XFA form. Restart with the ' +
    'V8-enabled build to render it; its packet data is still readable.');
end;

Будьте реалістичними щодо цих обмежень і у власному продукті. Якщо ви не розгортаєте бібліотеку з підтримкою V8, динамічні форми XFA не рендеритимуться для користувачів. Проте ви все одно зможете виявляти їх, вилучати пакети та зчитувати введені дані. Цього цілком достатньо для великого класу завдань, пов'язаних з обробкою та вилученням даних із банківських чи державних форм, де інтерактивне відображення не є обов'язковим. Використовуйте важчу збірку V8 лише тоді, коли користувачам дійсно потрібно показувати та заповнювати такі форми в інтерфейсі програми. Якщо ваш конвеєр обробки даних також працює з вкладеними файлами, аналогічний підхід діє і для роботи з вкладеннями PDF в Delphi.

Описані тут методи та властивості FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig та функції перевірки можливостей є частиною компонента PDFium Component для Delphi та C++Builder.