PDFium Component convierte formularios XFA estáticos en campos AcroForm ordinarios desde Delphi mediante FlattenPdfXfaToAcroForm en la unidad FPdfDocumentXfaFlatten. La función lee los packets de plantilla y datasets de XFA, mapea cada campo posicionado de forma absoluta a un campo terminal AcroForm con su propio appearance stream, y añade el resultado como una revisión incremental, así que el PDF convertido se abre como un formulario normal en visores que nunca soportaron XFA
La motivación suele ser un ticket de soporte más que un punto de hoja de ruta. Un formulario de gobierno o de seguros se maquetó en un diseñador que lo guardó como XFA, funciona en un lector de escritorio, y en todas partes se abre como una página en blanco que dice que el documento requiere un visor más nuevo. XFA quedó obsoleto en PDF 2.0 (ISO 32000-2), los navegadores y visores móviles nunca lo implementaron, y el propio PDFium solo renderiza XFA con la build con V8. Los formularios XFA estáticos, los que tienen el layout fijo en tiempo de diseño, llevan geometría suficiente en la plantilla como para reconstruirse como campos AcroForm, y esa es exactamente la porción que maneja este conversor
Qué produce realmente FlattenPdfXfaToAcroForm
FlattenPdfXfaToAcroForm produce una revisión AcroForm a nivel de bytes añadida a una copia sin cambios del origen, nunca una reescritura del fichero original. PDFium no expone ninguna API pública para crear campos de formulario, así que el componente no pasa por PDFium para este trabajo: lee la revisión activa con su propio lector de objetos, construye diccionarios de campo, widget annotations, appearance XObjects y un recurso de fuente /Helv como objetos PDF en crudo, y los serializa mediante 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 objeto indirecto, ese objeto se reescribe en su sitio; un diccionario inline se sustituye dentro del catálogo. En cualquier caso la revisión activa deja de apuntar a los packets 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; // el valor 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 rellena Report.ErrorText para 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 rellenar el informe. TPdfXfaFlattenReport lleva además SourceSha256 y OutputSha256, útil cuando un trabajo por lotes tiene que probar qué entrada produjo qué fichero convertido. TPdfXfaFlattenOptions.Default fija un presupuesto de packets de 8 MiB, 512 campos y un techo de salida de 512 MiB; el origen no debe estar cifrado
Leer los packets de plantilla y datasets
PDFium Component acepta ambos layouts que ISO 32000-1 §12.7.8 permite para la entrada /XFA: la forma de array que alterna nombres de packet y referencias a streams, y la forma XDP de stream único donde un stream contiene todo el documento <xdp:xdp>. Los streams de packet pueden estar sin comprimir o con /FlateDecode sin predictor, y la descompresión está acotada por MaxPacketBytes
El caso de stream único es donde el manejo XML ingenuo se rompe. Los diseñadores escriben habitualmente <xfa:datasets> con prefijo de namespace mientras la plantilla va sin prefijo, así que trocear por nombre de tag literal encuentra un packet y pierde el otro. El conversor empareja nombres de elemento y atributo por nombre local, tras una comprobación aparte de que las tags de apertura y cierre se emparejen por su nombre con prefijo completo. Si solo necesitáis mirar los packets en lugar de convertirlos, la guía para detectar formularios XFA y extraer sus packets con PDFium VCL cubre esa ruta de solo lectura
Los valores de campo vienen de dos sitios. Un elemento <value> en la plantilla suministra el valor por defecto, y el packet de datasets suministra los datos rellenados, ligados por nombre de campo. Los nombres de campo se repiten en formularios reales, así que el ligador compara la cadena de ancestros <subform> nombrados con la ruta de ancestros en los datos y toma la coincidencia más larga sin conflictos. Cuando dos campos hoja comparten nombre bajo subforms distintos, el /T del AcroForm se convierte en SubformPath.Name para que los dos campos sigan siendo distintos tras la conversión
Por qué choiceList necesita un flag Combo explícito
XFA y PDF tienen valores por defecto opuestos para los campos de elección, así que un conversor que no copia ningún flag convierte cada desplegable en una 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 una list box con scroll, 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 convierte en combo box. El conversor pone por tanto el flag Combo para los casos de desplegable y deja los flags vacíos solo cuando la plantilla dice open="always" o open="multiSelect", que mantienen la lista permanentemente abierta. Las builds anteriores a 3.181.1 intercambiaban los valores de Combo y Pushbutton y buscaban un open="on" no estándar, así que los formularios aplanados con ellas merecen reprocesarse
El resto del mapeo de UI sigue las tablas de flags de campo de ISO 32000-1 §12.7.4:
textEdit,numericEditydateTimeEditse convierten en/FT /Tx; un campo de texto multilínea recibe el flag Multiline (/Ff 4096, Tabla 228) y un appearance stream partido en líneaspasswordEditse convierte en/FT /Txcon el flag Password (/Ff 8192) y un appearance que dibuja un asterisco por escalar Unicode, no por unidad de código UTF-16checkButtonse convierte en una casilla/FT /Btncuyo valor es/Yescuando el dato ligado es1y/Offen caso contrario<exclGroup>se convierte en 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 convierte en un pushbutton cuyo appearance pinta la imagen; un JPEG de línea base se incrusta verbatim bajo/DCTDecode, mientras que el PNG se decodifica y recodifica como píxeles/FlateDecodecon un/SMaskextra cuando el PNG lleva alfa o transparenciatRNS
Los valores que no son ASCII imprimible plano, incluidos saltos de línea, se escriben como strings <FEFF...> UTF-16BE, así que un nombre japonés o una dirección de dos líneas sobreviven al viaje. La salida Fields refleja lo escrito, lo que la hace una pista de auditoría práctica
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 al espacio de usuario PDF volteando cada campo contra la altura del MediaBox de su propia página, porque XFA mide hacia abajo desde la parte superior de la página y PDF mide hacia arriba desde abajo. Los offsets de los elementos <subform> anidados se acumulan al bajar por la plantilla, y cada campo debe acabar con anchura y altura positivas o la conversión se rechaza
Las medidas aceptan las unidades que los diseñadores reales emiten: números pelados 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 de la locale, así que una instalación de Windows en alemán no convierte 8.5in en nonsense. 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 soltado en la página uno
Qué formularios XFA rechaza el conversor
El conversor rechaza cualquier cosa cuyo layout no se pueda conocer sin ejecutar el motor de layout XFA. Eso significa un atributo layout distinto de position en un <subform> o <exclGroup> (los fluidos tb, lr-tb y amigos, con los 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 la repaginación XFA dinámica y los recuentos de páginas en PDFium Component muestra qué implica ese layout en vivo
El rechazo es deliberado. Un subform fluido no tiene coordenadas fijas, así que cualquier «conversión» tendría que inventar posiciones, y un formulario con scripts suele calcular totales o conmutar visibilidad que un AcroForm estático no puede reproducir. Producir un PDF que parece completo pero que perdió silenciosamente 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 conversor no pretende lo contrario
Trampas al escribir y verificar la revisión AcroForm
Dos problemas aparecieron mientras se construía el writer, y ambos aplican a cualquiera que produzca actualizaciones incrementales a mano. El primero: el catálogo debe formar siempre parte del conjunto de actualización, aunque sus bytes no cambien porque el AcroForm es un objeto indirecto reescrito en otra parte. El writer de actualización dispersa 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 fichero convertido a través del envoltorio de PDFium dispara la inicialización del entorno de formularios en cuanto el documento tiene un /AcroForm, y el pdfium.dll anfitrión en la configuración de pruebas carecía del export FORM_GetTextDirection, así que la inicialización fallaba antes de que se pudiera comprobar un solo campo. Las comprobaciones de lectura de vuelta 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 conversor no lleva entrada /XFA
Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
finally
Stream.Free;
end;
end;
Los appearance streams son deliberadamente simples: Helvetica a 10 puntos para texto y valores de elección, un cuadrado con trazo para las casillas, un cuadrado interior relleno para el estado de radio seleccionado. Hacen el formulario convertido legible en todas partes, pero no son una copia al píxel del renderizado XFA original, así que las fuentes, colores y bordes definidos en la plantilla no se trasladan. Si un formulario tiene que verse idéntico a la salida del diseñador, renderizadlo una vez con la build XFA y comparad antes de cambiar un pipeline de producción
FAQ
¿Necesita FlattenPdfXfaToAcroForm la build V8 o XFA de PDFium?
No. FlattenPdfXfaToAcroForm parsea los packets 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 conversor manejar documentos XFA cifrados?
No directamente. Un origen cifrado se rechaza con un error, así que descifrad primero el documento y convertid la copia descifrada
¿Los campos convertidos siguen siendo editables?
Sí. La salida contiene campos AcroForm reales de texto, casilla, radio, elección y pushbutton con valores y appearance streams, así que los usuarios pueden seguir rellenando el formulario en cualquier visor con soporte AcroForm
La conversión de XFA estático, la extracción de packets XFA y el runtime XFA con V8 son todos parte del PDFium Component para Delphi y C++Builder, que envuelve PDFium para proyectos Delphi, C++Builder y Lazarus