Una factura AcroForm con un puñado de campos de texto y sin contenido XFA abre bien mediante un pdfium.dll común, y sin embargo el mismo archivo puede hacer que FPDFDOC_InitFormFillEnvironment devuelva NULL en cuanto el proceso corre con el pdfium.v8.dll con XFA. Los bytes del documento son idénticos en ambas corridas. Lo que difiere es un entero: los builds de PDFium con XFA exigen que FPDF_FORMFILLINFO.version sea 2, y un binding que escribe 1 para documentos sin XFA es rechazado antes de que PDFium mire un solo callback
¿Por qué FPDFDOC_InitFormFillEnvironment rechaza un PDF AcroForm común?
El rechazo viene del binario, y el documento no tiene voz en ello. El encabezado de form fill, fpdf_formfill.h en PDFium y su traducción Pascal en FPdfFormFill.pas, dice que un build compilado con el módulo XFA exige la versión 2, y lo dice dos veces en el mismo bloque de comentarios. Un build sin XFA acepta 1 o 2. Bajo un binding que manda la versión 1 para archivos sin XFA, solo una de las cuatro combinaciones de binario y documento falla: un registro de versión 1 entregado al pdfium.v8.dll junto con un archivo AcroForm ordinario. El DLL común acepta ese registro. El DLL V8 acepta un registro de versión 2 para cualquier documento. Un formulario XFA real sobre el DLL V8 nunca recibe la versión 1 de un binding que derive la versión del documento, así que la celda que falla es precisamente la que nadie piensa en probar
Un binding escrito a mano ve que FPDFDOC_InitFormFillEnvironment devuelve NULL en ese punto y no tiene nada más de dónde agarrarse. TPdf en PDFium Component reportaría un entorno rechazado como un EPdfError durante Active := True, pero siempre escribe la versión 2, así que esa celda nunca aparece ahí. Caer en esa celda es fácil. Un host pone el EnableV8Engine global en True porque quiere JavaScript de AcroForm, y desde entonces cada PDF del proceso pasa por el binario V8. O el host nunca toca el flag, pero el primer archivo que abre tiene una entrada /XFA en su diccionario AcroForm o /NeedsRendering true en su catálogo: cuando la biblioteca PDFium aún no se ha cargado, TPdf escanea el archivo buscando esos dos marcadores y activa EnableV8Engine antes de cargar el DLL. En cualquier caso la elección vale para el resto del proceso, y cada archivo AcroForm común abierto después se topa con el build XFA. Qué binario distribuir en primer lugar lo cubre la guía de despliegue del DLL de PDFium y diagnóstico de fallos de carga; todo lo de abajo asume que el build V8 ya está cargado
Lo que promete el campo version de FPDF_FORMFILLINFO
FPDF_FORMFILLINFO.version le dice a PDFium cuánto del registro puede leer. La versión 1 cubre la parte estable: Release, los callbacks de FFI_Invalidate a FFI_DoGoToAction, y el puntero m_pJsPlatform. La versión 2 añade una cola experimental. Un build sin XFA que recibe la versión 2 también llamará a los callbacks experimentales que encuentre ahí; un build con XFA no acepta nada menor. El encabezado nunca menciona el documento. Al escribir 2 usted promete que la memoria después de m_pJsPlatform existe y que cada ranura en ella contiene o una función cdecl válida o NULL
Esa cola empieza con xfa_disabled, un FPDF_BOOL que se ignora por debajo de la versión 2, seguido de diecisiete punteros a función desde FFI_DisplayCaret hasta FFI_DoURIActionWithKeyboardModifier. Quince de ellos, de FFI_DisplayCaret a FFI_PutRequestURL, están documentados como "Required for XFA, otherwise set to NULL". Los dos últimos, FFI_OnFocusChange y FFI_DoURIActionWithKeyboardModifier, son opcionales para cualquier host, y el encabezado añade que un embedder de versión 2 que deje FFI_DoURIActionWithKeyboardModifier sin asignar ve sus clics de URI redirigidos a FFI_DoURIAction. En conjunto, NULL es el valor especificado para un host que no maneja XFA. Un registro puesto a cero con FillChar y sellado con la versión 2 cumple el contrato en un build común exactamente como lo cumple un registro de versión 1, y además es el único registro que un build XFA aceptará
El lado de JavaScript sigue el mismo patrón. m_pJsPlatform no lo usan los builds sin V8; en un build V8 un puntero NULL impide que JavaScript corra mientras el documento se renderiza. El registro IPDF_JsPlatform al que apunta tiene su propio campo version, y el encabezado dice que ese actualmente debe ser 2
La trampa: dejar que el documento elija la versión del ABI
El error es un condicional que se lee sensato por sí solo. Un binding escrito a mano ya tiene que decidir si vale la pena cablear los callbacks XFA, y es tentador dejar que la misma prueba elija el número de versión
// Una forma tentadora para un binding escrito a mano: la versión sigue al documento
FillChar(FormInfo, SizeOf(FormInfo), 0);
IsXfa := FPDF_GetFormType(Doc) in [FORMTYPE_XFA_FULL, FORMTYPE_XFA_FOREGROUND];
if IsXfa and XfaFeaturesAvailable and V8FeaturesAvailable then
FormInfo.version := 2
else
FormInfo.version := 1; // todo documento sin XFA recibe 1
FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo);
// en pdfium.v8.dll con un archivo AcroForm común, FormHandle es nil aquí
La intención detrás de esa rama es defensiva: mantener la versión en 1 para que un build XFA nunca lea ranuras que el host no llenó. Pero el FillChar de la línea de arriba ya las llenó con NULL, que el encabezado define como el valor correcto para un host que no corre XFA. La rama se protege de un peligro que la declaración eliminó y camina directo hacia el que el encabezado advierte. Tampoco puede atraparse probando archivos XFA, porque esos toman la ruta de versión 2 siempre
PDFium Component construye el registro al revés. Siempre que TPdf.FormFill es True, abrir un documento limpia el registro completo, escribe la versión 2 y xfa_disabled := 1, y solo entonces pregunta qué necesita el documento. Dicho como regla para cualquier binding: fije la versión según el registro que declaró, y deje que nada más la toque. Esa es la forma para su propio código, con un envoltorio THostFormFillInfo declarado más abajo
// FormInfo es un campo THostFormFillInfo de su objeto visor, así que sobrevive
// al form handle; HostInvalidate, HostGetPage y el resto son sus propios
// callbacks cdecl
FillChar(FormInfo, SizeOf(FormInfo), 0);
FormInfo.Owner := Self;
FormInfo.Info.version := 2; // coincide con el diseño declarado, siempre
FormInfo.Info.xfa_disabled := 1; // XFA queda apagado salvo prueba de usabilidad
FormInfo.Info.FFI_Invalidate := HostInvalidate;
FormInfo.Info.FFI_GetPage := HostGetPage;
FormInfo.Info.FFI_GetCurrentPage := HostGetCurrentPage;
// ... los demás callbacks de versión 1
IsXfa := FPDF_GetFormType(Doc) in [FORMTYPE_XFA_FULL, FORMTYPE_XFA_FOREGROUND];
RuntimeReady := IsXfa and XfaFeaturesAvailable and V8FeaturesAvailable;
if RuntimeReady then
begin
FormInfo.Info.xfa_disabled := 0;
FormInfo.Info.FFI_DisplayCaret := HostDisplayCaret;
FormInfo.Info.FFI_PageEvent := HostPageEvent;
// ... las demás ranuras de versión 2
end;
FormHandle := FPDFDOC_InitFormFillEnvironment(Doc, FormInfo.Info);
if FormHandle = nil then
raise Exception.Create('Cannot initialize form fill environment');
XfaRunning := RuntimeReady and (FPDF_LoadXFA(Doc) <> 0);
Dónde pertenecen RuntimeReady y V8FeaturesAvailable
Las comprobaciones de capacidades deciden qué ranuras se llenan, y en PDFium Component jamás tocan el diseño. TPdf calcula la preparación a partir de tres hechos: el documento reporta un tipo de formulario XFA vía TPdf.XFA, el XfaFeaturesAvailable global encuentra los helpers de cadenas XFA exportados por el DLL cargado, y V8FeaturesAvailable encuentra las exportaciones V8. Los callbacks de versión 1 se cablean para todo documento, porque AcroForm y XFA dependen de ellos. Las diecisiete ranuras de versión 2 se asignan, y xfa_disabled se limpia, solo cuando los tres hechos se cumplen. Después de que el entorno existe, FPDF_LoadXFA corre solo para ese caso, y solo un retorno distinto de cero hace que TPdf.XfaRuntimeAvailable reporte True. FPDF_LoadXFA se exporta incluso en builds sin el motor XFA y ahí simplemente devuelve false, razón por la cual la sola comprobación de la exportación no basta
Cuando el documento es XFA pero falta el runtime, el registro se queda en versión 2 con xfa_disabled en 1 y la cola en NULL. El entorno abre, el contenido XFA queda estático, y TPdf lanza OnXfaRuntimeMissing para que el host pueda sugerir reiniciar sobre pdfium.v8.dll. m_pJsPlatform sigue una tercera regla: se adjunta siempre que V8FeaturesAvailable sea true, también para archivos AcroForm comunes, que es lo que permite que los scripts de documento, los cálculos de campos y app.alert corran bajo el build V8. PageCount lee el árbol de páginas estático hasta que XFA repagina el documento vía FFI_PageEvent; en ese momento el total nuevo se toma de FPDF_GetPageCount y se reporta vía OnXfaPageCountChanged
En el lado del host, dos propiedades responden dos preguntas distintas. XFA le dice qué contiene el archivo, desde FPDF_GetFormType, sin importar el binario. XfaRuntimeAvailable le dice si el motor realmente arrancó para este documento. Lea la segunda después de que Active pase a True, como hace el artículo de detección de formularios XFA y extracción de paquetes XFA, y deje el campo version fuera del razonamiento por completo
procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
// Se lanza mientras el documento abre cuando es XFA pero el DLL cargado no
// puede correr el motor. El entorno de formulario sigue existiendo, sin XFA
StatusBar.SimpleText :=
'XFA form detected; restart with pdfium.v8.dll for dynamic rendering';
end;
procedure TMainForm.OpenDocument(const FileName: string);
begin
Pdf.Active := False;
Pdf.OnXfaRuntimeMissing := PdfXfaRuntimeMissing;
Pdf.FormFill := True;
Pdf.FileName := FileName;
Pdf.Active := True; // los archivos AcroForm comunes también abren bajo pdfium.v8.dll
if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
ShowStaticXfaNotice; // su propia rutina de UI
end;
Errores de diseño que rompen el registro antes de leer la versión
Un número de versión correcto solo ayuda si los bytes detrás de él están donde el compilador de C los puso. Varios errores de binding producen un registro que PDFium malinterpreta sin importar qué versión lleve, y uno de ellos permanece invisible en Win32: declarar FPDF_FORMFILLINFO como packed record. En Win32 cada campo es de cuatro bytes, así que los diseños packed y alineados coinciden de casualidad. En Win64 el struct de C rellena cuatro bytes después de version para que Release caiga en un límite de 8 bytes, y vuelve a rellenar después de xfa_disabled; una declaración Pascal packed suelta ambos huecos y desplaza cada puntero de callback cuatro bytes. La declaración en FPdfFormFill.pas es un record plano por esa razón
El segundo error es un registro truncado: una traducción que se detiene en m_pJsPlatform pero escribe la versión 2. PDFium entonces lee la cola de la memoria que sea que siga al registro. El tercero es el tiempo de vida. El encabezado dice que el registro pasado a FPDFDOC_InitFormFillEnvironment debe seguir válido hasta que el form handle devuelto se cierre, así que un registro en una variable local del procedure que abre el documento es un puntero colgante en cuanto ese procedure retorna. TPdf conserva su registro como campo del componente, envuelto en un registro externo que coloca el struct de PDFium en el offset cero y un back-pointer al TPdf dueño justo después, que es cómo cada callback estático encuentra de nuevo su objeto
type
// Mal en Win64: el packing elimina el relleno que el struct de C tiene
// después de version, así que Release queda en el offset 4 aquí y en el 8 en PDFium
TBadFormFillInfo = packed record
version: Integer;
Release: Pointer;
// ... los demás campos
end;
// Su envoltorio propio: primero el registro de PDFium, su back-pointer después
THostFormFillInfo = record
Info: FPDF_FORMFILLINFO; // offset 0, así que pThis se convierte de vuelta directo
Owner: TObject;
end;
PHostFormFillInfo = ^THostFormFillInfo;
procedure HostInvalidate(pThis: PFPDF_FORMFILLINFO; page: FPDF_PAGE;
left, top, right, bottom: Double); cdecl;
begin
try
// TMyViewer e InvalidatePdfRect representan su propia clase visor
TMyViewer(PHostFormFillInfo(pThis).Owner).InvalidatePdfRect(
page, left, top, right, bottom);
except
// una excepción Pascal no debe desenvolverse a través de los frames C++ de PDFium
end;
end;
Dos ranuras más merecen cuidado. FFI_GetLocalTime devuelve un FPDF_SYSTEMTIME de 16 bytes por valor, y Delphi y MSVC discrepan en Win32 sobre dónde va el puntero de resultado oculto, así que una implementación Pascal corrompería el FPDF_FORMFILLINFO de quien llama. El PDFium actual marca el miembro como sin uso, y la declaración Pascal trae un comentario de dejarlo en nil; PDFium Component lo hace. Cada callback que el componente instala atrapa excepciones Pascal y devuelve el valor de fallo documentado en su lugar, porque el ABI de C no tiene forma de llevar una excepción de vuelta a través del límite. El escrito sobre endurecer el ABI de PDFium en Delphi recorre esa disciplina usando FFI_OpenFile como ejemplo
Versión de protocolo y disponibilidad de características son dos ejes
Un campo de versión en una estructura de callbacks responde qué tan grande es el registro y qué se puede leer de él. La detección de características responde cuáles de esas ranuras harán algo. Lo primero lo fija el binario nativo y la declaración contra la que compiló; lo segundo varía por documento, por tabla de exportaciones del DLL y por ajuste del host. El caso XFA necesita ambas respuestas a la vez, lo que hace tentador plegarlas en un booleano, y ese plegado se rompe en cuanto un build exige una versión mínima. Los formularios XFA, que ISO 32000-1 §12.7.8 describe como un payload XML transportado junto al diccionario AcroForm, son la característica de esta historia. El diseño del registro es el protocolo, y PDFium tiene derecho a comprobar el diseño antes de leer el archivo
La misma estructura aparece donde sea que una biblioteca de C versione sus structs: FPDF_LIBRARY_CONFIG, IPDF_JsPlatform, cualquier tabla de callbacks con un encabezado de tamaño o versión. Declare el diseño más nuevo que entienda, límpielo por completo, escriba la versión correspondiente sin condición, y llene ranuras según lo que el documento y el DLL realmente puedan hacer. Si un futuro fpdf_formfill.h introduce la versión 3, el trabajo es una declaración nueva y una asignación cambiada. Una rama guiada por el contenido del documento tendría que revisarse para cada combinación de build y archivo, y la que equivoca suele ser la que nadie abrió
El manejo de registros descrito aquí es parte de PDFium Component para Delphi, Lazarus y C++Builder, con la misma declaración sirviendo para builds Win32 y Win64. Una aplicación que ya distribuye pdfium.v8.dll para AcroForms con JavaScript puede abrir el resto de su archivo, XFA o no, mediante ese único binario sin manejo especial para el entorno de formulario