Artículo técnico

Detección y extracción de formularios XFA en Delphi con PDFium

Sí, PDFium admite formularios XFA, con una división importante. PDFium Component puede detectar un formulario XFA y extraer sus paquetes de template, datasets y config como XML sin formato utilizando la DLL estándar, con cero dependencias en tiempo de ejecución. Ejecutar un formulario XFA dinámico —distribuyendo su descripción XML en páginas activas e interactivas— es un problema independiente que requiere además la compilación compatible con V8. La comunidad sigue preguntando si PDFium "admite XFA" como si fuera una única capacidad de sí o no, y la respuesta honesta es que leer datos de XFA y renderizar un formulario XFA son dos cosas muy diferentes con dos huellas de dependencia muy distintas

Este artículo analiza ambas partes con PDFium Component, el envoltorio (wrapper) VCL nativo al rededor de PDFium para Delphi y C++Builder. Primero la ruta de detección y extracción estática —FormType, la propiedad XFA y los lectores GetXfaPacket*—, luego la ruta de ejecución dinámica con su restricción de V8, y finalmente las pruebas de capacidad que le permiten fallar de manera controlada cuando una DLL desplegada no puede ejecutar el motor

¿Admite PDFium formularios XFA?

Interprételo como dos capacidades, no como una. La extracción estática de XFA siempre está disponible: las exportaciones de bajo nivel FPDF_GetXFAPacket* que respaldan a GetXfaPacketCount, GetXfaPacketName y GetXfaPacketContent están compiladas en cada pdfium.dll razonablemente moderna, incluso en una compilación con XFA desactivado. Es por eso que extraer el XML sin formato de template, datasets y config de un formulario no conlleva ninguna dependencia en tiempo de ejecución en absoluto: es una lectura estructural pura, sin ningún motor involucrado. El XFA dinámico es la otra capacidad: ejecutar el formulario en sí, evaluar sus cálculos y permitir que el motor de diseño de XFA lo pagine. Esa ruta existe solo en una compilación de PDFium compilada con soporte XFA sobre el motor JavaScript V8, y es la parte de la que realmente se discute en la mayoría de los hilos de debate sobre si PDFium admite XFA

Los estándares enmarcan esta división de forma limpia. XFA se define fuera de la gramática central de PDF en Adobe XFA 3.3 (2012), y PDF 1.7 §8.6.7 describe cómo se transporta un formulario XFA dentro de un PDF: los paquetes de formulario residen bajo la entrada /XFA del diccionario AcroForm, y un documento que debe ser estructurado por un procesador XFA antes de tener sentido establece /NeedsRendering true en el catálogo. Esos dos marcadores son exactamente lo que busca PDFium Component, y en lo que puede confiar al razonar sobre un archivo que nunca ha visto

¿En qué se diferencia XFA de un AcroForm?

Un AcroForm es the classic PDF form: widget annotations — text fields, check boxes, radio groups — anchored at fixed coordinates on ordinary PDF pages. The page you see is the page in the file, and the fields sit on top of it. XFA takes the opposite approach. The interactive form is described entirely in XML, and in a full (dynamic) XFA document the PDF pages are effectively a placeholder — the real content is generated at open time by an XFA engine that reads the XML template and lays it out, growing or shrinking to fit the data. That is what /NeedsRendering true announces: without an XFA processor, the static pages may be blank or carry only a "please open in a compatible viewer" notice

El paquete de formulario XFA en sí es un conjunto de paquetes con nombre, y tres de ellos contienen todo lo que la mayoría de las integraciones necesitan. El paquete template es la definición del formulario: campos, diseño, cálculos y scripts. El paquete datasets contiene los datos de la instancia real, es decir, los valores de los campos completados como un árbol XML. El paquete config transporta las instrucciones de procesamiento para el motor XFA. Existen otros —localeSet, connectionSet, el contenedor xdp—, pero para auditar un formulario o migrar los datos fuera de él, la combinación de template y datasets suele ser todo el trabajo. Debido a que esos datos son simplemente XML dentro de un flujo de datos (stream), leerlos nunca requiere ejecutar nada, que es la razón principal por la que la extracción estática está libre de dependencias

Detección de un formulario XFA en Delphi

La detección comienza de la forma en que lo hace cada tarea de PDFium Component: establezca FileName, cambie a Active := True y compruebe que realmente se haya abierto. Active := True nunca genera una excepción: un archivo faltante, una contraseña incorrecta o una DLL ausente simplemente dejan Active en False, por lo que la comprobación es obligatoria. Una vez abierto el documento, FormType le indica qué modelo de formulario utiliza. Dos de los valores de TPdfFormType son XFA: ftXfaFull es un formulario XFA dinámico completo, el que necesita renderizado, mientras que ftXfaForeground es XFAF, el subconjunto de primer plano donde el contenido de XFA se dibuja sobre páginas AcroForm normales para que las páginas estáticas sigan teniendo sentido. El booleano XFA es un acceso rápido para "este es un documento XFA de cualquier tipo". Si también trabaja con los campos a nivel de widget en un documento XFAF o AcroForm, la mecánica se cubre en la guía de navegación de campos de formulario de 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;

Extracción de paquetes XFA como XML sin formato

La extracción es la mitad de alto valor y libre de dependencias. Protéjala con XfaStaticReadable —confirma que las exportaciones del lector de paquetes se resolvieron en la DLL cargada— y luego recorra los paquetes por índice. GetXfaPacketCount devuelve cuántos paquetes contiene el formulario, GetXfaPacketName le da su nombre a cada uno y GetXfaPacketContent devuelve los bytes sin procesar, que para los paquetes conocidos son XML en formato UTF-8. Cuando ya sabe qué paquete desea, los asistentes con nombre GetXfaTemplate, GetXfaDatasets y GetXfaConfig resuelven el nombre por usted y le devuelven TBytes directamente. Para una tarea de migración de datos, el que importa es GetXfaDatasets: le proporciona los valores de los campos enviados como un documento XML que puede analizar y mapear en su propio esquema, el mismo tipo de trabajo derivado que haría después de extraer texto de un 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;

¿Por qué el renderizado dinámico de XFA necesita el motor V8?

La conclusión primero: porque un formulario XFA dinámico es un pequeño programa, no un dibujo estático. El paquete de plantilla (template) contiene cálculos, validaciones y scripts de eventos en FormCalc y JavaScript, y el motor de diseño XFA tiene que ejecutarlos para decidir cómo se verán las páginas. PDFium implementa ese motor solo en una compilación compilada con soporte XFA sobre V8, el motor JavaScript de Google —la misma razón por la cual la DLL habilitada para V8 (pdfium32v8.dll o pdfium64v8.dll) es notablemente más grande que la compilación estándar. Sin V8 no hay nadie para ejecutar los scripts, por lo que no hay páginas renderizadas que mostrar, solo el XML sin formato que ya puede leer de forma estática

Seleccionar esa compilación tiene una restricción estricta que no puede evitar al diseñar: es una decisión de una sola vez por proceso. Un solo proceso no puede cargar tanto la pdfium.dll estándar como la compilación habilitada para V8 —exportan los mismos símbolos y V8 mantiene el estado global del entorno aislado (isolate)—, por lo que PDFium Component debe elegir la compilación V8 antes de que la biblioteca se cargue por primera vez. Para que eso sea automático, LoadDocument preescanea el archivo en busca de los marcadores /XFA y /NeedsRendering true (la misma comprobación expuesta como la función independiente PdfFileLooksXfa) y establece EnableV8Engine := True antes de la carga cuando los encuentra. Una vez que una pdfium.dll normal ya reside en memoria, el cambio ya no puede surtir efecto, y ese caso es el que informa la ruta de falta de entorno de ejecución

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;

Control controlado de la falta del entorno de ejecución XFA

Tres pruebas le indican exactamente cuánta capacidad XFA tiene, y un código honesto las comprueba en lugar de asumirlo. XfaFeaturesAvailable es una comprobación global de que las exportaciones auxiliares de XFA/V8 están presentes en la DLL cargada. XfaStaticReadable confirma, por documento, que los lectores de paquetes se resolvieron —la protección para la extracción. XfaRuntimeAvailable es la fuerte: es true solo cuando este documento específico activó realmente el motor XFA, lo que significa que es XFA, se cargó la compilación V8 y FPDF_LoadXFA tuvo éxito. Un documento puede reportar XFA = True y sin embargo XfaRuntimeAvailable = False siempre que la DLL desplegada carezca de XFA o la compilación estándar ya se haya cargado

Cuando se abre un documento XFA pero el motor no puede ejecutarse, PDFium Component no se la juega. Vincula el formulario al modo estático seguro en lugar de solicitar un entorno de ejecución dinámico que no puede cumplir, y genera OnXfaRuntimeMissing una vez para que el anfitrión pueda reaccionar —normalmente indicando al usuario que reinicie con la compilación habilitada para V8, al tiempo que ofrece los datos legibles estáticamente. Conectar ese controlador representa la diferencia entre una alternativa controlada y un formulario que no muestra nada de forma silenciosa

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;

Sea honesto también sobre los límites en su propio producto. Si nunca distribuye la DLL habilitada para V8, los formularios XFA dinámicos nunca se renderizarán para sus usuarios, pero aún puede detectarlos, extraer cada paquete y obtener los datos enviados, lo cual es suficiente para la gran cantidad de tareas que consisten realmente en extraer datos de formularios bancarios y gubernamentales heredados en lugar de presentarlos interactivamente. Reserve la compilación V8 más pesada para los casos que realmente necesiten un formulario interactivo y rellenable, y permita que las pruebas de capacidad dirijan cada documento a la ruta que realmente puede tomar. Si su flujo de extracción también maneja archivos incrustados, la misma filosofía de solo lectura se aplica a los archivos adjuntos PDF en Delphi

Las API de FormType, XFA, GetXfaPacketCount, GetXfaPacketName, GetXfaPacketContent, GetXfaTemplate, GetXfaDatasets, GetXfaConfig y las pruebas de capacidad que se muestran aquí forman parte de PDFium Component para Delphi y C++Builder