Artículo técnico

FPDF_FORMFILLINFO versión 2 en Delphi: sigue el ABI del DLL

PDFium Component pone ahora FPDF_FORMFILLINFO.version a 2 en todos los entornos de form-fill que inicializa, porque la versión que acepta una build nativa de PDFium es una propiedad de esa build, no del documento que se abre. Un pdfium.v8.dll con XFA habilitado rechaza la versión 1 de plano, así que un PDF AcroForm normal abierto a través de él fallaba en FPDFDOC_InitFormFillEnvironment sin un solo XFA a la vista. El arreglo de la v3.116.0 es pequeño, pero el error que hay detrás es general y merece que se le ponga nombre: un campo de versión de protocolo describe el diseño de memoria que espera la otra parte, y no debe derivarse nunca de si resulta que necesitas las funciones que ese diseño trae

¿Por qué falla FPDFDOC_InitFormFillEnvironment con un PDF normal y pdfium.v8.dll?

El entorno falla porque una build de PDFium con XFA valida el campo version antes de hacer ninguna otra cosa, y la lógica antigua del wrapper le entregaba un 1 siempre que el documento actual no fuera un formulario XFA. El síntoma en un host Delphi es un EPdfError lanzado desde TPdf.InitializeFormFill con el mensaje Cannot initialize form fill environment, disparado al abrir una factura o un formulario fiscal corriente que no tiene más que campos de texto AcroForm. El mismo archivo se abre sin problema contra el pdfium.dll normal. El mismo DLL abre sin problema un documento XFA de verdad. Solo rompe la combinación de la build V8 con un documento no XFA, que es exactamente la combinación en la que acaba un host después de activar EnableV8Engine para tener JavaScript de AcroForm, o después de que la autoselección de LoadDocument haya comprometido ya el proceso con pdfium.v8.dll por un archivo XFA anterior. Ese compromiso es de todo el proceso: EnableV8Engine se lee antes del primer LoadLibrary, y una vez cargada la build XFA todos los PDF normales posteriores pasan por el mismo montaje de entorno contra el mismo binario. El host no hizo nada mal; el wrapper hizo la pregunta equivocada al rellenar el registro. Si todavía estás decidiendo qué binario distribuir, nuestra nota sobre desplegar el DLL de PDFium y diagnosticar fallos de carga cubre la elección entre normal y V8, y este artículo da por hecho que la build V8 ya está en el proceso

Diagrama de PDFium Component de las cuatro combinaciones del pdfium.dll normal y el pdfium.v8.dll con XFA frente a documentos AcroForm y XFA: un registro de versión 1 solo rompía la build V8 con un formulario normal, EPdfError en FPDFDOC_InitFormFillEnvironment, mientras que el registro de versión 2 corregido abre las cuatro
Un único condicional ataba la versión del ABI al documento, así que la elección de la build V8 para todo el proceso convertía cada PDF normal posterior en una inicialización de entorno fallida

¿Qué promete en realidad el campo version de FPDF_FORMFILLINFO?

FPDF_FORMFILLINFO.version le dice a PDFium qué campos del registro puede leer, y la cabecera pública fpdf_formfill.h ata los valores aceptables a cómo se compiló la librería y no al documento. Parafraseando, el contrato tiene tres partes. La versión 1 cubre los callbacks estables desde FFI_Invalidate hasta FFI_DoGoToAction más el puntero m_pJsPlatform. Una build sin el módulo XFA acepta 1 o 2, y con 2 también llamará a los callbacks experimentales adicionales. Una build con el módulo XFA exige 2, punto, y la cabecera repite ese requisito dos veces como si esperara que la gente se lo saltara. En ningún sitio menciona el contrato al documento. La versión es una declaración sobre el registro que has reservado: con un 2 estás prometiendo que la memoria posterior a m_pJsPlatform existe y contiene punteros de función válidos o NULL

La región de la versión 2 es donde vive toda la maquinaria XFA. Empieza con xfa_disabled, un FPDF_BOOL que la cabecera describe como ignorado por debajo de la versión 2 y significativo solo cuando el módulo XFA está compilado, y sigue con diecisiete punteros de función, de FFI_DisplayCaret a FFI_DoURIActionWithKeyboardModifier. De cada uno se documenta que es obligatorio para XFA y que en caso contrario debe ponerse a NULL. Esa frase es la clave de todo el arreglo. NULL no es un estado de error en esos huecos; es el estado documentado para un host que no está gobernando XFA. Un registro limpiado con FillChar y marcado luego como versión 2 cumple el contrato en una build sin XFA exactamente igual de bien que un registro de versión 1, y es el único registro que una build con XFA aceptará

Diagrama de PDFium Component del registro FPDF_FORMFILLINFO en Delphi: la versión 1 cubre los callbacks de FFI_Invalidate a FFI_DoGoToAction más m_pJsPlatform, la versión 2 añade xfa_disabled y diecisiete punteros de la era de FFI_DisplayCaret, FillChar limpia todos los bytes, y los huecos a NULL son el estado documentado para un host que no gobierna XFA
El registro Pascal es siempre el diseño completo de la versión 2, así que una build con XFA lo acepta y una build normal simplemente nunca llama a los huecos experimentales que se quedan a NULL

La antigua selección ataba el ABI al documento

El defecto era un único condicional que en aislamiento parecía razonable. TPdf.InitializeFormFill calcula un flag RuntimeReady a partir de tres hechos: que el documento informe de un tipo de formulario XFA a través de TPdf.XFA, que los helpers de cadenas XFA se hayan resuelto vía XfaFeaturesAvailable y que las exportaciones V8 se hayan resuelto vía V8FeaturesAvailable. Antes de la v3.116.0 ese mismo flag elegía también la versión

// v3.115.0 y anteriores: la versión del ABI seguía al documento
RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;

if RuntimeReady then
  FFormFillInfo.Info.version := 2
else
  FFormFillInfo.Info.version := 1;

// ... y la rama de runtime ausente la volvía a fijar
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Léelo con la cabecera en la mano y el fallo es evidente. RuntimeReady es false para todo documento AcroForm normal, así que todo documento normal anunciaba la versión 1. En pdfium.dll eso da igual. En pdfium.v8.dll, que es la build con XFA, PDFium comprueba el campo, lo encuentra por debajo del 2 exigido y devuelve un FPDF_FORMHANDLE nulo, que CheckPdf convierte en la excepción de arriba. La intención del código antiguo era defensiva: mantener la versión 1 para que una build XFA no leyera nunca los huecos de la versión 2 sin asignar. Se defendía de un problema que la cabecera ya descarta y creaba uno del que la cabecera avisa explícitamente. El código corregido decide la versión una sola vez, por delante, a partir de lo que el registro es físicamente

procedure TPdf.InitializeFormFill;
var
  RuntimeReady: Boolean;
begin
  FXfaRuntimeUsable := False;
  FXfaPageCountOverride := -1;   // centinela: usa el árbol de páginas estático
  if not FormFill then
    Exit;

  FillChar(FFormFillInfo, SizeOf(FFormFillInfo), 0);
  FFormFillInfo.Pdf := Self;

  // El registro versión 2 completo se reserva y se limpia arriba. PDFium
  // acepta la versión 2 sin XFA y la exige en toda build con XFA
  // habilitado, incluso cuando este documento no contiene formulario XFA.
  FFormFillInfo.Info.version := 2;
  FFormFillInfo.Info.xfa_disabled := 1;

  // RuntimeReady condiciona los callbacks XFA y xfa_disabled, nunca la versión.
  RuntimeReady := XFA and XfaFeaturesAvailable and V8FeaturesAvailable;
  ...

Dónde sigue perteneciendo RuntimeReady: los callbacks y xfa_disabled

RuntimeReady conserva su trabajo como puerta del comportamiento XFA; simplemente ya no toca el diseño del registro. Los callbacks de la versión 1, FFI_Invalidate, FFI_SetTimer, FFI_GetPage, FFI_DoURIAction, FFI_DoGoToAction y el resto de ese bloque, se cablean incondicionalmente porque tanto AcroForm como XFA dependen de ellos. Los diecisiete punteros de la versión 2 se asignan solo dentro de la rama RuntimeReady, junto con xfa_disabled := 0. Cuando el documento es XFA pero el runtime no está, el registro se queda en versión 2 con xfa_disabled a 1 y los huecos de la versión 2 a NULL, y el wrapper dispara OnXfaRuntimeMissing para que el host pueda sugerir reiniciar con pdfium.v8.dll. Una vez existe el entorno, FPDF_LoadXFA se llama solo cuando RuntimeReady era true, y solo un retorno true pone FXfaRuntimeUsable, que es lo que informa TPdf.XfaRuntimeAvailable

  if RuntimeReady then
  begin
    FFormFillInfo.Info.xfa_disabled := 0;   // 0 = XFA habilitado
    FFormFillInfo.Info.FFI_DisplayCaret := FormFillDisplayCaret;
    FFormFillInfo.Info.FFI_GetCurrentPageIndex := FormFillGetCurrentPageIndex;
    FFormFillInfo.Info.FFI_SetCurrentPage := FormFillSetCurrentPage;
    FFormFillInfo.Info.FFI_GotoURL := FormFillGotoURL;
    FFormFillInfo.Info.FFI_GetPageViewRect := FormFillGetPageViewRect;
    FFormFillInfo.Info.FFI_PageEvent := FormFillPageEvent;
    FFormFillInfo.Info.FFI_PopupMenu := FormFillPopupMenu;
    FFormFillInfo.Info.FFI_OpenFile := FormFillOpenFile;
    FFormFillInfo.Info.FFI_EmailTo := FormFillEmailTo;
    // ... de FFI_UploadTo a FFI_DoURIActionWithKeyboardModifier
  end
  else if XFA then
  begin
    // Runtime no disponible: mantén la versión 2, deja XFA desactivado y avisa al host.
    if Assigned(FOnXfaRuntimeMissing) then
      FOnXfaRuntimeMissing(Self);
  end;

  FFormHandle := FPDFDOC_InitFormFillEnvironment(FDocument, FFormFillInfo.Info);
  CheckPdf(FFormHandle <> nil, 'Cannot initialize form fill environment');
  if RuntimeReady then
    FXfaRuntimeUsable := FPDF_LoadXFA(FDocument) <> 0;

Dos detalles de ese bloque son fáciles de estropear cuando escribes tu propio binding. FXfaPageCountOverride se reinicia a -1 como centinela antes de que ocurra nada más, para que PageCount caiga al árbol de páginas estático hasta que FFI_PageEvent informe de una repaginación; un cero ahí reclamaría en silencio un documento vacío. Y cada uno de los callbacks de la versión 2 es una rutina estática cdecl que recupera el TPdf propietario desde el registro y se traga cualquier excepción Pascal antes de volver a PDFium, que es la disciplina que nuestra nota sobre endurecer el ABI de PDFium en Delphi detalla para FFI_OpenFile. Nada del cambio de versión relaja ninguna de las dos reglas

¿Es segura la versión 2 cuando el DLL no tiene módulo XFA?

Sí, y la razón está en el registro, no en una promesa de la librería. En una build sin XFA la cabecera dice que la versión 2 hace que también se llamen los callbacks experimentales, así que la pregunta es qué encuentra PDFium cuando mira. TPdfFormFillInfo es un registro empaquetado cuyo miembro Info es el FPDF_FORMFILLINFO completo, incluidos todos los campos de la versión 2, y InitializeFormFill limpia todo el conjunto con FillChar antes de tocar un byte. Así que en un pdfium.dll normal con un documento normal la librería ve versión 2, xfa_disabled puesto y NULL en todos los huecos experimentales, que es precisamente el estado que la cabecera prescribe para un host que no implementa XFA. No hay ningún registro truncado por el que la librería pueda leer de más, porque el registro nunca fue más corto que la versión 2. La lógica antigua defendía un desajuste de diseño que la declaración Pascal ya había eliminado

El límite que merece decirse con honestidad es el que el registro no puede cubrir. La versión 2 en un documento normal no enciende JavaScript, ni el scripting de XFA, ni ninguno de los eventos de host que hay detrás de esos callbacks. m_pJsPlatform se engancha solo cuando V8FeaturesAvailable es true, XFA sigue desactivado a menos que RuntimeReady fuera true, y TPdf.XFA sigue informando del tipo de formulario desde FPDF_GetFormType independientemente de lo que negociara el entorno. Un host que quiera saber si el XFA dinámico va a renderizarse de verdad debería seguir leyendo XfaRuntimeAvailable después de que Active pase a true, como recomienda nuestra nota sobre detectar formularios XFA y extraer paquetes XFA, en vez de deducir nada del campo version

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Se dispara desde InitializeFormFill cuando el documento es XFA pero el
  // pdfium.dll cargado no puede ejecutar el motor. El entorno de formulario se abre igual,
  // porque la versión 2 se pasó en ambos casos; solo está apagado el runtime XFA.
  StatusBar.SimpleText :=
    'XFA form detected; restart with pdfium.v8.dll to enable 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;   // ya no lanza excepción con un PDF normal bajo pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

Versión de protocolo y disponibilidad de funciones son dos ejes distintos

La regla general que sale de este arreglo es que un campo de versión en una estructura de callbacks responde a la pregunta «qué tamaño tiene este registro y qué puedes leer de él», mientras que la detección de capacidades responde a «cuáles de esos huecos harán algo útil». Lo primero lo fijan el binario nativo y la declaración Pascal contra la que compilaste. Lo segundo varía por documento, por tabla de exportaciones del DLL y por configuración del host. Fundir las dos cosas en un solo booleano es tentador porque el caso XFA resulta necesitar ambas, pero en el momento en que una build impone una versión mínima la fusión se rompe para todos los documentos que no necesitan la función. Los formularios XFA, descritos en ISO 32000-1 §12.7.8 como una carga XML que vive junto al diccionario AcroForm, son aquí la función; el diseño del registro es el protocolo, y PDFium tiene derecho a insistir en el diseño antes de mirar siquiera el archivo. La misma forma aparece allí donde una librería C versiona sus estructuras: un bloque de viewer-info, un registro de opciones de render, una tabla de callbacks de plataforma. El patrón seguro es el que sigue el InitializeFormFill corregido. Declara el diseño más nuevo que entiendas, límpialo por completo, pon la versión a juego con ese diseño de forma incondicional y deja luego que las comprobaciones de capacidad decidan qué huecos rellenar. Si una cabecera futura de PDFium añade una versión 3, el cambio es en la declaración y en esa única asignación, no en una rama dependiente del documento que estará mal para la combinación que nadie probó

Diagrama de PDFium Component que separa los dos ejes que hay detrás de FPDF_FORMFILLINFO: la versión de protocolo, fijada por el diseño del registro y el binario nativo, y la disponibilidad de funciones, donde RuntimeReady condiciona xfa_disabled, los diecisiete huecos de la versión 2, FPDF_LoadXFA y m_pJsPlatform por documento y por host
Un campo de versión describe la memoria que la otra parte puede leer, las comprobaciones de capacidad deciden qué huecos hacen algo útil, y fundir ambos en un booleano rompe la build que impone un mínimo

La inicialización de form-fill corregida viene en PDFium Component para Delphi, Lazarus y C++Builder, y se aplica igual en Win32 y Win64, ya que ambas builds comparten la misma declaración de registro. Si tu aplicación ya selecciona pdfium.v8.dll para AcroForms dirigidos por JavaScript, este es el cambio que le permite abrir el resto de tu archivo PDF con el mismo binario sin casos especiales en el entorno de formulario