Tehnički članak

Otkrivanje i izdvajanje XFA obrazaca u Delphiju pomoću PDFiuma

Da — PDFium podržava XFA obrasce, ali uz jednu važnu podjelu. PDFium komponenta može otkriti XFA obrazac i izdvojiti njegove pakete template, datasets i config kao sirovi XML koristeći standardni DLL, bez ikakvih ovisnosti o vremenu izvršavanja (runtime). Pokretanje dinamičkog XFA obrasca — raspoređivanje njegovog XML opisa u aktivne, interaktivne stranice — zaseban je problem koji dodatno zahtijeva verziju s omogućenim V8 pokretačem. Zajednica često pita "podržava li PDFium XFA" kao da je riječ o jednoj jedinoj da-ili-ne mogućnosti, a iskren odgovor je da su čitanje XFA podataka i iscrtavanje XFA obrasca dvije vrlo različite stvari s vrlo različitim zahtjevima ovisnosti

Ovaj članak prolazi kroz obje polovice s PDFium komponentom, izvornim VCL omotačem oko PDFiuma za Delphi i C++Builder. Najprije putanju otkrivanja i statičkog izdvajanja — FormType, svojstvo XFA i čitače GetXfaPacket* — zatim dinamičku putanju izvršavanja s njezinim V8 ograničenjem, te na kraju ispitivanja mogućnosti koja vam omogućuju da elegantno postupite kada implementirani DLL ne može pokrenuti taj mehanizam

Podržava li PDFium XFA obrasce?

Shvatite to kao dvije mogućnosti, a ne jednu. Statičko izdvajanje XFA-a uvijek je dostupno: izvozi niske razine FPDF_GetXFAPacket* koji podržavaju GetXfaPacketCount, GetXfaPacketName i GetXfaPacketContent kompajlirani su u svaki iole moderniji pdfium.dll, čak i u verziju s isključenim XFA-om. Zbog toga izvlačenje sirovog XML-a za template, datasets i config iz obrasca ne donosi nikakvu ovisnost o vremenu izvršavanja — to je čisto strukturno čitanje bez sudjelovanja pokretača. Dinamički XFA je druga mogućnost: stvarno pokretanje obrasca, procjena njegovih izračuna i omogućavanje XFA mehanizmu rasporeda da ga prelama na stranice. Ta putanja postoji samo u PDFium verziji kompajliranoj s podrškom za XFA na vrhu V8 JavaScript pokretača, i to je dio oko kojeg se zapravo raspravlja u većini rasprava o tome podržava li PDFium XFA

Standardi jasno uokviruju ovu podjelu. XFA je definiran izvan temeljne PDF gramatike u Adobe XFA 3.3 (2012), a PDF 1.7 §8.6.7 opisuje kako se XFA obrazac prenosi unutar PDF-a: paketi obrasca žive pod unosem /XFA u rječniku AcroForm, a dokument koji XFA procesor mora rasporediti prije nego što išta znači postavlja /NeedsRendering true u katalogu. Ta dva markera su točno ono što PDFium komponenta traži i točno ono na što se možete osloniti kada analizirate datoteku koju nikada prije niste vidjeli

Kako se XFA razlikuje od AcroForma?

AcroForm je klasični PDF obrazac: widget bilješke — tekstualna polja, potvrdni okviri, radio grupe — usidreni na fiksnim koordinatama na običnim PDF stranicama. Stranica koju vidite je stranica u datoteci, a polja leže na njoj. XFA ima suprotan pristup. Interaktivni obrazac je u potpunosti opisan u XML-u, a u punom (dinamičkom) XFA dokumentu PDF stranice su zapravo rezervirano mjesto — stvarni sadržaj generira se prilikom otvaranja pomoću XFA pokretača koji čita XML predložak i raspoređuje ga, šireći se ili skupljajući prema podacima. To je ono što najavljuje /NeedsRendering true: bez XFA procesora, statične stranice mogu biti prazne ili sadržavati samo obavijest "molimo otvorite u kompatibilnom pregledniku"

Sam XFA sadržaj je skup imenovanih paketa, a tri od njih nose sve što je većini integracija potrebno. Paket template je definicija obrasca — polja, raspored, izračuni i skripte. Paket datasets sadrži stvarne podatke instance, odnosno popunjene vrijednosti polja u obliku XML stabla. Paket config prenosi upute za obradu XFA pokretaču. Postoje i drugi — localeSet, connectionSet, xdp omotač — ali za reviziju obrasca ili migraciju podataka iz njega, template plus datasets obično čine cijeli posao. Budući da su ti podaci samo XML smješten u toku, njihovo čitanje nikada ne zahtijeva izvršavanje bilo kakvog koda, što je temeljni razlog zašto je statičko izdvajanje bez ikakvih vanjskih ovisnosti

Otkrivanje XFA obrasca u Delphiju

Otkrivanje počinje na način na koji počinje svaki zadatak s PDFium komponentom: postavite FileName, promijenite Active := True i provjerite je li se stvarno otvorio. Active := True nikada ne podiže iznimku — nedostajuća datoteka, pogrešna lozinka ili odsutni DLL samo ostavljaju Active na False — stoga je ta provjera obavezna. Kada je dokument otvoren, FormType vam govori koji model obrasca koristi. Dvije od vrijednosti TPdfFormType su XFA: ftXfaFull je puni, dinamički XFA obrazac, onaj koji zahtijeva iscrtavanje, dok je ftXfaForeground XFAF, podskup u prvom planu gdje se XFA sadržaj crta preko inače normalnih AcroForm stranica kako bi statične stranice ostale smislene. Logička vrijednost XFA je kratica za "ovo je XFA dokument bilo koje vrste". Ako također radite s poljima na razini widgeta u XFAF ili AcroForm dokumentu, mehanika je pokrivena u vodiču za navigaciju kroz polja PDFium obrazaca

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;

Izdvajanje XFA paketa kao sirog XML-a

Izdvajanje je vrijedna polovica bez ikakvih ovisnosti. Zaštitite ga s XfaStaticReadable — to potvrđuje da su izvozi čitača paketa razriješeni u učitanom DLL-u — a zatim prolazite kroz pakete po indeksu. GetXfaPacketCount vraća koliko paketa obrazac nosi, GetXfaPacketName daje svakom njegovo ime, a GetXfaPacketContent vraća sirove bajtove, koji su za poznate pakete UTF-8 XML. Kada već znate koji paket želite, imenovani pomoćnici GetXfaTemplate, GetXfaDatasets i GetXfaConfig razrješavaju ime za vas i vraćaju izravno TBytes. Za zadatak migracije podataka najvažniji je GetXfaDatasets: on vam daje predane vrijednosti polja kao XML dokument koji možete raščlaniti i preslikati u vlastitu shemu, što je ista vrsta posla koju biste radili nakon izdvajanja teksta iz PDF-a

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;

Zašto dinamičko iscrtavanje XFA-a zahtijeva V8 pokretač?

Najprije zaključak: zato što je dinamički XFA obrazac mali program, a ne statičan crtež. Paket predloška (template) nosi izračune FormCalc i JavaScripta, provjere i skripte događaja, a XFA mehanizam rasporeda mora ih izvršiti kako bi uopće odlučio kako stranice izgledaju. PDFium implementira taj mehanizam samo u verziji kompajliranoj s podrškom za XFA na vrhu V8, Googleovog JavaScript pokretača — što je razlog zašto je DLL s omogućenim V8 (pdfium32v8.dll ili pdfium64v8.dll) znatno veći od standardne verzije. Bez V8 pokretača nema tko pokrenuti skripte, pa nema iscrtanih stranica koje bi se mogle prikazati, već samo sirovi XML koji već možete čitati statički

Odabir te verzije ima čvrsto ograničenje koje ne možete zaobići: to je odluka koja se donosi jednom po procesu. Jedan proces ne može učitati i standardni pdfium.dll i verziju s omogućenim V8 — oni izvoze iste simbole, a V8 zadržava globalno stanje izolata — stoga PDFium komponenta mora odabrati V8 verziju prije nego što se knjižnica uopće učita prvi put. Kako bi to bilo automatski, LoadDocument unaprijed skenira datoteku tražeći markere /XFA i /NeedsRendering true (ista provjera izložena kao samostalna funkcija PdfFileLooksXfa) i postavlja EnableV8Engine := True prije učitavanja kada ih pronađe. Nakon što je obični pdfium.dll već učitan u memoriju, ta promjena više ne može stupiti na snagu, a taj slučaj prijavljuje putanja za nedostajuće vrijeme izvršavanja

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;

Elegantno rukovanje nedostajućim XFA vremenom izvršavanja

Tri ispitivanja govore vam točno koliko XFA mogućnosti imate, a ozbiljan kôd ih provjerava umjesto pretpostavljanja. XfaFeaturesAvailable je globalna provjera jesu li XFA/V8 pomoćni izvozi prisutni u učitanom DLL-u. XfaStaticReadable potvrđuje, po dokumentu, da su čitači paketa razriješeni — što je zaštita za izdvajanje. XfaRuntimeAvailable je najjači: istinit je samo kada je ovaj konkretni dokument stvarno aktivirao XFA pokretač, što znači da je to XFA, da je V8 verzija učitana i da je FPDF_LoadXFA uspio. Dokument može prijaviti XFA = True, a ipak XfaRuntimeAvailable = False kad god implementiranom DLL-u nedostaje XFA ili je standardna verzija već bila učitana

Kada se XFA dokument otvori, ali se pokretač ne može pokrenuti, PDFium komponenta ne kocka. Ona zaključava obrazac u siguran statički način rada umjesto da traži dinamičko vrijeme izvršavanja koje ne može podržati, te jednom podiže događaj OnXfaRuntimeMissing kako bi domaćin mogao reagirati — obično govoreći korisniku da ponovno pokrene aplikaciju s omogućenom V8 verzijom, dok u međuvremenu i dalje nudi statički čitljive podatke. Povezivanje tog rukovatelja čini razliku između čistog sigurnosnog rješenja i obrasca koji tiho ne prikazuje ništa

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;

Budite iskreni o granicama i u vlastitom proizvodu. Ako nikada ne isporučite DLL s omogućenim V8 pokretačem, dinamički XFA obrasci se nikada neće iscrtati za vaše korisnike — ali ih i dalje možete otkriti, izdvojiti svaki paket i izvući predane podatke, što je dovoljno za veliku klasu poslova koji se zapravo bave dobivanjem podataka iz naslijeđenih državnih i bankarskih obrazaca umjesto njihovog interaktivnog predstavljanja. Rezervirajte težu V8 verziju za slučajeve koji uistinu zahtijevaju aktivan obrazac koji se može popunjavati, i dopustite ispitivanjima mogućnosti da usmjere svaki dokument na putanju kojom stvarno može proći. Ako vaš cjevovod za izdvajanje također rukuje ugrađenim datotekama, ista filozofija samo za čitanje vrijedi i za PDF privitke u Delphiju

API-ji FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig te API-ji za ispitivanje mogućnosti prikazani ovdje dio su komponente PDFium Component za Delphi i C++Builder