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

Откриване и извличане на XFA формуляри в Delphi с PDFium

Да — PDFium поддържа XFA формуляри, но с едно важно разграничение. Компонентът PDFium може да открие XFA формуляр и да извлече неговите пакети template, datasets и config като чист XML, използвайки стандартната DLL библиотека, с нулева зависимост по време на изпълнение. Изпълнението на динамичен XFA формуляр — разполагането на неговото XML описание в живи, интерактивни страници — е отделен проблем, който допълнително изисква V8-съвместима компилация. Общността често пита дали PDFium „поддържа XFA“, сякаш е единична функция, но честният отговор е, че четенето на XFA данни и рендерирането на XFA формуляр са две много различни неща с много различни изисквания за зависимости

Тази статия разглежда и двете страни с компонента PDFium — нативната VCL обвивка (wrapper) около PDFium за Delphi и C++Builder. Първо ще се спрем на пътя за откриване и статично извличане — FormType, свойството XFA и четците GetXfaPacket* — а след това и на динамичния път за изпълнение с неговото V8 ограничение, и накрая на тестовете за съвместимост, които ви позволяват да реагирате плавно, когато внедрена DLL библиотека не може да изпълни енджина

Поддържа ли PDFium XFA формуляри?

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

Стандартите дефинират това разделение много ясно. XFA е дефиниран извън основната PDF граматика в Adobe XFA 3.3 (2012), а PDF 1.7 §8.6.7 описва как XFA формуляр се пренася в PDF: пакетите на формуляра се съхраняват под записа /XFA в речника AcroForm, а документ, който трябва да бъде оформен от 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 от някой от двата типа. Ако работите и с полета на ниво графични обекти (widgets) в 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;                        // повреден, шифрован или липсваща DLL
    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 връща необработените байтове, които за стандартните пакети представляват UTF-8 XML. Когато вече знаете кой пакет ви трябва, помощните методи 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;                          // функциите за четене на пакети не присъстват
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // необработен UTF-8 XML
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Или преминете директно към попълнените стойности по име:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

Защо динамичното рендериране на XFA се нуждае от V8 енджин?

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

Изборът на тази версия има сериозно ограничение, което не можете да заобиколите: това решение се взема веднъж за целия процес. Един процес не може да зареди едновременно стандартната pdfium.dll и V8-съвместимата версия — те експортират едни и същи символи и V8 поддържа глобално състояние на изолация, поради което компонента PDFium трябва да избере V8 компилацията още преди първото зареждане на библиотеката. За да стане това автоматично, LoadDocument предварително сканира файла за маркерите /XFA и /NeedsRendering true (същата проверка, изложена като самостоятелна функция PdfFileLooksXfa) и задава EnableV8Engine := True преди зареждането, ако ги открие. След като обикновената pdfium.dll е вече заредена в процеса, тази настройка не може да влезе в сила, и този случай се докладва като липсваща среда за изпълнение

begin
  // Бърз поглед за XFA, преди PDFium да се зареди за първи път, така че
  // V8-съвместимата компилация все още да може да бъде избрана. PdfFileLooksXfa сканира за
  // /XFA и /NeedsRendering true без парсване на целия файл.
  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)          // енджинът е активен: динамично оформление
      else
        ExtractXfaData(Pdf, 'C:\out'); // връщане към статични пакети
    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-съвместимата версия, като междувременно все пак предложи достъп до статично четимите данни. Свързването на това събитие е разликата между плавен резервен план и формуляр, който просто не показва нищо

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Изпълнява се веднъж, когато XFA документ се отвори, но енджинът не може да се стартира:
  // обикновена pdfium.dll е била вече заредена или в компилацията липсва 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-съвместимия DLL файл, динамичните XFA формуляри никога няма да се рендерират за вашите потребители — но все пак можете да ги откривате, да извличате всеки пакет и да изтегляте подадените данни, което е напълно достатъчно за голям кръг задачи, свързани с извличане на данни от стари държавни или банкови формуляри, вместо интерактивното им представяне. Запазете по-тежката V8 компилация за случаите, които наистина се нуждаят от жив, попълваем формуляр, и оставете тестовете за съвместимост да насочат всеки документ по пътя, по който реално може да премине. Ако вашата система за извличане обработва и вградени файлове, същата философия само за четене важи и за PDF прикачени файлове в Delphi

Методите FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig и съответните тестови свойства, показани тук, са част от PDFium Component за Delphi и C++Builder