Teknisk artikel

Detektering og ekstraktion af XFA-formularer i Delphi med PDFium

Ja — PDFium understøtter XFA-formularer, men med en vigtig opdeling. PDFium-komponenten kan detektere en XFA-formular og ekstrahere dens template-, datasets- og config-pakker som rå XML ved hjælp af standard-DLL'en med nul runtime-afhængigheder. At køre en dynamisk XFA-formular — altså at layoute dens XML-beskrivelse til levende, interaktive sider — er et separat problem, som derudover kræver det V8-aktiverede build. Fællesskabet bliver ved med at spørge, om PDFium "kan XFA", som om det var en enkelt ja-eller-nej-funktion, og det ærlige svar er, at læsning af XFA-data og rendering af en XFA-formular er to meget forskellige ting med to meget forskellige afhængighedsaftryk

Denne artikel gennemgår begge dele med PDFium-komponenten, den indfødte (native) VCL-wrapper omkring PDFium til Delphi og C++Builder. Først detektering og den statiske ekstraktionssti — FormType, XFA-egenskaben og GetXfaPacket*-læserne — derefter den dynamiske runtime-sti med dens V8-begrænsning og til sidst de funktionstjek (capability probes), der lader dig fejle yndefuldt, når en udrullet DLL ikke kan køre motoren

Understøtter PDFium XFA-formularer?

Betragt det som to funktioner, ikke én. Statisk XFA-ekstraktion er altid tilgængelig: De lavniveaueksport-funktioner FPDF_GetXFAPacket*, der understøtter GetXfaPacketCount, GetXfaPacketName og GetXfaPacketContent, er kompileret ind i enhver rimeligt moderne pdfium.dll, selv i et build med XFA deaktiveret. Derfor medfører det overhovedet ingen runtime-afhængighed at trække den rå XML for template, datasets og config ud af en formular — det er ren strukturel læsning, hvor ingen motor er involveret. Dynamisk XFA er den anden funktion: Rent faktisk at køre formularen, evaluere dens beregninger og lade XFA-layoutmotoren paginere den. Denne sti eksisterer kun i et PDFium-build kompileret med XFA-understøttelse oven på V8 JavaScript-motoren, og det er den del, de fleste diskussioner om "understøtter PDFium XFA" reelt handler om

Standarderne rammer opdelingen rent ind. XFA er defineret uden for PDF'ens kerne-grammatik i Adobe XFA 3.3 (2012), og PDF 1.7 §8.6.7 beskriver, hvordan en XFA-formular bæres inde i en PDF: Formularpakkerne lever under AcroForm-ordbogens /XFA-indgang, og et dokument, der skal layoutes af en XFA-processor, før det giver mening, sætter /NeedsRendering true i kataloget. Disse to markører er præcis, hvad PDFium-komponenten leder efter, og præcis det, du kan stole på, når du ræsonnerer om en fil, du aldrig har set før

Hvordan adskiller XFA sig fra en AcroForm?

En AcroForm er den klassiske PDF-formular: Widget-annoteringer — tekstfelter, afkrydsningsfelter, radiogrupper — forankret ved faste koordinater på almindelige PDF-sider. Den side, du ser, er den side, der ligger i filen, og felterne ligger oven på den. XFA vælger den modsatte tilgang. Den interaktive formular er beskrevet fuldstændigt i XML, og i et fuldt (dynamisk) XFA-dokument er PDF-siderne reelt en pladsholder — det rigtige indhold genereres ved åbning af en XFA-motor, der læser XML-skabelonen og layouter den, så den vokser eller krymper for at passe til dataene. Det er det, /NeedsRendering true meddeler: Uden en XFA-processor kan de statiske sider være tomme eller blot indeholde en meddelelse om at "åbne i en kompatibel fremviser"

Selve XFA-payloaden er et sæt navngivne pakker, og tre af dem indeholder alt, hvad de fleste integrationer har brug for. Pakken template er formular-definitionen — felter, layout, beregninger og scripts. Pakken datasets indeholder de faktiske forekomstsdata, de udfyldte feltværdier som et XML-træ. Pakken config indeholder behandlingsinstruktioner til XFA-motoren. Andre eksisterer — localeSet, connectionSet, xdp-wrapperen — men til revision af en formular eller migrering af data ud af den, er template plus datasets normalt hele opgaven. Da disse data blot er XML, der ligger i en strøm, kræver læsning af dem aldrig kørsel af noget som helst, hvilket er den underliggende årsag til, at statisk ekstraktion er helt uafhængig af andre komponenter

Detektering af en XFA-formular i Delphi

Detektering starter på samme måde som enhver anden opgave i PDFium-komponenten: Sæt FileName, slå Active := True til, og kontroller, at den rent faktisk åbnede. Active := True udløser aldrig fejl — en manglende fil, en forkert adgangskode eller en manglende DLL efterlader blot Active som False — så denne kontrol er obligatorisk. Når dokumentet er åbent, fortæller FormType dig, hvilken formularmodel det bruger. To af TPdfFormType-værdierne er XFA: ftXfaFull er en fuld, dynamisk XFA-formular, den slags der kræver rendering, mens ftXfaForeground er XFAF, forgrunds-delmængden, hvor XFA-indhold tegnes over ellers normale AcroForm-sider, så de statiske sider forbliver meningsfulde. XFA-booleantypen er en genvej til "dette er et XFA-dokument af en af typerne". Hvis du også arbejder med felter på widget-niveau på et XFAF- eller AcroForm-dokument, er mekanismerne dækket i vejledningen til PDFium formularfelt-navigation

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;

Ekstraktion af XFA-pakker som rå XML

Ekstraktion er den værdifulde del, der ikke kræver nogen eksterne afhængigheder. Beskyt med XfaStaticReadable — det bekræfter, at pakkelæserens eksporter blev fundet i den indlæste DLL — og gennemgå derefter pakkerne efter indeks. GetXfaPacketCount returnerer, hvor mange pakker formularen indeholder, GetXfaPacketName giver hver enkelt sit navn, og GetXfaPacketContent returnerer de rå bytes, som for de velkendes pakker er UTF-8 XML. Når du allerede ved, hvilken pakke du vil have, finder de navngivne hjælpere GetXfaTemplate, GetXfaDatasets og GetXfaConfig navnet for dig og afleverer TBytes direkte. For et datamigreringsjob er det GetXfaDatasets, der betyder noget: Det giver dig de indsendte feltværdier som et XML-dokument, du kan parse og tilknytte dit eget skema, det samme arbejde som du ville gøre efter ekstraktion af tekst fra en 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;

Hvorfor kræver dynamisk XFA-rendering V8-motoren?

Konklusionen først: Fordi en dynamisk XFA-formular er et lille program, ikke en statisk tegning. Skabelon-pakken (template) bærer FormCalc- og JavaScript-beregninger, valideringer samt hændelses-scripts, og XFA-layoutmotoren skal udføre dem for overhovedet at bestemme, hvordan siderne skal se ud. PDFium implementerer kun denne motor i et build, der er kompileret med XFA-understøttelse oven på V8, Googles JavaScript-motor — hvilket også er årsagen til, at den V8-aktiverede DLL (pdfium32v8.dll eller pdfium64v8.dll) er markant større end standard-buildet. Uden V8 er der ingen til at køre scripterne, så der er ingen renderede sider at vise, kun den rå XML, som du allerede kan læse statisk

Valget af dette build har en hård begrænsning, du ikke kan designe dig udenom: Det er en beslutning, der tages én gang pr. proces. En enkelt proces kan ikke indlæse både standard-pdfium.dll og det V8-aktiverede build — de eksporterer de samme symboler, og V8 bevarer en global isolate-tilstand — så PDFium-komponenten skal vælge V8-buildet, før biblioteket indlæses allerførste gang. For at gøre det automatisk scanner LoadDocument filen på forhånd efter markørerne /XFA og /NeedsRendering true (det samme tjek, som er eksponeret som den selvstændige funktion PdfFileLooksXfa) og sætter EnableV8Engine := True før indlæsningen, hvis den finder dem. Når en almindelig pdfium.dll først er indlæst, kan skiftet ikke længere træde i kraft, og det er netop det tilfælde, som runtime-missing-stien rapporterer

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;

Elegant håndtering af en manglende XFA-runtime

Tre tjek (probes) fortæller dig præcis, hvor meget XFA-kapacitet du har, og ordentlig kode kontrollerer dem i stedet for at antage noget. XfaFeaturesAvailable er et globalt tjek af, at XFA/V8-hjælpeeksporterne er til stede i den indlæste DLL. XfaStaticReadable bekræfter pr. dokument, at pakkelæserne blev fundet — det vil sige beskyttelsen for ekstraktion. XfaRuntimeAvailable er det afgørende: Det er kun sandt, når dette specifikke dokument rent faktisk aktiverede XFA-motoren, hvilket betyder, at det er XFA, V8-buildet blev indlæst, og FPDF_LoadXFA lykkedes. Et dokument kan rapportere XFA = True, men XfaRuntimeAvailable = False, når den udrullede DLL mangler XFA, eller standard-buildet allerede var indlæst

Når et XFA-dokument åbnes, men motoren ikke kan køre, tager PDFium-komponenten ingen chancer. Den låser formularen til den sikre statiske tilstand i stedet for at anmode om en dynamisk runtime, den ikke kan opfylde, og den udløser OnXfaRuntimeMissing én gang, så værten kan reagere — typisk ved at fortælle brugeren, at de skal genstarte med det V8-aktiverede build, mens den stadig tilbyder de statisk læsbare data i mellemtiden. At forbinde denne hændelse (handler) er forskellen på et rent fallback-scenarie og en formular, der i stilhed intet viser

Vær også ærlig om denne grænse i dit eget produkt. Hvis du aldrig leverer den V8-aktiverede DLL, vil dynamiske XFA-formularer aldrig kunne renderes for dine brugere — men du kan stadig detektere dem, ekstrahere hver pakke og trække de indsendte data ud, hvilket er tilstrækkeligt til den store klasse af opgaver, der reelt handler om at hente data ud af ældre offentlige blanketter eller bankformularer frem for at præsentere dem interaktivt. Reserver det tungere V8-build til de tilfælde, der reelt kræver en aktiv formular, der kan udfyldes, og lad funktionstjekkene rute hvert dokument til den sti, det faktisk kan tage. Hvis din ekstraktions-pipeline også håndterer indlejrede filer, gælder den samme skrivebeskyttede filosofi for PDF-vedhæftninger i Delphi

De API'er for FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig samt funktionstjek vist her er en del af PDFium-komponenten til Delphi og C++Builder