Да, PDFium поддерживает формы XFA, но с одним важным разделением. Компонент PDFium Component может обнаруживать форму XFA и извлекать ее XML-пакеты template, datasets и config в виде исходного XML-кода с использованием стандартной библиотеки DLL без каких-либо внешних runtime-зависимостей. Однако запуск динамической формы 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 определена за пределами базовой грамматики PDF в документе Adobe XFA 3.3 (2012), а в PDF 1.7 §8.6.7 описывается способ хранения XFA внутри файла: пакеты форм находятся в словаре AcroForm под ключом /XFA, а документ, который должен быть сгенерирован XFA-процессором перед отображением, помечается флагом /NeedsRendering true в каталоге. Эти два маркера — именно то, что ищет компонент PDFium Component для анализа неизвестных файлов
Чем 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 Component способом: задайте FileName, установите Active := True и проверьте успешность открытия. Установка Active := True никогда не вызывает исключений (отсутствие файла, неверный пароль или отсутствие DLL просто оставят свойство Active в состоянии False), поэтому проверка обязательна. После открытия документа свойство FormType сообщит используемую модель формы. Два значения из перечисления TPdfFormType относятся к XFA: ftXfaFull указывает на полноценную динамическую форму XFA, требующую рендеринга, а ftXfaForeground — на XFAF (foreground-подмножество), в котором данные 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 — это полноценная программа, а не статичный рисунок. Пакет template содержит расчеты, проверки и скрипты на FormCalc и JavaScript, и процессор макетов XFA должен выполнять их, чтобы построить страницы документа. PDFium реализует этот процессор только в сборках с поддержкой XFA поверх движка V8 JavaScript (разработанного Google) — именно поэтому DLL с поддержкой V8 (pdfium32v8.dll или pdfium64v8.dll) значительно больше стандартной сборки. Без V8 выполнять скрипты некому, поэтому срендерить страницы динамически не получится, и вам будет доступен только статический разбор XML
Выбор нужной сборки DLL имеет важное ограничение: это решение принимается один раз на весь процесс. Процесс не может одновременно загрузить и стандартную pdfium.dll, и сборку с V8 (они используют одинаковые имена функций, а V8 хранит глобальное состояние isolate). Поэтому компонент PDFium Component должен выбрать 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 Runtime
Три свойства позволяют точно определить уровень поддержки XFA в приложении. Свойство XfaFeaturesAvailable проверяет наличие функций экспорта XFA/V8 в загруженной DLL. Свойство XfaStaticReadable подтверждает возможность чтения пакетов для конкретного документа (защита при извлечении). Свойство XfaRuntimeAvailable является ключевым: оно возвращает True только тогда, когда для данного документа запущен процессор XFA, то есть документ содержит XFA, загружена сборка с V8 и метод FPDF_LoadXFA завершился успешно. Документ может возвращать XFA = True, но иметь XfaRuntimeAvailable = False, если загруженная DLL не поддерживает XFA/V8 или стандартная DLL была загружена в процесс ранее
Когда открывается документ XFA, но движок не может быть запущен, компонент PDFium Component переводит форму в безопасный статический режим чтения вместо попыток запустить неподдерживаемый процессор, и один раз вызывает событие OnXfaRuntimeMissing. Хост-приложение может отреагировать на него (например, сообщив пользователю о необходимости перезапустить программу со сборкой DLL, поддерживающей V8, но при этом показав извлеченные статические данные). Настройка этого обработчика гарантирует контролируемое поведение программы вместо неожиданного отображения пустых страниц
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;
Будьте реалистичны в оценке возможностей в вашем продукте. Если вы не поставляете DLL с поддержкой V8, динамические формы XFA не будут отображаться у пользователей. Однако вы по-прежнему сможете обнаруживать их и извлекать все XML-пакеты с введенными данными, чего вполне достаточно для переноса данных из старых государственных и банковских систем без необходимости интерактивного отображения форм. Ограничьте использование тяжелой сборки с V8 только теми сценариями, где интерактивное заполнение действительно необходимо, и используйте диагностические свойства для правильной маршрутизации документов. Если ваш процесс извлечения данных также работает со встроенными файлами, аналогичные правила применяются к вложениям в файлах PDF в Delphi
Свойства FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig и диагностические API поставляются в составе компонента PDFium Component для Delphi и C++Builder