Teknisk artikkel

Deteksjon og utpakking av XFA-skjemaer i Delphi med PDFium

Ja — PDFium støtter XFA-skjemaer, med ett skille. PDFium-komponenten kan detektere et XFA-skjema og pakke ut dets template-, datasets- og config-pakker som rå XML ved bruk av standard DLL, helt uten kjøretidsavhengigheter. Å kjøre et dynamisk XFA-skjema — der dens XML-beskrivelse legges ut på levende, interaktive sider — er et separat problem som i tillegg krever den V8-aktiverte versjonen. Fellesskapet fortsetter å spørre om PDFium "støtter XFA" som om det var en enkelt ja-eller-nei-funksjon, og det ærlige svaret er at lesing av XFA-data og rendring av et XFA-skjema er to svært forskjellige ting med to vidt forskjellige avhengighetsfotavtrykk

Denne artikkelen går gjennom begge halvdeler med PDFium-komponenten, den opprinnelige VCL-wrapperen rundt PDFium for Delphi og C++Builder. Først deteksjonen og den statiske utpakkingsbanen — FormType, XFA-egenskapen og GetXfaPacket*-leserne — deretter den dynamiske kjøretidsbanen med dens V8-begrensning, og til slutt funksjonssondene som lar deg feile grasiøst når en distribuert DLL ikke kan kjøre motoren

Støtter PDFium XFA-skjemaer?

Se på det som to funksjoner, ikke én. Statisk XFA-utpakking er alltid tilgjengelig: de lavnivå FPDF_GetXFAPacket*-eksporteringene som støtter GetXfaPacketCount, GetXfaPacketName og GetXfaPacketContent er kompilert inn i alle rimelig moderne pdfium.dll-filer, selv en versjon der XFA er slått av. Det er grunnen til at det å hente ut de rå template-, datasets- og config-XML-dataene fra et skjema ikke medfører noen kjøretidsavhengighet i det hele tatt — det er ren strukturell lesing, ingen motor er involvert. Dynamisk XFA – er den andre funksjonen: faktisk å kjøre skjemaet, evaluere dets beregninger og la XFA-layoutmotoren paginere det. Den banen eksisterer bare i en PDFium-versjon kompilert med XFA-støtte på toppen av V8 JavaScript-motoren, og det er den delen de fleste "støtter PDFium XFA"-tråder egentlig krangler om

Standardene rammer inn dette skillet på en ryddig måte. XFA er definert utenfor kjerne-PDF-grammatikken i Adobe XFA 3.3 (2012), og PDF 1.7 §8.6.7 beskriver hvordan et XFA-skjema bæres inne i en PDF: skjemapakkene ligger under AcroForm-ordbokens /XFA-oppføring, og et dokument som må legges ut av en XFA-prosessor før det betyr noe, setter /NeedsRendering true i katalogen. Disse to markørene er nøyaktig hva PDFium-komponenten ser etter, og nøyaktig hva du kan stole på når du vurderer en fil du aldri har sett før

Hvordan skiller XFA seg fra en AcroForm?

En AcroForm er det klassiske PDF-skjemaet: widget-annoteringer — tekstfelt, avmerkingsbokser, radiogrupper — forankret på faste koordinater på vanlige PDF-sider. Siden du ser er siden i filen, og feltene ligger på toppen av den. XFA tar motsatt tilnærming. Det interaktive skjemaet er beskrevet utelukkende i XML, og i et fullstendig (dynamisk) XFA-dokument er PDF-sidene i praksis bare en plassholder — det virkelige innholdet genereres ved åpning av en XFA-motor som leser XML-malen og legger den ut, og vokser eller krymper for å tilpasse seg dataene. Det er dette /NeedsRendering true kunngjør: uten en XFA-prosessor kan de statiske sidene være tomme eller bare inneholde en melding om å "åpne i en kompatibel leser"

Selve XFA-nyttelasten er et sett med navngitte pakker, og tre av dem inneholder alt de fleste integrasjoner trenger. Pakken template er skjemadefinisjonen — felt, layout, beregninger og skript. Pakken datasets inneholder de faktiske forekomstdataene, de utfylte feltverdiene som et XML-tre. Pakken config inneholder behandlingsinstruksjoner for XFA-motoren. Andre eksisterer også — localeSet, connectionSet, xdp-wrapperen — men for å revidere et skjema eller migrere dataene ut av det, er mal pluss datasett vanligvis hele jobben. Fordi disse dataene bare er XML som ligger i en strøm, krever lesing av dem aldri kjøring av noe, som er den grunnleggende årsaken til at statisk utpakking er avhengighetsfri

Detektere et XFA-skjema i Delphi

Deteksjon starter på samme måte som alle oppgaver med PDFium-komponenten gjør: sett FileName, vri Active := True, og sjekk at den faktisk åpnet seg. Active := True utløser aldri feil — en manglende fil, feil passord eller manglende DLL etterlater bare Active som False — så denne sjekken er obligatorisk. Når dokumentet er åpent, forteller FormType deg hvilken skjemamodell det bruker. To av TPdfFormType-verdiene er XFA: ftXfaFull er et fullstendig, dynamisk XFA-skjema, typen som krever rendring, mens ftXfaForeground er XFAF, forgrunnsdelsettet der XFA-innhold tegnes over ellers vanlige AcroForm-sider slik at de statiske sidene forblir meningsfulle. Boolske XFA er en snarvei for "dette er et XFA-dokument av enten den ene eller andre typen". Hvis du også jobber med felt på widget-nivå i et XFAF- eller AcroForm-dokument, er mekanismene dekket i veiledningen for navigering i PDFium-skjemafelt

uses
  SysUtils, PDFium;

procedure OpenDocument(const AFileName: string);
var
  Pdf: TPdf;
begin
  // Deploy pdfium.dll next to the executable, matched to the build target:
  //   <AppDir>\DLLs\Win32\pdfium.dll   for a 32-bit host process
  //   <AppDir>\DLLs\Win64\pdfium.dll   for a 64-bit host process
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := AFileName;
    try
      Pdf.Active := True;   // first use lazily binds pdfium.dll
    except
      on E: EPdfError do
        // The message already names the real cause: a 32/64-bit mismatch
        // (ERROR_BAD_EXE_FORMAT) or a pdfium.dll older than this binding
        raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
    end;
    if Pdf.Active then
      RenderFirstPage(Pdf);
  finally
    Pdf.Free;
  end;
end;

Pakk ut XFA-pakker som rå XML

Utpakking er den mest verdifulle, avhengighetsfrie halvdelen. Beskytt med XfaStaticReadable — det bekrefter at pakkeleserens eksporter ble løst i den lastede DLL-en — og gå deretter gjennom pakkene etter indeks. GetXfaPacketCount returnerer hvor mange pakker skjemaet bærer, GetXfaPacketName gir hver av dem sitt navn, og GetXfaPacketContent returnerer de rå bytene, som for de kjente pakkene er UTF-8 XML. Når du allerede vet hvilken pakke du vil ha, vil de navngitte hjelperne GetXfaTemplate, GetXfaDatasets og GetXfaConfig løse navnet for deg og levere TBytes direkte. For en datamigreringsjobb er den som betyr noe GetXfaDatasets: den gir deg de innsendte feltverdiene som et XML-dokument du kan analysere og tilordne til ditt eget skjema, samme type etterarbeid du ville gjort etter å ha pakket ut 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 trenger dynamisk XFA-rendring V8-motoren?

Konklusjonen først: fordi et dynamisk XFA-skjema er et lite program, ikke en statisk tegning. Malpakken bærer FormCalc- og JavaScript-beregninger, valideringer og hendelsesskript, og XFA-layoutmotoren må kjøre dem for å bestemme hvordan sidene i det hele tatt skal se ut. PDFium implementerer denne motoren bare i en versjon kompilert med XFA-støtte på toppen av V8, Googles JavaScript-motor — samme grunn til at den V8-aktiverte DLL-en (pdfium32v8.dll eller pdfium64v8.dll) er markert større enn standardversjonen. Uten V8 er det ingen til å kjøre skriptene, så det er ingen rendrede sider å vise, bare den rå XML-en du allerede kan lese statisk

Å velge denne versjonen har en streng begrensning du ikke kan omgå: det er en beslutning per prosess som gjøres én gang. En enkelt prosess kan ikke laste både standard pdfium.dll og den V8-aktiverte versjonen — de eksporterer de samme symbolene og V8 beholder den globale isolattilstanden — så PDFium-komponenten må velge V8-versjonen før biblioteket lastes for aller første gang. For å gjøre det automatisk, forhåndsskanner LoadDocument filen etter markørene /XFA og /NeedsRendering true (samme sjekk som eksponeres som den frittstående funksjonen PdfFileLooksXfa) og setter EnableV8Engine := True før innlasting når den finner dem. Når en vanlig pdfium.dll allerede er lastet inn, kan bryteren ikke lenger tre i kraft, og det er dette tilfellet som rapporteres av den manglende kjøretidsbanen

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;

Håndtere en manglende XFA-kjøretid på en ryddig måte

Tre sonder forteller deg nøyaktig hvor mye XFA-kapasitet du har, og ryddig kode sjekker dem i stedet for å anta. XfaFeaturesAvailable er en global sjekk på at hjelpeeksporteringene for XFA/V8 er til stede i den lastede DLL-en. XfaStaticReadable bekrefter, per dokument, at pakkeleserne ble løst — sjekken for utpakking. XfaRuntimeAvailable er den sterke sjekken: den er sann bare når dette spesifikke dokumentet faktisk aktiverte XFA-motoren, noe som betyr at det er XFA, V8-versjonen er lastet inn, og FPDF_LoadXFA var vellykket. Et dokument kan rapportere XFA = True, men likevel XfaRuntimeAvailable = False når den distribuerte DLL-en mangler XFA eller standardversjonen allerede var lastet inn

Når et XFA-dokument åpnes, men motoren ikke kan kjøre, tar ikke PDFium-komponenten noen sjanser. Den låser skjemaet til den sikre statiske modusen i stedet for å be om en dynamisk kjøretid den ikke kan oppfylle, og den utløser OnXfaRuntimeMissing én gang slik at verten kan reagere — vanligvis ved å be brukeren om å starte på nytt med den V8-aktiverte versjonen, samtidig som den fortsatt tilbyr de statisk lesbare dataene i mellomtiden. Å koble til den behandleren er forskjellen mellom et rent tilbakefall og et skjema som lydløst ikke viser noe

Vær ærlig om grensene i ditt eget produkt også. Hvis du aldri distribuerer den V8-aktiverte DLL-en, vil dynamiske XFA-skjemaer aldri rendres for brukerne dine — men du kan fortsatt oppdage dem, pakke ut hver pakke og hente ut de innsendte dataene, noe som er nok for den store klassen av jobber som egentlig handler om å hente data ut av eldre offentlige og bankskjemaer fremfor å presentere dem interaktivt. Reserver den tyngre V8-versjonen for de tilfellene som oppriktig trenger et levende, utfyllbart skjema, og la funksjonssondene rute hvert dokument til den banen det faktisk kan ta. Hvis utpakkingsrørledningen din også håndterer innebygde filer, gjelder den samme skrivebeskyttede filosofien for PDF-vedlegg i Delphi

API-ene for FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig og funksjonssondene som vises her er en del av PDFium Component for Delphi og C++Builder