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
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
El resto del mapeo de interfaz sigue las tablas de flags de campos en ISO 32000-1 §12.7.4:
textEdit,numericEditydateTimeEditse 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íneaspasswordEditse vuelve/FT /Txcon el flag Password (/Ff 8192) y una apariencia que dibuja un asterisco por escalar Unicode, no por unidad de código UTF-16checkButtonse vuelve un check box/FT /Btncuyo valor es/Yescuando el dato atado es1y/Offen caso contrario<exclGroup>se vuelve un padre de radio/FT /Btn /Ff 32768(Tabla 226) con/V,/DV,/Opty/Kids, más un widget hijo por miembro con su propio/ASy/APde dos estadosimageEditse 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/FlateDecodecon un/SMaskextra cuando el PNG lleva alfa o transparenciatRNS
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
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