Artículo técnico

XFA estático a campos AcroForm en Delphi con PDFium

PDFium Component convierte formularios XFA estáticos en campos AcroForm ordinarios desde Delphi a través de FlattenPdfXfaToAcroForm en la unidad FPdfDocumentXfaFlatten. La función lee los paquetes template y datasets de XFA, mapea cada campo posicionado absolutamente a un campo terminal AcroForm con su propio flujo de apariencia, y anexa el resultado como una revisión incremental, así el PDF convertido abre como un formulario normal en visores que nunca soportaron XFA

La motivación suele ser un ticket de soporte más que un ítem de roadmap. Un formulario de gobierno o de seguros se autoría en un diseñador que lo guardó como XFA, funciona en un lector de escritorio, y en todos los demás abre como una página en blanco que dice que el documento requiere un visor más nuevo. XFA quedó obsoleta en PDF 2.0 (ISO 32000-2), los navegadores y los visores móviles nunca la implementaron, y el propio PDFium solo renderiza XFA con la compilación habilitada para V8. Los formularios XFA estáticos, los de layout fijo en tiempo de diseño, llevan suficiente geometría en el template para reconstruirse como campos AcroForm, y ese es exactamente el segmento que este convertidor maneja

¿Qué produce realmente FlattenPdfXfaToAcroForm?

FlattenPdfXfaToAcroForm produce una revisión AcroForm a nivel de bytes anexada a una copia sin cambios del origen, nunca una reescritura del archivo original. PDFium no expone API pública para crear campos de formulario, así que el componente no pasa por PDFium para este trabajo en absoluto: lee la revisión activa con su propio lector de objetos, construye diccionarios de campos, anotaciones widget, XObjects de apariencia y un recurso de fuente /Helv como objetos PDF crudos, y los serializa a través de una actualización incremental dispersa en el sentido de ISO 32000-1 §7.5.6

El nuevo diccionario /AcroForm lleva /Fields, /DA y /DR y ninguna entrada /XFA. Cuando el origen conserva su AcroForm como un objeto indirecto, ese objeto se reescribe en el lugar; un diccionario inline se reemplaza dentro del catálogo. De cualquier forma la revisión activa ya no apunta a los paquetes XFA, así que un visor que entiende ambos modelos abre los campos AcroForm en lugar de intentar maquetar el formulario XFA. Los bytes XFA originales siguen en la revisión anterior, que es lo que significa una actualización incremental, pero nada en la revisión actual los referencia

Pipeline de conversión de XFA estático a AcroForm en PDFium Component para Delphi: FlattenPdfXfaToAcroForm lee los paquetes template y datasets, mapea campos posicionados absolutamente a diccionarios de campo y widgets, y anexa el AcroForm como una revisión incremental dispersa sin entrada /XFA
FlattenPdfXfaToAcroForm nunca toca los bytes originales: los campos convertidos, los widgets y los flujos de apariencia se anexan como una revisión cuyo /AcroForm simplemente deja de apuntar a los paquetes XFA
uses
  System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;

procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
  Source: TFileStream;
  Options: TPdfXfaFlattenOptions;
  OutputPdf: TBytes;
  Report: TPdfXfaFlattenReport;
  Fields: TPdfXfaFlattenFields;
begin
  Options := TPdfXfaFlattenOptions.Default;
  Options.MaxFields := 1024;   // por defecto es 512, el rango permitido es 1..4096

  Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
  try
    if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
      raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
  finally
    Source.Free;
  end;

  TFile.WriteAllBytes(TargetFile, OutputPdf);
  Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
    [Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;

La función devuelve False y llena Report.ErrorText ante cualquier entrada rechazada o malformada, y restaura la posición del stream de origen al salir. La cancelación es la única ruta de excepción que se propaga: si Options.CancellationToken se dispara, EPdfOperationCancelled se relanza después de llenar el reporte. TPdfXfaFlattenReport también lleva SourceSha256 y OutputSha256, lo que resulta práctico cuando un trabajo por lotes tiene que probar qué entrada produjo qué archivo convertido. TPdfXfaFlattenOptions.Default fija un presupuesto de paquetes de 8 MiB, 512 campos y un techo de salida de 512 MiB; el origen no debe estar cifrado

Leer los paquetes template y datasets

PDFium Component acepta ambos layouts que ISO 32000-1 §12.7.8 permite para la entrada /XFA: la forma de arreglo que alterna nombres de paquete y referencias de stream, y la forma XDP de un solo stream donde un stream contiene el documento <xdp:xdp> completo. Los streams de paquetes pueden estar sin comprimir o con /FlateDecode sin predictor, y la descompresión está acotada por MaxPacketBytes

El caso de un solo stream es donde el manejo de XML ingenuo se rompe. Los diseñadores suelen escribir <xfa:datasets> con prefijo de namespace mientras el template va sin prefijo, así que partir por nombre literal de etiqueta encuentra un paquete y pierde el otro. El convertidor empareja nombres de elementos y atributos por nombre local, después de una verificación separada de que las etiquetas de apertura y cierre se emparejen por su nombre completo con prefijo. Si solo necesita mirar los paquetes en lugar de convertirlos, la guía para detectar formularios XFA y extraer sus paquetes con PDFium VCL cubre esa ruta de solo lectura

Los valores de campos vienen de dos lugares. Un elemento <value> en el template suministra el valor por defecto, y el paquete datasets suministra los datos llenados, atados por nombre de campo. Los nombres de campos se repiten en formularios reales, así que el vinculador compara la cadena de ancestros <subform> con nombres contra la ruta de ancestros en los datos y toma la coincidencia más larga sin conflicto. Cuando dos campos hoja comparten nombre bajo subforms distintos, el /T del AcroForm se vuelve SubformPath.Name para que los dos campos sigan distintos tras la conversión

¿Por qué choiceList necesita un flag Combo explícito?

XFA y PDF tienen valores por defecto opuestos para campos de elección, así que un convertidor que no copia ningún flag convierte cada desplegable en un list box. Un <choiceList> de XFA con el open="userControl" por defecto o con open="onEntry" es un desplegable; un campo /FT /Ch de PDF sin flags es un list box desplazable, y solo el flag Combo (posición de bit 18 en la Tabla 230 de ISO 32000-1, valor 131072, el mismo bit que PDFium llama FPDF_FORMFLAG_CHOICE_COMBO) lo vuelve un combo box. El convertidor por tanto activa el flag Combo para los casos de desplegable y deja los flags vacíos solo cuando el template dice open="always" o open="multiSelect", que mantienen la lista permanentemente abierta. Las compilaciones anteriores a 3.181.1 intercambiaban los valores Combo y Pushbutton y buscaban un open="on" no estándar, así que los formularios aplanados con ellas vale la pena reprocesar

Decisión de flags del campo de elección en la conversión de XFA estático a AcroForm de PDFium Component: un atributo open de userControl o onEntry activa el flag Combo, bit 18 valor 131072, para producir un combo box desplegable, mientras open always o multiSelect deja los flags vacíos para un list box permanentemente abierto
Sin el flag Combo un desplegable XFA se aplana en un list box desplazable, así que el convertidor lee el atributo open y activa el bit 18 solo para los casos de desplegable

El resto del mapeo de interfaz sigue las tablas de flags de campos en ISO 32000-1 §12.7.4:

  • textEdit, numericEdit y dateTimeEdit se vuelven /FT /Tx; un campo de texto multilínea recibe el flag Multiline (/Ff 4096, Tabla 228) y un flujo de apariencia partido por líneas
  • passwordEdit se vuelve /FT /Tx con el flag Password (/Ff 8192) y una apariencia que dibuja un asterisco por escalar Unicode, no por unidad de código UTF-16
  • checkButton se vuelve un check box /FT /Btn cuyo valor es /Yes cuando el dato atado es 1 y /Off en caso contrario
  • <exclGroup> se vuelve un padre de radio /FT /Btn /Ff 32768 (Tabla 226) con /V, /DV, /Opt y /Kids, más un widget hijo por miembro con su propio /AS y /AP de dos estados
  • imageEdit se vuelve un pushbutton cuya apariencia pinta la imagen; un JPEG baseline se incrusta verbatim bajo /DCTDecode, mientras que el PNG se decodifica y re-codifica como píxeles /FlateDecode con un /SMask extra cuando el PNG lleva alfa o transparencia tRNS

Los valores que no son ASCII imprimible simple, incluidos los saltos de línea, se escriben como cadenas UTF-16BE <FEFF...>, así un nombre japonés o una dirección de dos líneas sobrevive el viaje. La salida Fields refleja lo que se escribió, lo que la convierte en una pista de auditoría conveniente

procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
  KindNames: array[TPdfXfaFlattenFieldKind] of string = (
    'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
  I: Integer;
  F: TPdfXfaFlattenField;
begin
  for I := 0 to High(Fields) do
  begin
    F := Fields[I];
    Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
      [F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
    case F.Kind of
      fxfkChoice:
        Writeln(Format('  %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
      fxfkRadio:
        Writeln(Format('  %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
      fxfkImage:
        Writeln(Format('  %d image bytes', [Length(F.ImageData)]));
    else
      Writeln('  value "' + F.Value + '"');
    end;
  end;
end;

Geometría, unidades de medida y enrutado de páginas

La geometría XFA se convierte a espacio de usuario de PDF invirtiendo cada campo contra la altura del MediaBox de su propia página, porque XFA mide hacia abajo desde el tope de la página y PDF mide hacia arriba desde el fondo. Los offsets de elementos <subform> anidados se acumulan en el descenso por el template, y cada campo debe terminar con ancho y alto positivos o la conversión se rechaza

Conversión de geometría XFA a PDF en PDFium Component para Delphi: un rectángulo de campo medido hacia abajo desde el borde superior en el template XFA se invierte contra la altura del MediaBox de su propia página al espacio de usuario de PDF, donde la coordenada y crece hacia arriba desde el fondo
El template XFA hacia abajo y el espacio de usuario PDF hacia arriba se encuentran en una resta contra la altura del MediaBox, que es lo que coloca cada widget convertido en su posición de diseño

Las medidas aceptan las unidades que los diseñadores reales emiten: números sueltos y pt, más in (72 puntos), mm (72 / 25.4), cm (72 / 2.54), pc (12), px (0.75) y mp (0.001, milipuntos). El parseo es independiente del locale, así que una instalación de Windows en alemán no convierte 8.5in en sin sentido. Un sufijo de unidad desconocido se rechaza en lugar de leerse como puntos, lo que colocaría el campo mal en silencio

El enrutado de páginas sigue el <pageSet>: los elementos <pageArea> hermanos se numeran desde uno en orden de declaración y reclaman los campos anidados bajo ellos, mientras que los campos sin ancestro <pageArea> se quedan en la página uno. Cada página recibe su propia actualización de /Annots. Un campo enrutado más allá de la última página del PDF es un error en lugar de un widget tirado en la página uno

¿Qué formularios XFA rechaza el convertidor?

El convertidor rechaza cualquier cosa cuyo layout no se pueda conocer sin correr el motor de layout de XFA. Eso significa un atributo layout distinto de position en un <subform> o <exclGroup> (flujo tb, lr-tb y amigos, con lo que se construyen los formularios dinámicos), cualquier elemento <script>, y cualquier tipo de UI fuera de los siete listados arriba. Los formularios dinámicos que crecen páginas en tiempo de ejecución pertenecen a otra cadena de herramientas; el artículo sobre repaginación XFA dinámica y conteos de páginas en PDFium Component muestra qué implica ese layout en vivo

El rechazo es deliberado. Un subform con flujo no tiene coordenadas fijas, así que cualquier "conversión" tendría que inventar posiciones, y un formulario con script a menudo calcula totales o conmuta visibilidad que un AcroForm estático no puede reproducir. Producir un PDF que se ve completo pero que silenciosamente perdió un total calculado es peor que un error claro en Report.ErrorText. La especificación XFA 3.3 define mucho más que este subconjunto, y el convertidor no finge lo contrario

Trampas al escribir y verificar la revisión AcroForm

Dos problemas aparecieron mientras se construía el escritor, y ambos aplican a cualquiera que produzca actualizaciones incrementales a mano. El primero: el catálogo siempre debe ser parte del conjunto de actualización, incluso cuando sus bytes no cambian porque el AcroForm es un objeto indirecto reescrito en otro lado. El escritor de actualizaciones dispersas deriva el /Root del trailer de los objetos que lleva, y una actualización sin el objeto raíz produce una revisión que los visores no pueden anclar

El segundo concierne a la verificación. Cargar el archivo convertido a través del wrapper de PDFium dispara la inicialización del entorno de formularios tan pronto como el documento tiene un /AcroForm, y el pdfium.dll anfitrión en el montaje de pruebas carecía del export FORM_GetTextDirection, así que la inicialización fallaba antes de poder revisar un solo campo. Las verificaciones de relectura usan por tanto el lector de objetos del componente, que parsea la revisión activa sin tocar la capa de formularios

uses
  System.SysUtils, System.Classes, FPdfCompress;

function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
  Stream: TBytesStream;
  Objects: TPdfActiveObjects;
  Revision: TPdfSparseRevisionInfo;
  ErrorText: string;
begin
  Stream := TBytesStream.Create(Pdf);
  try
    if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
      Objects, Revision, ErrorText) then
      raise Exception.Create(ErrorText);
    // el catálogo activo debe referenciar un AcroForm; un AcroForm inline
    // escrito por el convertidor no lleva entrada /XFA
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Los flujos de apariencia son deliberadamente simples: Helvetica a 10 puntos para texto y valores de elección, un cuadrado trazado para los check boxes, un cuadrado interno relleno para el estado de radio seleccionado. Hacen el formulario convertido legible en todas partes, pero no son una copia píxel a píxel del renderizado XFA original, así que las fuentes, los colores y los bordes definidos en el template no se trasladan. Si un formulario debe verse idéntico a la salida del diseñador, renderícelo una vez con la compilación XFA y compare antes de cambiar un pipeline de producción

FAQ

¿FlattenPdfXfaToAcroForm necesita la compilación V8 o XFA de PDFium?

No. FlattenPdfXfaToAcroForm parsea los paquetes y escribe la revisión con código de PDFium Component, así que funciona con el pdfium.dll estándar y no ejecuta ningún script XFA

¿Puede el convertidor manejar documentos XFA cifrados?

No directamente. Un origen cifrado se rechaza con un error, así que descifre primero el documento y convierta la copia descifrada

¿Los campos convertidos siguen siendo editables?

Sí. La salida contiene campos AcroForm reales de texto, check box, radio, elección y pushbutton con valores y flujos de apariencia, así que los usuarios pueden seguir llenando el formulario en cualquier visor con capacidad AcroForm

La conversión de XFA estático, la extracción de paquetes XFA y el runtime XFA habilitado para V8 son todos parte del PDFium Component for Delphi y C++Builder, que envuelve PDFium para proyectos Delphi, C++Builder y Lazarus