Teknisk artikel

Find og udtræk XFA-formularer i Delphi med PDFium

Ja — PDFium understøtter XFA-formularer, med én vigtig opdeling. PDFium Component kan finde en XFA-formular og udtrække dens template-, datasets- og config-pakker som rå XML med standard-DLL-filen, uden nogen runtime-afhængighed. At køre en dynamisk XFA-formular — altså at sætte dens XML-beskrivelse op som levende, interaktive sider — er et separat problem, der derudover kræver den V8-aktiverede build. Fællesskabet bliver ved med at spørge, om PDFium "kan XFA", som om det var én ja-eller-nej-egenskab, og det ærlige svar er, at det at læse XFA-data og det at gengive en XFA-formular er to meget forskellige ting med to meget forskellige afhængighedsaftryk

Denne artikel gennemgår begge halvdele med PDFium Component, den native VCL-indpakning omkring PDFium til Delphi og C++Builder. Først vejen til detektion og statisk udtræk — FormType, XFA-egenskaben og GetXfaPacket*-læserne — dernæst den dynamiske runtime-vej med dens V8-begrænsning, og til sidst de egenskabssonder, der lader dig fejle elegant, når en udrullet DLL ikke kan køre motoren

Én PDF med XFA-pakker føder to PDFium-egenskaber i Delphi: statisk pakkeudtræk, der kører på enhver moderne DLL, og dynamisk gengivelse, der derudover kræver den V8-aktiverede build
Statisk udtræk læser pakkerne template, datasets og config som ren XML uden nogen motor, mens dynamisk XFA-gengivelse derudover kræver den V8-aktiverede PDFium-build

Understøtter PDFium XFA-formularer?

Læs det som to egenskaber, ikke én. Statisk XFA-udtræk er altid tilgængeligt: de lavniveau-eksporter FPDF_GetXFAPacket*, der ligger bag GetXfaPacketCount, GetXfaPacketName og GetXfaPacketContent, er kompileret ind i enhver nogenlunde moderne pdfium.dll, selv en build med XFA slået fra. Derfor bærer det at trække den rå template-, datasets- og config-XML ud af en formular slet ingen runtime-afhængighed — det er ren strukturel læsning, uden nogen motor involveret. Dynamisk XFA er den anden egenskab: rent faktisk at køre formularen, evaluere dens beregninger og lade XFA-layoutmotoren paginere den. Den vej findes kun i en PDFium-build kompileret med XFA-understøttelse oven på V8-JavaScript-motoren, og det er den del, de fleste tråde om "understøtter PDFium XFA" i virkeligheden skændes om

Standarderne rammer opdelingen rent. XFA er defineret uden for den centrale PDF-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 bor under AcroForm-ordbogens /XFA-post, og et dokument, der skal sættes op af en XFA-processor, før det betyder noget, sætter /NeedsRendering true i kataloget. Netop de to markører er det, PDFium Component leder efter, og netop 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-annotationer — tekstfelter, afkrydsningsfelter, radiogrupper — forankret på faste koordinater på almindelige PDF-sider. Den side, du ser, er siden i filen, og felterne sidder oven på den. XFA gør det stik modsatte. Den interaktive formular er beskrevet helt i XML, og i et fuldt (dynamisk) XFA-dokument er PDF-siderne reelt en pladsholder — det virkelige indhold genereres ved åbning af en XFA-motor, der læser XML-skabelonen og sætter den op, så den vokser eller skrumper efter dataene. Det er det, /NeedsRendering true bekendtgør: uden en XFA-processor kan de statiske sider være blanke eller kun bære en besked om at åbne filen i en kompatibel fremviser

Selve XFA-nyttelasten er et sæt navngivne pakker, og tre af dem bærer alt, hvad de fleste integrationer har brug for. template-pakken er formulardefinitionen — felter, layout, beregninger og scripts. datasets-pakken rummer de faktiske instansdata, altså de udfyldte feltværdier som et XML-træ. config-pakken bærer behandlingsinstruktioner til XFA-motoren. Der findes andre — localeSet, connectionSet, xdp-indpakningen — men til at auditere en formular eller migrere dataene ud af den er template plus datasets som regel hele opgaven. Fordi de data blot er XML, der ligger i en strøm, kræver det aldrig at eksekvere noget at læse dem, og det er den grundlæggende årsag til, at statisk udtræk er afhængighedsfrit

AcroForm-widgets forankret på faste sidekoordinater over for en XFA-formular i PDFium, hvis pakker template, datasets og config sættes op af en XFA-motor under /NeedsRendering true
AcroForm-felter sidder på færdige sider, mens en XFA-formular bærer sin definition som navngivne XML-pakker under AcroForm-postens /XFA og kan sætte /NeedsRendering true

Sådan finder du en XFA-formular i Delphi

Detektion starter, som enhver PDFium Component-opgave gør: sæt FileName, slå Active := True til, og tjek, at den faktisk blev åbnet. Active := True rejser aldrig en fejl — en manglende fil, en forkert adgangskode eller en fraværende DLL efterlader bare Active på False — så vagten er obligatorisk. Når dokumentet er åbent, fortæller FormType dig, hvilken formularmodel det bruger. To af værdierne i TPdfFormType er XFA: ftXfaFull er en fuld, dynamisk XFA-formular, den slags der kræver gengivelse, mens ftXfaForeground er XFAF, forgrundsdelmængden hvor XFA-indhold tegnes oven på ellers normale AcroForm-sider, så de statiske sider forbliver meningsfulde. Den boolske XFA er en genvej til "dette er et XFA-dokument af den ene eller anden slags". Arbejder du også med felterne på widget-niveau i et XFAF- eller AcroForm-dokument, er mekanikken dækket i guiden til navigation i PDFium-formularfelter

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'application-form.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
      Exit;                        // beskadiget, krypteret eller manglende 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;

Sådan udtrækker du XFA-pakker som rå XML

Udtræk er den værdifulde, afhængighedsfrie halvdel. Sæt vagt med XfaStaticReadable — den bekræfter, at pakkelæserens eksporter blev fundet i den indlæste DLL — og gennemløb derefter pakkerne efter indeks. GetXfaPacketCount returnerer, hvor mange pakker formularen bærer, GetXfaPacketName giver hver enkelt sit navn, og GetXfaPacketContent returnerer de rå bytes, som for de velkendte pakker er UTF-8-XML. Ved du allerede, hvilken pakke du vil have, opløser de navngivne hjælpere GetXfaTemplate, GetXfaDatasets og GetXfaConfig navnet for dig og rækker TBytes tilbage direkte. Til en datamigreringsopgave er den, der betyder noget, GetXfaDatasets: den giver dig de indsendte feltværdier som et XML-dokument, du kan parse og mappe ind i dit eget skema, altså den samme slags efterfølgende arbejde, du ville lave efter at have udtrukket 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;                          // pakkelæserens eksporter er ikke til stede
  Count := Pdf.GetXfaPacketCount;
  for I := 0 to Count - 1 do
  begin
    PacketName := Pdf.GetXfaPacketName(I);
    Content := Pdf.GetXfaPacketContent(I);   // rå UTF-8-XML
    TFile.WriteAllBytes(
      TPath.Combine(Dir, PacketName + '.xml'), Content);
  end;

  // Eller spring direkte til de udfyldte feltværdier via navn:
  Datasets := Pdf.GetXfaDatasets;
  if Length(Datasets) > 0 then
    ParseAndMap(Datasets);
end;

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

Konklusionen først: fordi en dynamisk XFA-formular er et lille program, ikke en statisk tegning. Template-pakken bærer FormCalc- og JavaScript-beregninger, valideringer og hændelsesscripts, og XFA-layoutmotoren skal eksekvere dem for overhovedet at afgøre, hvordan siderne ser ud. PDFium implementerer kun den motor i en build kompileret med XFA-understøttelse oven på V8, Googles JavaScript-motor — og det er også grunden til, at den V8-aktiverede DLL (pdfium32v8.dll eller pdfium64v8.dll) er markant større end standardbuilden. Uden V8 er der ingen til at køre scriptene, så der er ingen gengivne sider at vise, kun den rå XML, du allerede kan læse statisk

At vælge den build har en hård begrænsning, du ikke kan designe dig uden om: det er en engangsbeslutning pr. proces. En enkelt proces kan ikke indlæse både standardudgaven af pdfium.dll og den V8-aktiverede build — de eksporterer de samme symboler, og V8 holder global isolattilstand — så PDFium Component skal vælge V8-builden, før biblioteket indlæses allerførste gang. For at gøre det automatisk forhåndsscanner LoadDocument filen for markørerne /XFA og /NeedsRendering true (det samme tjek, der er eksponeret som den fritstående funktion PdfFileLooksXfa) og sætter EnableV8Engine := True før indlæsningen, når den finder dem. Når en almindelig pdfium.dll allerede er resident, kan skiftet ikke længere træde i kraft, og netop det tilfælde er det, vejen for manglende runtime melder

begin
  // Kig efter XFA, før PDFium indlæses første gang, så den
  // V8-aktiverede build stadig kan vælges. PdfFileLooksXfa scanner efter
  // /XFA og /NeedsRendering true uden at parse hele filen.
  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)          // motoren er levende: dynamisk layout
      else
        ExtractXfaData(Pdf, 'C:\out'); // fald tilbage til statiske pakker
    end;
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

Sådan håndterer du en manglende XFA-runtime elegant

Tre sonder fortæller dig præcis, hvor meget XFA-kapacitet du har, og ærlig kode tjekker dem i stedet for at antage. XfaFeaturesAvailable er et globalt tjek af, at hjælpereksporterne til XFA/V8 er til stede i den indlæste DLL. XfaStaticReadable bekræfter, pr. dokument, at pakkelæserne blev fundet — vagten til udtræk. XfaRuntimeAvailable er den stærke: den er kun sand, når netop dette dokument rent faktisk aktiverede XFA-motoren, altså at det er XFA, at V8-builden blev indlæst, og at FPDF_LoadXFA lykkedes. Et dokument kan melde XFA = True og alligevel XfaRuntimeAvailable = False, når den udrullede DLL mangler XFA, eller standardbuilden allerede var indlæst

Når et XFA-dokument åbner, men motoren ikke kan køre, spiller PDFium Component ikke hasard. Den fastlåser formularen til den sikre statiske tilstand i stedet for at bede om en dynamisk runtime, den ikke kan indfri, og den udløser OnXfaRuntimeMissing én gang, så værten kan reagere — typisk ved at bede brugeren genstarte med den V8-aktiverede build, mens den stadig tilbyder de statisk læsbare data i mellemtiden. At koble den hændelseshåndtering op er forskellen mellem et rent tilbagefald og en formular, der lydløst intet viser

Beslutningsforløb i Delphi for et dynamisk XFA-dokument i PDFium, der sonderer XfaRuntimeAvailable for at gengive med V8 eller fastlåse statisk tilstand, udløse OnXfaRuntimeMissing én gang og stadig udtrække pakkerne
Når XfaRuntimeAvailable er sand, gengiver V8-motoren levende sider, ellers fastlåses formularen til statisk tilstand, OnXfaRuntimeMissing udløses én gang, og pakkerne forbliver læsbare
procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Udløses én gang, når et XFA-dokument åbner, men motoren ikke kan køre:
  // en almindelig pdfium.dll var allerede indlæst, eller builden mangler 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;

Vær også ærlig om grænsen i dit eget produkt. Leverer du aldrig den V8-aktiverede DLL, vil dynamiske XFA-formularer aldrig blive gengivet for dine brugere — men du kan stadig finde dem, udtrække hver eneste pakke og hive de indsendte data ud, hvilket er nok til den store klasse af opgaver, der i virkeligheden handler om at få data ud af gamle formularer fra det offentlige og bankerne frem for at præsentere dem interaktivt. Reservér den tungere V8-build til de tilfælde, der oprigtigt kræver en levende, udfyldbar formular, og lad egenskabssonderne dirigere hvert dokument hen på den vej, det rent faktisk kan tage. Håndterer din udtrækspipeline også indlejrede filer, gælder den samme skrivebeskyttede filosofi for PDF-vedhæftninger i Delphi

API'erne FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig og egenskabssonderne, der er vist her, er en del af PDFium Component til Delphi og C++Builder