Technical Article

Detecting and Extracting XFA Forms in Delphi with PDFium

Ja — PDFium unterstützt XFA-Formulare, allerdings mit einer wichtigen Unterscheidung: Die PDFium-Komponente für Delphi und Lazarus kann ein XFA-Formular erkennen und seine Vorlagen- (template), Datensatz- (datasets) und Konfigurationspakete (config) mit der Standard-DLL und ohne jegliche Laufzeitabhängigkeit als rohes XML extrahieren. Das Ausführen eines dynamischen XFA-Formulars — also das Rendern seiner XML-Beschreibung in aktive, interaktive Seiten — ist ein separates Problem, das zusätzlich das V8-aktivierte Build erfordert. In Entwickler-Foren wird oft gefragt, ob PDFium „XFA unterstützt“, als handele es sich um eine einfache Ja-Nein-Option. Die ehrliche Antwort lautet, dass das Auslesen von XFA-Daten und das Rendern eines XFA-Formulars zwei völlig unterschiedliche Dinge mit sehr unterschiedlichen Abhängigkeiten sind

Dieser Artikel führt Sie durch beide Pfade in der PDFium-Komponente, dem nativen VCL-Wrapper um PDFium für Delphi und C++Builder. Zuerst betrachten wir den Pfad der Erkennung und statischen Extraktion — FormType, die Eigenschaft XFA und die GetXfaPacket*-Leser —, gefolgt vom dynamischen Laufzeit-Pfad mit seiner V8-Einschränkung und den Eigenschaftsprüfungen, mit denen Sie Fehler sauber abfangen können, falls die bereitgestellte DLL die Engine nicht ausführen kann

Unterstützt PDFium XFA-Formulare?

Betrachten Sie dies als zwei getrennte Fähigkeiten, nicht als eine. Die statische XFA-Extraktion ist immer verfügbar: Die Low-Level-Exporte FPDF_GetXFAPacket*, die den Methoden GetXfaPacketCount, GetXfaPacketName und GetXfaPacketContent zugrunde liegen, sind in jeder einigermaßen modernen pdfium.dll enthalten, selbst wenn das XFA-Modul beim Kompilieren deaktiviert wurde. Aus diesem Grund erfordert das Auslesen der rohen XML-Daten für template, datasets und config aus einem Formular keinerlei Laufzeitabhängigkeiten — es ist reines strukturelles Lesen, bei dem die eigentliche Engine nicht gestartet wird. Dynamisches XFA ist die andere Fähigkeit: das Ausführen des Formulars, das Auswerten seiner Berechnungen und das seitenweise Formatieren durch die XFA-Layout-Engine. Dieser Pfad existiert nur in einem PDFium-Build, das mit XFA-Unterstützung auf der JavaScript-Engine V8 von Google kompiliert wurde. Das ist der Teil, um den es bei Fragen zur XFA-Unterstützung meistens geht

Die Standards trennen dies sauber. XFA ist außerhalb der PDF-Kerngrammatik in Adobe XFA 3.3 (2012) definiert, und PDF 1.7 §8.6.7 beschreibt, wie ein XFA-Formular in einem PDF transportiert wird: Die Formularpakete liegen unter dem AcroForm-Verzeichnis-Eintrag /XFA, und ein Dokument, das vor der Darstellung durch einen XFA-Prozessor formatiert werden muss, setzt den Eintrag /NeedsRendering true im Katalog. Diese beiden Marker sucht die PDFium-Komponente und darauf können Sie sich verlassen, wenn Sie eine Ihnen unbekannte Datei analysieren

Wie unterscheidet sich XFA von einem AcroForm?

Ein AcroForm ist das klassische PDF-Formular: Widget-Anmerkungen (Textfelder, Kontrollkästchen, Optionsfelder), die an festen Koordinaten auf normalen PDF-Seiten verankert sind. Die Seite, die Sie sehen, ist die Seite in der Datei, und die Felder liegen darüber. XFA wählt den entgegengesetzten Ansatz. Das interaktive Formular wird vollständig in XML beschrieben. In einem echten (dynamischen) XFA-Dokument sind die PDF-Seiten praktisch nur ein Platzhalter — der tatsächliche Inhalt wird beim Öffnen von einer XFA-Engine generiert, die die XML-Vorlage liest, das Layout erstellt und sich je nach Datenmenge vergrößert oder verkleinert. Das kündigt der Eintrag /NeedsRendering true an: Ohne einen XFA-Prozessor sind die statischen Seiten eventuell leer oder enthalten nur den Hinweis „Bitte in einem kompatiblen Viewer öffnen“

Der XFA-Payload selbst besteht aus einer Reihe benannter XML-Pakete, von denen drei die Grundlage für fast alle Integrationen bilden. Das Paket template enthält die Formulardefinition — Felder, Layout, Berechnungen und Skripte. Das Paket datasets enthält die eigentlichen Instanzdaten, also die ausgefüllten Feldwerte als XML-Baum. Das Paket config speichert Verarbeitungsanweisungen für die XFA-Engine. Es gibt noch weitere (localeSet, connectionSet, die xdp-Hülle), aber für die Analyse eines Formulars oder die Migration der Daten ist die Kombination aus template und datasets meist völlig ausreichend. Da diese Daten nur als XML in einem Stream vorliegen, muss für ihr Auslesen kein Code ausgeführt werden, weshalb die statische Extraktion frei von Abhängigkeiten ist

Erkennen eines XFA-Formulars in Delphi

Die Erkennung beginnt so, wie fast jede Aufgabe in der PDFium-Komponente: Setzen Sie FileName, schalten Sie Active := True um und prüfen Sie, ob das Dokument geladen wurde. Active := True wirft keine Exception. Wenn die Datei fehlt, das Passwort falsch ist oder die DLL fehlt, bleibt Active einfach auf False, weshalb diese Prüfung zwingend erforderlich ist. Sobald das Dokument geöffnet ist, zeigt FormType das genutzte Formularmodell an. Zwei der Werte von TPdfFormType stehen für XFA: ftXfaFull ist ein vollständiges, dynamisches XFA-Formular, das gerendert werden muss. ftXfaForeground ist ein XFAF-Formular (Foreground-Untermenge), bei dem XFA-Inhalte über normale AcroForm-Seiten gezeichnet werden, sodass die statischen Seiten lesbar bleiben. Der Boolean XFA ist eine Abkürzung für „dies ist ein XFA-Dokument beider Typen“. Wenn Sie mit den Steuerelementen auf einem XFAF- oder AcroForm-Dokument arbeiten möchten, beschreibt der Leitfaden zur Formularfeld-Navigation in PDFium diesen Ablauf

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;

Extrahieren von XFA-Paketen als rohes XML

Das Extrahieren ist der lohnende Teil, der ohne Abhängigkeiten funktioniert. Sichern Sie den Aufruf mit XfaStaticReadable ab — dies bestätigt, dass die Exporte für den Paket-Leser in der geladenen DLL vorhanden sind — und durchlaufen Sie die Pakete nach Index. GetXfaPacketCount gibt an, wie viele Pakete das Formular enthält, GetXfaPacketName liefert den Namen jedes Pakets und GetXfaPacketContent gibt die rohen Bytes zurück (was bei den bekannten Paketen UTF-8-XML ist). Wenn Sie bereits wissen, welches Paket Sie benötigen, holen die benannten Hilfsfunktionen GetXfaTemplate, GetXfaDatasets und GetXfaConfig dieses direkt als TBytes. Für eine Datenmigration ist GetXfaDatasets der wichtigste Aufruf: Er liefert die ausgefüllten Feldwerte als XML-Dokument, das Sie parsen und in Ihr eigenes Schema einlesen können — eine ähnliche Aufgabe, wie man sie nach dem Extrahieren von Text aus einer PDF durchführt

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;

Warum das dynamische XFA-Rendering die V8-Engine benötigt

Zusammenfassend: weil ein dynamisches XFA-Formular ein kleines Programm und keine statische Zeichnung ist. Das template-Paket enthält FormCalc- und JavaScript-Berechnungen, Validierungen und Ereignis-Skripte, und die XFA-Layout-Engine muss diese ausführen, um festzulegen, wie die Seiten überhaupt aussehen. PDFium implementiert diese Engine nur in einem Build, das mit XFA-Unterstützung auf der V8-JavaScript-Engine kompiliert wurde. Das ist auch der Grund, warum die V8-aktivierte DLL (pdfium32v8.dll bzw. pdfium64v8.dll) deutlich größer ist als das Standard-Build. Ohne V8 können die Skripte nicht ausgeführt werden, sodass keine gerenderten Seiten dargestellt werden können und nur die XML-Daten statisch lesbar bleiben

Die Auswahl dieses Builds unterliegt einer harten Einschränkung: Es ist eine prozessweite Entscheidung, die nur einmal getroffen werden kann. Ein einzelner Prozess kann nicht sowohl die Standard-pdfium.dll als auch die V8-aktivierte Version laden — sie exportieren dieselben Symbole und V8 hält globale Isolate-Zustände im Speicher. Daher muss die PDFium-Komponente das V8-Build auswählen, bevor die Bibliothek zum allerersten Mal geladen wird. Um dies zu automatisieren, scannt LoadDocument die Datei vorab nach den Markern /XFA und /NeedsRendering true (dieselbe Prüfung, die auch über die Standalone-Funktion PdfFileLooksXfa bereitgestellt wird) und setzt EnableV8Engine := True vor dem Laden, wenn sie fündig wird. Sobald eine normale pdfium.dll bereits geladen ist, kann diese Einstellung nicht mehr wirksam werden. Dieser Fall wird als fehlende Laufzeitumgebung gemeldet

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;

Sinnvoller Umgang mit einer fehlenden XFA-Laufzeitumgebung

Drei Eigenschaften zeigen Ihnen genau, welche XFA-Fähigkeiten zur Verfügung stehen. XfaFeaturesAvailable ist eine globale Prüfung, ob die XFA/V8-Hilfsexporte in der geladenen DLL vorhanden sind. XfaStaticReadable bestätigt pro Dokument, dass die Paketleser aufgelöst werden konnten — dies sichert die Extraktion ab. XfaRuntimeAvailable ist die entscheidende Eigenschaft: Sie ist nur True, wenn dieses Dokument die XFA-Engine tatsächlich aktivieren konnte, also wenn es sich um XFA handelt, das V8-Build geladen wurde und FPDF_LoadXFA erfolgreich war. Ein Dokument kann XFA = True, aber XfaRuntimeAvailable = False melden, wenn die bereitgestellte DLL kein XFA unterstützt oder die Standard-DLL bereits geladen war

Wenn sich ein XFA-Dokument öffnet, die Engine jedoch nicht laufen kann, schaltet die PDFium-Komponente die Formularanzeige in den sicheren statischen Modus zurück, statt eine dynamische Laufzeitumgebung anzufordern, die sie nicht bedienen kann. Sie löst einmal das Ereignis OnXfaRuntimeMissing aus, damit die Anwendung reagieren kann — meist, indem sie den Benutzer auffordert, die Anwendung mit dem V8-Build neu zu starten, während sie in der Zwischenzeit die statisch lesbaren Daten anbietet. Das Zuweisen dieses Handlers ist der Unterschied zwischen einem sauberen Fallback und einem Formular, das stillschweigend nichts anzeigt

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;

Machen Sie sich diese Grenze auch für Ihr eigenes Produkt klar: Wenn Sie die V8-aktivierte DLL niemals ausliefern, werden dynamische XFA-Formulare für Ihre Benutzer niemals gerendert. Sie können sie jedoch weiterhin erkennen, jedes Paket extrahieren und die ausgefüllten Daten auslesen, was für viele Aufgaben ausreicht, bei denen es eigentlich nur darum geht, Daten aus alten Behörden- und Bankformularen zu migrieren statt diese interaktiv darzustellen. Reservieren Sie das größere V8-Build für jene Fälle, in denen Sie zwingend ein ausfüllbares Formular anzeigen müssen, und lassen Sie die Eigenschaftsprüfungen jedes Dokument auf den passenden Pfad leiten. Wenn Ihre Extraktions-Pipeline auch eingebettete Dateien verarbeitet, gilt dieselbe Nur-Lese-Logik für PDF-Anhänge in Delphi

Die hier gezeigten APIs FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig und die Eigenschaften zur Prüfung der Laufzeitunterstützung sind alle Teil der PDFium-Komponente für Delphi und C++Builder