Technický článek

Detekce a extrakce formulářů XFA v Delphi s PDFium

Ano — PDFium podporuje formuláře XFA, a to s jedním důležitým rozdělením. Komponenta PDFium dokáže detekovat formulář XFA a extrahovat jeho balíčky `template`, `datasets` a `config` jako čisté XML pomocí standardní DLL, tedy se zcela nulovou závislostí na běhovém prostředí. Běh dynamického formuláře XFA — vykreslení jeho XML popisu do živých, interaktivních stránek — je samostatný problém, který navíc vyžaduje sestavení s povoleným V8. Komunita se neustále ptá, zda PDFium „umí XFA“, jako by šlo o jedinou schopnost typu ano/ne, a upřímná odpověď zní, že čtení dat XFA a vykreslování formuláře XFA jsou dvě velmi odlišné věci s velmi odlišnými nároky na závislosti

Tento článek popisuje obě poloviny s komponentou PDFium Component, což je nativní obal VCL kolem PDFium pro Delphi a C++Builder. Nejprve probereme cestu detekce a statické extrakce — `FormType`, vlastnost `XFA` a čtečky `GetXfaPacket*` —, poté dynamickou běhovou cestu s jejím omezením na V8 a nakonec testy schopností, které vám umožní elegantně selhat, když nasazená DLL nedokáže spustit příslušný engine

Podporuje PDFium formuláře XFA?

Vnímejte to jako dvě schopnosti, nikoli jako jednu. Statická extrakce XFA je k dispozici vždy: nízkoúrovňové exporty `FPDF_GetXFAPacket*`, které stojí za metodami `GetXfaPacketCount`, `GetXfaPacketName` a `GetXfaPacketContent`, jsou zkompilovány do každé rozumně moderní knihovny pdfium.dll, a to i do sestavení s vypnutým XFA. Proto vytažení čistého XML šablon `template`, `datasets` a `config` z formuláře nevyžaduje vůbec žádnou závislost za běhu — jedná se o čistě strukturální čtení, do kterého není zapojen žádný engine. Dynamické XFA je druhá schopnost: skutečné spuštění formuláře, vyhodnocení jeho výpočtů a umožnění engine rozvržení XFA jej stránkovat. Tato cesta existuje pouze v sestavení PDFium zkompilovaném s podporou XFA nad JavaScript enginem V8, a to je ta část, o které se většina vláken typu „podporuje PDFium XFA“ ve skutečnosti přou

Standardy toto rozdělení definují jasně. XFA je definováno mimo základní gramatiku PDF v Adobe XFA 3.3 (2012) a specifikace PDF 1.7 §8.6.7 popisuje, jak se formulář XFA přenáší uvnitř PDF: balíčky formuláře žijí pod položkou `/XFA` ve slovníku AcroForm a dokument, který musí být rozvržen procesorem XFA předtím, než začne cokoli znamenat, nastavuje `/NeedsRendering true` v katalogu. Tyto dva identifikátory jsou přesně tím, co komponenta PDFium vyhledává, a přesně tím, na co se můžete spolehnout, když uvažujete o souboru, který jste nikdy předtím neviděli

V čem se XFA liší od AcroForm?

AcroForm je klasický formulář PDF: ovládací prvky (widget annotations) — textová pole, zaškrtávací políčka, přepínače — ukotvené na pevných souřadnicích na běžných stránkách PDF. Stránka, kterou vidíte, je stránka v souboru a pole leží na ní. XFA k tomu přistupuje opačně. Interaktivní formulář je kompletně popsán v XML a v plnohodnotném (dynamickém) dokumentu XFA jsou stránky PDF v podstatě zástupným symbolem — skutečný obsah se generuje při otevření engine XFA, který načte XML šablonu a rozvrhne ji, přičemž se rozšiřuje nebo smršťuje podle dat. To je to, co oznamuje `/NeedsRendering true`: bez procesoru XFA mohou být statické stránky prázdné nebo nést pouze upozornění „otevřete prosím v kompatibilním prohlížeči“

Samotný obsah XFA je sada pojmenovaných balíčků a tři z nich nesou vše, co většina integrací potřebuje. Balíček `template` je definice formuláře — pole, rozvržení, výpočty a skripty. Balíček `datasets` obsahuje skutečná data instance, tedy vyplněné hodnoty polí jako strom XML. Balíček `config` nese pokyny pro zpracování pro engine XFA. Existují i další — localeSet, connectionSet, obal xdp —, ale pro audit formuláře nebo migraci dat z něj je šablona plus datové sady obvykle celou prací. Protože tato data jsou pouze XML ležící v proudu, jejich čtení nikdy nevyžaduje spuštění ničeho, což je hlavní důvod, proč je statická extrakce bez závislostí

Detekce formuláře XFA v Delphi

Detekce začíná tak, jak začíná každý úkol s komponentou PDFium Component: nastavte `FileName`, přepněte `Active := True` a zkontrolujte, zda se skutečně otevřel. `Active := True` nikdy nevyvolá výjimku — chybějící soubor, špatné heslo nebo chybějící DLL pouze ponechá `Active` na hodnotě `False` —, takže kontrola je povinná. Jakmile je dokument otevřen, `FormType` vám sdělí, který model formuláře používá. Dvě z hodnot `TPdfFormType` jsou XFA: `ftXfaFull` je plnohodnotný, dynamický formulář XFA, tedy ten, který vyžaduje vykreslení, zatímco `ftXfaForeground` je XFAF, tedy popředí, kde je obsah XFA vykreslen nad jinak normálními stránkami AcroForm, takže statické stránky zůstávají smysluplné. Logická hodnota `XFA` je zkratkou pro „jedná se o dokument XFA jakéhokoli typu“. Pokud pracujete také s poli na úrovni ovládacích prvků v dokumentu XFAF nebo AcroForm, mechanika je popsána v příručce pro navigaci v polích formuláře PDFium

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'application-form.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                        // poškozeno, šifrováno nebo chybí DLL
    case Pdf.FormType of
      ftXfaFull:
        Log('Plný (dynamický) XFA formulář');
      ftXfaForeground:
        Log('XFAF: XFA navrstvené nad statickými stránkami AcroForm');
      ftAcroForm:
        Log('Klasický AcroForm');
    else
      Log('Žádný interaktivní formulář');
    end;
    if Pdf.XFA then
      Log('Dokument nese obsah balíčku XFA');
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Extrakce balíčků XFA jako čistého XML

Extrakce je ta hodnotnější polovina bez závislostí. Opatřete ji kontrolou `XfaStaticReadable` — ta potvrzuje, že se v načtené DLL vyřešily exporty čtečky balíčků —, a poté procházejte balíčky podle indexu. `GetXfaPacketCount` vrací, kolik balíčků formulář nese, `GetXfaPacketName` dává každému z nich jeho název a `GetXfaPacketContent` vrací nezpracované bajty, které jsou u známých balíčků XML v kódování UTF-8. Pokud již víte, který balíček chcete, pojmenovaní pomocníci `GetXfaTemplate`, `GetXfaDatasets` a `GetXfaConfig` vyřeší název za vás a předají přímo `TBytes`. U úlohy migrace dat je tím, na čem záleží, `GetXfaDatasets`: dává vám vyplněné hodnoty polí jako XML dokument, který můžete analyzovat a mapovat do vlastního schématu, což je stejný typ následné práce, jakou byste dělali po extrahování textu z PDF

procedure ExtractXfaData(Pdf: TPdf; const Dir: string);
var
  I, Count: Integer;
  PacketName: string;
  Content, Datasets: TBytes;
begin
  if not Pdf.XfaStaticReadable then
    Exit;                          // exporty čtečky balíčků nejsou přítomny
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // nezpracované UTF-8 XML
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Nebo přejít přímo k vyplněným hodnotám polí podle názvu:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

Proč dynamické vykreslování XFA vyžaduje engine V8?

Závěr nejprve: protože dynamický formulář XFA je malý program, nikoli statický výkres. Balíček šablon obsahuje výpočty FormCalc a JavaScript, validace a skripty událostí a engine rozvržení XFA je musí spustit, aby vůbec rozhodl, jak stránky vypadají. PDFium implementuje tento engine pouze v sestavení zkompilovaném s podporou XFA nad V8, JavaScript engine Google — což je stejný důvod, proč je DLL s povoleným V8 (pdfium32v8.dll nebo pdfium64v8.dll) výrazně větší než standardní sestavení. Bez V8 nemá skripty kdo spustit, takže neexistují žádné vykreslené stránky k zobrazení, pouze nezpracované XML, které si již můžete přečíst staticky

Výběr tohoto sestavení má pevné omezení, které nelze obejít: je to jednorázové rozhodnutí pro každý proces. Jediný proces nemůže načíst standardní pdfium.dll i sestavení s podporou V8 — exportují stejné symboly a V8 udržuje globální stav izolátu —, takže komponenta PDFium Component musí vybrat sestavení V8 dříve, než se knihovna načte úplně poprvé. Aby to bylo automatické, `LoadDocument` předem prohledá soubor na přítomnost identifikátorů `/XFA` a `/NeedsRendering true` (stejná kontrola vystavená jako samostatná funkce `PdfFileLooksXfa`) a nastaví `EnableV8Engine := True` před načtením, když je najde. Jakmile je již v paměti přítomna obyčejná pdfium.dll, přepnutí již nemůže mít účinek, a to je případ, který hlásí chybějící cesta za běhu

begin
  // Nahlédnout na XFA před prvním načtením PDFium, aby bylo možné
  // vybrat sestavení s V8. PdfFileLooksXfa vyhledává
  // /XFA a /NeedsRendering true bez analýzy celého souboru.
  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 běží: dynamické rozvržení
      else
        ExtractXfaData(Pdf, 'C:\out'); // vrátit se ke statickým balíčkům
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Elegantní zpracování chybějícího běhového prostředí XFA

Tři testy vám přesně řeknou, jakou kapacitu XFA máte, a poctivý kód je kontroluje, místo aby předpokládal. `XfaFeaturesAvailable` je globální kontrola, zda jsou v načtené DLL přítomny exporty pomocníka XFA/V8. `XfaStaticReadable` potvrzuje pro každý dokument, že se vyřešily čtečky balíčků — tedy ochrana pro extrakci. `XfaRuntimeAvailable` je ten nejsilnější: je true pouze tehdy, když tento konkrétní dokument skutečně aktivoval engine XFA, což znamená, že se jedná o XFA, načetlo se sestavení s V8 a volání `FPDF_LoadXFA` uspělo. Dokument může hlásit `XFA = True` a přitom `XfaRuntimeAvailable = False` pokaždé, když nasazená DLL postrádá XFA nebo když již byla načtena standardní knihovna

Když se dokument XFA otevře, ale engine nemůže běžet, komponenta PDFium neriskuje. Připne formulář k bezpečnému statickému režimu, místo aby požadovala dynamický runtime, který nemůže splnit, a jednou vyvolá událost `OnXfaRuntimeMissing`, takže hostitel může reagovat — obvykle tím, že uživateli řekne, aby aplikaci restartoval se sestavením s povoleným V8, a mezitím mu nabídne staticky čitelná data. Zapojení této obsluhy představuje rozdíl mezi čistým fallbackem a formulářem, který tichým způsobem nic nezobrazí

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Spustí se jednou, když se dokument XFA otevře, ale engine nemůže běžet:
  // obyčejná pdfium.dll již byla načtena, nebo sestavení postrádá XFA/V8.
  ShowMessage('Jedná se o dynamický formulář XFA. Restartujte aplikaci se sestavením s povoleným V8 pro jeho vykreslení; data jeho balíčků jsou stále čitelná.');
end;

Buďte upřímní ohledně této hranice i ve svém vlastním produktu. Pokud nikdy neexpedujete DLL s povoleným V8, dynamické formuláře XFA se vašim uživatelům nikdy nevykreslí — ale stále je můžete detekovat, extrahovat každý balíček a vytáhnout odeslaná data, což stačí pro velkou část úloh, které jsou ve skutečnosti o získávání dat ze starších vládních a bankovních formulářů, a nikoli o jejich interaktivní prezentaci. Těžší sestavení s V8 si rezervujte pro případy, které skutečně potřebují živý, vyplnitelný formulář, a nechte testy schopností nasměrovat každý dokument na cestu, kterou se může skutečně vydat. Pokud váš pipeline pro extrakci zpracovává také vložené soubory, stejná filozofie pouze pro čtení platí pro přílohy PDF v Delphi

Rozhraní API `FormType`, `XFA`, `GetXfaPacketCount`, `GetXfaPacketName`, `GetXfaPacketContent`, `GetXfaTemplate`, `GetXfaDatasets`, `GetXfaConfig` a testy schopností zde ukázané jsou součástí komponenty PDFium Component pro Delphi a C++Builder