Tehnični članek

Zaznavanje in ekstrakcija obrazcev XFA v Delphi s PDFium

Da — PDFium podpira obrazce XFA, z eno pomembno ločitvijo. Komponenta PDFium Component lahko zazna obrazec XFA in ekstrahira njegove pakete template, datasets in config kopije kot surovi XML z uporabo standardne knjižnice DLL, z ničelno odvisnostjo med delovanjem. Izvajanje dinamičnega obrazca XFA — postavitev njegovega XML opisa v žive, interaktivne strani — pa je ločen problem, ki dodatno potrebuje različico z omogočenim motorjem V8. Skupnost nenehno sprašuje, ali PDFium "podpira XFA", kot da bi šlo za eno samo možnost z odgovorom da ali ne, pošten odgovor pa je, da sta branje podatkov XFA in upodabljanje obrazca XFA dve zelo različni stvari z zelo različnima odvisnostnima odtisoma

Ta članek obravnava obe polovici s komponento PDFium Component, domačim VCL-ovim ovitkom okoli PDFium za Delphi in C++Builder. Najprej pot zaznavanja in statične ekstrakcije — FormType, lastnost XFA in bralniki GetXfaPacket* —, nato pot dinamičnega izvajanja z njeno omejitvijo V8, na koncu pa še preizkuse zmogljivosti, ki vam omogočajo dostojno odstopanje, ko nameščeni DLL ne more zagnati motorja

Ali PDFium podpira obrazce XFA?

Razumevajte to kot dve zmogljivosti in ne kot eno. Statična ekstrakcija XFA je vedno na voljo: nizkonivojski izvozi FPDF_GetXFAPacket*, na katerih slonijo GetXfaPacketCount, GetXfaPacketName in GetXfaPacketContent, so prevedeni v vsak razmeroma sodoben pdfium.dll, celo v različico z izklopljeno podporo za XFA. Zato vlečenje surovega XML-ja paketov template, datasets in config iz obrazca sploh ne prinaša odvisnosti med delovanjem — gre za čisto strukturno branje brez vpletenega motorja. Dinamični XFA pa je druga zmogljivost: dejansko izvajanje obrazca, vrednotenje njegovih izračunov in prepustitev strani motorju za postavitev XFA. Ta pot obstaja le v različici PDFium, prevedeni s podporo za XFA na vrhu JavaScript motorja V8, in to je tisti del, o katerem se večina niti z vprašanjem "ali PDFium podpira XFA" dejansko prepira

Standardi to delitev uokvirjajo čisto. XFA je določen izven jedrne slovnice PDF v Adobe XFA 3.3 (2012), PDF 1.7 §8.6.7 pa opisuje, kako se obrazec XFA prenaša znotraj PDF-ja: paketi obrazca živijo pod vnosom /XFA v slovarju AcroForm, dokument, ki ga mora procesor XFA razpostaviti, preden sploh kaj pomeni, pa v katalogu nastavi /NeedsRendering true. Ta dva označevalca sta natanko tisto, kar išče komponenta PDFium Component, in natanko tisto, na kar se lahko zanesete, ko razmišljate o datoteki, ki je niste še nikoli videli

Kako se XFA razlikuje od AcroForm-a?

AcroForm je klasičen obrazec PDF: kontrolniki opomb — besedilna polja, potrditvena polja, radijske skupine —, zasidrani na fiksnih koordinatah na običajnih straneh PDF. Stran, ki jo vidite, je stran v datoteki, polja pa sedijo na njenem vrhu. XFA ubira nasprotni pristop. Interaktivni obrazec je v celoti opisan v XML-ju, v celotnem (dinamičnem) dokumentu XFA pa so strani PDF dejansko le placeholder — realna vsebina se ustvari ob odpiranju s strani motorja XFA, ki prebere predlogo XML in jo razpostavi ter jo razširi ali skrči, da ustreza podatkom. To je tisto, kar sporoča /NeedsRendering true: brez procesorja XFA so statične strani lahko prazne ali pa prenašajo le obvestilo "prosimo, odprite v združljivem pregledovalniku"

Sam tovor XFA je nabor poimenovanih paketov, trije od njih pa prenašajo vse, kar večina integracij potrebuje. Paket template je definicija obrazca — polja, postavitev, izračuni in skripte. Paket datasets drži dejanske podatke instance, izpolnjene vrednosti polj kot drevo XML. Paket config pa prenaša navodila za obdelavo za motor XFA. Obstajajo tudi drugi — localeSet, connectionSet, ovitek xdp —, toda za revizijo obrazca ali selitev podatkov iz njega je predloga plus podatkovni nabor običajno celotno delo. Ker so ti podatki le XML, ki sedi v toku, branje nikoli ne zahteva izvajanja česar koli, kar je glavni razlog, da je statična ekstrakcija brez odvisnosti

Zaznavanje obrazca XFA v Delphi

Zaznavanje se začne tako, kot vsako opravilo s komponento PDFium Component: nastavite FileName, preklopite Active := True in preverite, ali se je dejansko odprlo. Active := True nikoli ne sproži izjeme — manjkajoča datoteka, napačno geslo ali odsoten DLL le pustijo Active na False —, zato je to preverjanje obvezno. Ko je dokument odprt, vam FormType pove, kateri model obrazca uporablja. Dve od vrednosti TPdfFormType sta XFA: ftXfaFull je celoten, dinamičen obrazec XFA, tisti, ki potrebuje upodabljanje, medtem ko je ftXfaForeground XFAF, prednja podmnožica, kjer se vsebina XFA izriše čez sicer običajne strani AcroForm, tako da statične strani ostanejo pomenljive. Logična vrednost XFA je bližnjica za "to je dokument XFA katere koli različice". Če delate tudi s polji na ravni kontrolnikov na dokumentu XFAF ali AcroForm, je mehanika zajeta v vodniku za navigacijo po poljih obrazcev PDFium

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;

Ekstrahiranje paketov XFA kot surovi XML

Ekstrakcija je tista vredna polovica brez odvisnosti. Varujte jo z XfaStaticReadable — potrjuje, da so se izvozi bralnika paketov razrešili v naloženem DLL-ju —, nato pa se sprehodite skozi pakete po indeksu. GetXfaPacketCount vrne, koliko paketov prenaša obrazec, GetXfaPacketName da vsakemu svoje ime, GetXfaPacketContent pa vrne surove bajte, kar so za znane pakete UTF-8 XML. Ko že veste, kateri paket želite, poimenovani pomočniki GetXfaTemplate, GetXfaDatasets in GetXfaConfig razrešijo ime namesto vas in vrnejo TBytes neposredno. Za delo pri selitvi podatkov je tisti, ki je pomemben, GetXfaDatasets: da vam oddane vrednosti polj kot dokument XML, ki ga lahko razčlenite in preslikate v lastno shemo, kar je enako delo, kot bi ga opravili po ekstrakciji besedila iz PDF-ja

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;

Zakaj dinamično upodabljanje XFA potrebuje motor V8?

Najprej zaključek: ker je dinamičen obrazec XFA majhen program in ne statična risba. Paket s predlogo prenaša izračune FormCalc in JavaScript, validacije ter skripte dogodkov, motor za postavitev XFA pa jih mora izvesti, da se sploh odloči, kako so strani videti. PDFium izvaja ta motor le v različici, prevedeni s podporo za XFA na vrhu V8, Googlovega motorja JavaScript — to je enak razlog, zakaj je DLL z omogočenim V8 (pdfium32v8.dll or pdfium64v8.dll) opazno večji od standardne različice. Brez V8 ni nikogar, ki bi zagnal skripte, zato ni izrisanih strani za prikaz, le surovi XML, ki ga že lahko preberete statično

Izbira te različice ima trdo omejitev, okoli katere ne morete načrtovati: to je odločitev na nivoju procesa, ki se sprejme enkrat. En sam proces ne more naložiti hkrati standardnega pdfium.dll in različice z omogočenim V8 — izvažata iste simbole, V8 pa ohranja globalno stanje izolata —, zato mora komponenta PDFium Component izbrati različico V8, preden se knjižnica sploh prvič naloži. Da bi bilo to samodejno, LoadDocument vnaprej pregleda datoteko za označevalca /XFA in /NeedsRendering true (enako preverjanje, izpostavljeno kot samostojna funkcija PdfFileLooksXfa), in nastavi EnableV8Engine := True pred nalaganjem, ko ju najde. Ko je običajen pdfium.dll že prisoten v pomnilniku, preklop ne more več začeti veljati in ta primer poroča pot za manjkajoči runtime

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;

Dostojno ravnanje z manjkajočim XFA runtime-om

Trije preizkusi vam povedo natanko to, koliko zmogljivosti XFA imate, in poštena koda jih preveri, namesto da bi predvidevala. XfaFeaturesAvailable je globalno preverjanje, ali so izvozi pomočnikov XFA/V8 prisotni v naloženem DLL-ju. XfaStaticReadable potrjuje na dokument, da so se bralniki paketov razrešili — zaščita za ekstrakcijo. XfaRuntimeAvailable pa je tisti močni: True je le takrat, ko je ta določen dokument dejansko aktiviral motor XFA, kar pomeni, da gre za XFA, da se je različica V8 naložila in je FPDF_LoadXFA uspel. Dokument lahko poroča XFA = True in hkrati XfaRuntimeAvailable = False kadar koli nameščeni DLL nima XFA ali pa je bila standardna različica že naložena

Ko se dokument XFA odpre, vendar motor ne more teči, komponenta PDFium Component ne tvega. Obrazec pripne na varen statični način in ne zahteva dinamičnega časa delovanja, ki ga ne more izpolniti, ter enkrat sproži dogodek OnXfaRuntimeMissing, da se gostitelj lahko odzove — običajno tako, da uporabniku pove, naj znova zažene z različico z omogočenim V8, medtem ko vmes še vedno ponuja statično berljive podatke. Povezovanje tega rokovalnika je razlika med čistim odstopom in obrazcem, ki tiho ne prikazuje ničesar

procedure TForm1.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Fires once when an XFA document opens but the engine cannot run:
  // a plain pdfium.dll was already loaded, or the build lacks 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;

Bodite pošteni glede teh meja tudi v lastnem izdelku

Bodite pošteni glede teh meja tudi v lastnem izdelku. Če nikoli ne pošljete DLL-ja z omogočenim V8, se dinamični obrazci XFA nikoli ne bodo izrisali za vaše uporabnike — vendar jih še vedno lahko zaznate, ekstrahirate vsak paket in izvlečete oddane podatke, kar zadostuje za velik razred opravil, ki so dejansko namenjena pridobivanju podatkov iz zapuščenih državnih in bančnih obrazcev namesto njihove interaktivne predstavitve. Rezervirajte težjo različico V8 za primere, ki resnično potrebujejo živ, izpolnljiv obrazec, preizkusi zmogljivosti pa naj usmerijo vsak dokument na pot, ki jo dejansko lahko opravi. Če vaš cevovod za ekstrakcijo obravnava tudi vgrajene datoteke, velja enaka filozofija le za branje za priloge PDF v Delphi

API-ji FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig in preizkusi zmogljivosti, prikazani tukaj, so del komponente PDFium Component za Delphi in C++Builder