Tak — PDFium obsługuje formularze XFA, z jednym ważnym podziałem. Komponent PDFium potrafi wykryć formularz XFA i wyodrębnić jego pakiety template, datasets i config jako surowy XML przy użyciu standardowej biblioteki DLL, przy zerowej liczbie zależności w czasie wykonywania. Uruchomienie dynamicznego formularza XFA — rozłożenie jego opisu XML na żywe, interaktywne strony — to osobny problem, który dodatkowo wymaga kompilacji z włączonym silnikiem V8. Społeczność ciągle pyta, czy PDFium „obsługuje XFA”, jakby to była jedna funkcja typu tak-lub-nie, a uczciwa odpowiedź brzmi, że czytanie danych XFA i renderowanie formularza XFA to dwie bardzo różne rzeczy o bardzo różnych wymaganiach dotyczących zależności
Ten artykuł omawia obie części z użyciem komponentu PDFium, który jest natywną nakładką VCL na PDFium dla Delphi i C++Builder. Najpierw ścieżka wykrywania i statycznego wyodrębniania — FormType, właściwość XFA i czytniki GetXfaPacket* — a następnie ścieżka dynamicznego czasu wykonywania z jej ograniczeniem V8, i na koniec testy możliwości, które pozwalają na bezpieczną obsługę błędów, gdy wdrożona biblioteka DLL nie może uruchomić silnika
Czy PDFium obsługuje formularze XFA?
Należy to postrzegać jako dwie osobne funkcje, a nie jedną. Statyczne wyodrębnianie XFA jest zawsze dostępne: niskopoziomowe eksporty FPDF_GetXFAPacket*, na których opierają się metody GetXfaPacketCount, GetXfaPacketName i GetXfaPacketContent, są kompilowane w każdej w miarę nowoczesnej bibliotece pdfium.dll, nawet w wersji z wyłączoną obsługą XFA. Właśnie dlatego wyciąganie surowego kodu XML pakietów template, datasets i config z formularza nie niesie za sobą żadnych zależności w czasie wykonywania — to czysty odczyt strukturalny bez udziału silnika. Dynamiczne XFA to druga funkcja: rzeczywiste uruchamianie formularza, obliczanie jego parametrów i pozwalanie silnikowi układu XFA na jego stronicowanie. Ścieżka ta istnieje tylko w kompilacji PDFium skompilowanej z obsługą XFA na bazie silnika JavaScript V8 i to jest właśnie ta część, o którą toczy się większość dyskusji w wątkach dotyczących obsługi XFA przez PDFium
Standardy jasno definiują ten podział. Standard XFA jest zdefiniowany poza podstawową gramatyką PDF w specyfikacji Adobe XFA 3.3 (2012), a PDF 1.7 §8.6.7 opisuje, jak formularz XFA jest przenoszony w pliku PDF: pakiety formularza znajdują się w słowniku AcroForm pod wpisem /XFA, a dokument, który musi zostać ułożony przez procesor XFA, aby miał jakikolwiek sens, ustawia wartość /NeedsRendering true w katalogu. Te dwa znaczniki to dokładnie to, czego szuka komponent PDFium, i dokładnie to, na czym można polegać przy analizie nieznanego pliku
Czym różni się XFA od klasycznego formularza AcroForm?
AcroForm to klasyczny formularz PDF: adnotacje widżetów — pola tekstowe, pola wyboru, grupy radiowe — zakotwiczone na stałych współrzędnych na zwykłych stronach PDF. Strona, którą widzisz, jest stroną zapisaną w pliku, a pola znajdują się na niej. XFA stosuje odwrotne podejście. Interaktywny formularz jest opisany w całości w kodzie XML, a w pełnym (dynamicznym) dokumencie XFA strony PDF są w rzeczywistości tylko miejscem zastępczym — rzeczywista zawartość jest generowana w momencie otwarcia przez silnik XFA, który odczytuje szablon XML i układa go, rozciągając lub kurcząc w celu dopasowania do danych. To właśnie ogłasza /NeedsRendering true: bez procesora XFA strony statyczne mogą być puste lub zawierać jedynie komunikat „otwórz w zgodnej przeglądarce”
Sam ładunek XFA to zestaw nazwanych pakietów, a trzy z nich zawierają wszystko, czego potrzebuje większość integracji. Pakiet template to definicja formularza — pola, układ, obliczenia i skrypty. Pakiet datasets przechowuje rzeczywiste dane instancji, czyli wypełnione wartości pól w postaci drzewa XML. Pakiet config zawiera instrukcje przetwarzania dla silnika XFA. Istnieją też inne — localeSet, connectionSet, opakowanie xdp — ale w celu audytu formularza lub migracji z niego danych, szablon oraz zestawy danych to zazwyczaj całe zadanie. Ponieważ te dane to po prostu XML umieszczony w strumieniu, ich odczyt nigdy nie wymaga uruchamiania czegokolwiek, co jest główną przyczyną, dla której statyczne wyodrębnianie nie niesie ze sobą żadnych zależności
Wykrywanie formularza XFA w Delphi
Wykrywanie rozpoczyna się tak samo, jak każde inne zadanie z komponentem PDFium: ustawiasz właściwość FileName, przełączasz Active := True i sprawdzasz, czy plik rzeczywiście się otworzył. Metoda Active := True nigdy nie zgłasza wyjątków — brak pliku, błędne hasło lub brak biblioteki DLL po prostu pozostawia Active na False — więc ten warunek jest obowiązkowy. Po otwarciu dokumentu właściwość FormType wskazuje, którego modelu formularza używa dokument. Dwie z wartości TPdfFormType reprezentują XFA: ftXfaFull to pełny, dynamiczny formularz XFA wymagający renderowania, a ftXfaForeground to XFAF, czyli podzbiór pierwszoplanowy, gdzie zawartość XFA jest rysowana na wierzchu normalnych skądinąd stron AcroForm, dzięki czemu strony statyczne zachowują znaczenie. Wartość logiczna XFA to skrót oznaczający „to jest dokument XFA dowolnego rodzaju”. Jeśli pracujesz również z polami na poziomie widżetów w dokumencie XFAF lub AcroForm, mechanikę tę opisano w przewodniku po nawigacji po polach formularzy 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;
Wyodrębnianie pakietów XFA jako surowego XML
Wyodrębnianie to ta cenna część, która nie niesie ze sobą żadnych zależności. Zabezpiecz kod za pomocą właściwości XfaStaticReadable — potwierdza ona, że eksporty czytnika pakietów zostały rozwiązane w załadowanej bibliotece DLL — a następnie przejdź przez pakiety według indeksu. Metoda GetXfaPacketCount zwraca liczbę pakietów w formularzu, GetXfaPacketName podaje nazwę każdego z nich, a GetXfaPacketContent zwraca surowe bajty, które w przypadku znanych pakietów reprezentują kod XML w formacie UTF-8. Gdy wiesz już, który pakiet chcesz pobrać, nazwani pomocnicy GetXfaTemplate, GetXfaDatasets i GetXfaConfig rozstrzygają nazwę za Ciebie i bezpośrednio zwracają TBytes. W przypadku migracji danych kluczowa jest metoda GetXfaDatasets: daje ona wypełnione wartości pól jako dokument XML, który można przetwarzać i mapować na własny schemat, podobnie jak ma to miejsce po wyodrębnieniu tekstu z pliku 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;
Dlaczego dynamiczne renderowanie XFA wymaga silnika V8?
Zaczynając od wniosków: ponieważ dynamiczny formularz XFA to mały program, a nie statyczny rysunek. Pakiet szablonu przenosi obliczenia FormCalc i JavaScript, walidacje oraz skrypty zdarzeń, a silnik układu XFA musi je wykonać, aby w ogóle zdecydować, jak wyglądają strony. PDFium implementuje ten silnik tylko w kompilacji skompilowanej z obsługą XFA na bazie silnika V8 (silnika JavaScript firmy Google) — z tego samego powodu biblioteka DLL z obsługą V8 (pdfium32v8.dll or pdfium64v8.dll) jest znacznie większa niż kompilacja standardowa. Bez silnika V8 skrypty nie mogą zostać uruchomione, co oznacza brak wyrenderowanych stron, a jedynie surowy XML, który można odczytać statycznie
Wybór tej kompilacji wiąże się ze sztywnym ograniczeniem, którego nie można obejść: to jednorazowa decyzja na poziomie procesu. Pojedynczy proces nie może załadować jednocześnie standardowej biblioteki pdfium.dll oraz wersji z obsługą V8 — eksportują one te same symbole, a V8 przechowuje globalny stan izolacji — dlatego komponent PDFium musi wybrać wersję z V8, zanim biblioteka zostanie załadowana po raz pierwszy. Aby to zautomatyzować, metoda LoadDocument wstępnie skanuje plik w poszukiwaniu znaczników /XFA i /NeedsRendering true (to samo sprawdzenie, które udostępnia autonomiczna funkcja PdfFileLooksXfa) i przed załadowaniem ustawia właściwość EnableV8Engine := True, gdy je znajdzie. Gdy zwykła biblioteka pdfium.dll jest już obecna w pamięci, przełączenie nie może już odnieść skutku, co jest raportowane jako brak środowiska wykonawczego
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;
Bezpieczna obsługa braku środowiska wykonawczego XFA
Trzy testy wskazują dokładnie, jakie możliwości XFA posiadasz, a dobrze napisany kod sprawdza je zamiast zakładać cokolwiek z góry. Właściwość XfaFeaturesAvailable to globalne sprawdzenie obecności eksportów pomocniczych XFA/V8 w załadowanej bibliotece DLL. Właściwość XfaStaticReadable potwierdza dla danego dokumentu, że czytniki pakietów zostały rozwiązane — stanowi zabezpieczenie dla wyodrębniania. Z kolei XfaRuntimeAvailable to silniejszy warunek: ma wartość true tylko wtedy, gdy dany dokument rzeczywiście aktywował silnik XFA, co oznacza, że jest to dokument XFA, załadowano wersję z V8, a wywołanie FPDF_LoadXFA powiodło się. Dokument może zgłaszać XFA = True, a jednocześnie XfaRuntimeAvailable = False, gdy wdrożona biblioteka DLL nie posiada obsługi XFA lub standardowa wersja została już wcześniej załadowana
Gdy dokument XFA się otwiera, ale silnik nie może zostać uruchomiony, komponent PDFium nie ryzykuje. Przypina formularz do bezpiecznego trybu statycznego, zamiast żądać dynamicznego środowiska wykonawczego, którego nie może obsłużyć, i zgłasza zdarzenie OnXfaRuntimeMissing, aby aplikacja mogła zareagować — zazwyczaj informując użytkownika o konieczności ponownego uruchomienia z wersją obsługującą V8, oferując w międzyczasie statycznie czytelne dane. Podpięcie tego zdarzenia decyduje o różnicy między czystym wycofaniem a formularzem, który po cichu nie pokazuje niczego
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;
Bądź szary i uczciwy wobec tych ograniczeń we własnym produkcie. Jeśli nigdy nie dostarczysz biblioteki DLL obsługującej V8, dynamiczne formularze XFA nigdy nie wyrenderują się u Twoich użytkowników — ale nadal możesz je wykrywać, wyodrębniać każdy pakiet i wyciągać przesłane dane, co wystarcza dla dużej klasy zadań polegających na pobieraniu danych ze starych formularzy rządowych czy bankowych, a nie na ich interaktywnej prezentacji. Zarezerwuj cięższą kompilację V8 dla przypadków, które rzeczywiście wymagają aktywnego formularza do wypełnienia, i pozwól testom możliwości kierować każdy dokument na ścieżkę, którą może rzeczywiście podążyć. Jeśli Twój potok wyodrębniania obsługuje również osadzone pliki, ta sama filozofia odczytu ma zastosowanie do załączników PDF w Delphi
Przedstawione tutaj interfejsy API FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig oraz testy możliwości są częścią komponentu PDFium dla Delphi i C++Builder