Artículo técnico

FPDF_FORMFILLINFO v2 en Delphi: siga el ABI de la DLL

PDFium Component ahora pone FPDF_FORMFILLINFO.version en 2 para cada entorno de form fill que inicializa, porque la versión que acepta una compilación nativa de PDFium es una propiedad de esa compilación, no del documento que se abre. Un pdfium.v8.dll con XFA rechaza la versión 1 de plano, así que un PDF AcroForm común abierto a través de él fallaba en FPDFDOC_InitFormFillEnvironment sin ninguna XFA a la vista. El fix de la v3.116.0 es pequeño, pero el error detrás de él es general y vale la pena nombrarlo: un campo de versión de protocolo describe el diseño de memoria que la otra parte espera, y nunca debe derivarse de si usted resulta necesitar o no las features que ese diseño conlleva

¿Por qué falla FPDFDOC_InitFormFillEnvironment con un PDF común y pdfium.v8.dll?

El entorno falla porque una compilación de PDFium con XFA valida el campo version antes de hacer cualquier otra cosa, y la lógica vieja del wrapper le pasaba 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, arrojado al abrir una factura o un formulario fiscal común que no tiene más que campos de texto AcroForm. El mismo archivo abre bien contra el pdfium.dll común. La misma DLL abre bien un documento XFA de verdad. Solo se rompe la combinación de la compilación V8 con un documento que no es XFA, que es exactamente la combinación en la que cae un host después de activar EnableV8Engine para tener JavaScript de AcroForm, o después de que la autoselección de LoadDocument ya haya comprometido el proceso a 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 compilación XFA, todo PDF común posterior pasa por la misma configuración de entorno contra el mismo binario. El host no hizo nada mal; el wrapper hizo la pregunta equivocada al llenar el registro. Si todavía está decidiendo qué binario distribuir, nuestra nota sobre desplegar la DLL de PDFium y diagnosticar fallos de carga cubre la selección entre el común y el V8, y este artículo asume que la compilación V8 ya está en el proceso

Diagrama de PDFium Component de las cuatro combinaciones de pdfium.dll común y pdfium.v8.dll con XFA frente a documentos AcroForm y XFA: un registro de versión 1 rompía solo la compilación V8 con un formulario común, EPdfError en FPDFDOC_InitFormFillEnvironment, mientras que el registro de versión 2 corregido abre las cuatro
Un condicional ataba la versión del ABI al documento, así que la elección del binario V8 a nivel de todo el proceso convertía cada PDF común 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 el header público fpdf_formfill.h ata los valores aceptables a cómo se compiló la librería y no al documento. En paráfrasis, 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 compilación sin el módulo XFA acepta 1 o 2, y con 2 también llamará a los callbacks experimentales adicionales. Una compilación con el módulo XFA exige 2, punto, y el header repite ese requisito dos veces como si esperara que la gente lo pasara por alto. En ninguna parte el contrato menciona el documento. La versión es una declaración sobre el registro que usted asignó: con un 2 usted está prometiendo que la memoria después de m_pJsPlatform existe y contiene punteros a función válidos o NULL

La región de la versión 2 es donde vive toda la maquinaria de XFA. Empieza con xfa_disabled, un FPDF_BOOL que el header 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 a función, de FFI_DisplayCaret a FFI_DoURIActionWithKeyboardModifier. Cada uno de ellos está documentado como requerido para XFA y, en caso contrario, como que debe ponerse en NULL. Esa frase es la clave de todo el fix. NULL no es un estado de error para esos espacios; es el estado documentado para un host que no está manejando XFA. Un registro que se limpió con FillChar y después se marcó como versión 2 cumple el contrato en una compilación sin XFA exactamente igual de bien que un registro de versión 1, y es el único registro que una compilación XFA va a 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 cada byte, y los espacios en NULL son el estado documentado para un host que no está manejando XFA
El registro Pascal es siempre el diseño completo de la versión 2, así que una compilación con XFA lo acepta y una compilación común simplemente nunca llama a los espacios experimentales que quedan en NULL

La selección vieja ataba el ABI al documento

El defecto era un solo condicional que aislado parecía razonable. TPdf.InitializeFormFill calcula un flag RuntimeReady a partir de tres hechos: que el documento reporte un tipo de formulario XFA a través de TPdf.XFA, que los helpers de cadena de XFA se resuelvan a través de XfaFeaturesAvailable, y que las exportaciones V8 se resuelvan a través de V8FeaturesAvailable. Antes de la v3.116.0 ese mismo flag también elegía 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 fijaba otra vez
else if XFA then
begin
  FFormFillInfo.Info.version := 1;
  FFormFillInfo.Info.xfa_disabled := 1;
  if Assigned(FOnXfaRuntimeMissing) then
    FOnXfaRuntimeMissing(Self);
end;

Léalo con el header a mano y el fallo es obvio. RuntimeReady es false para todo documento AcroForm común, así que cada documento común anunciaba la versión 1. Con pdfium.dll eso está bien. Con pdfium.v8.dll, que es la compilación con XFA, PDFium revisa el campo, lo encuentra por debajo del 2 requerido y devuelve un FPDF_FORMHANDLE nulo, que CheckPdf convierte en la excepción de arriba. La intención del código viejo era defensiva: mantener la versión 1 para que una compilación XFA nunca leyera los espacios sin asignar de la versión 2. Se defendía de un problema que el header ya descarta y creaba uno que el header advierte de forma explícita. El código corregido decide la versión una sola vez, por adelantado, a partir de lo que el registro es físicamente

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

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

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

  // RuntimeReady limita 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 conectan sin condición 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 en 1 y los espacios de la versión 2 en NULL, y el wrapper dispara OnXfaRuntimeMissing para que el host pueda sugerir reiniciar con pdfium.v8.dll. Una vez que el entorno existe, solo se llama a FPDF_LoadXFA cuando RuntimeReady era true, y solo un retorno true pone FXfaRuntimeUsable, que es lo que reporta 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: mantener la versión 2, dejar XFA deshabilitado y avisar 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 hacer mal cuando usted escribe su propio binding. FXfaPageCountOverride se reinicia a -1 como centinela antes de que pase cualquier otra cosa, así que PageCount cae al árbol de páginas estático hasta que FFI_PageEvent reporte 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 cdecl estática que recupera el TPdf propietario del 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 la 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 compilación sin XFA el header 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 con FillChar antes de tocar un byte. Así que con un pdfium.dll común y un documento común la librería ve la versión 2, xfa_disabled puesto y NULL en cada espacio experimental, que es precisamente el estado que el header prescribe para un host que no implementa XFA. No hay ningún registro truncado 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 vieja defendía un desajuste de diseño que la declaración Pascal ya había eliminado

El límite que vale la pena declarar con honestidad es el que el registro no puede cubrir. La versión 2 en un documento común no enciende JavaScript, ni el scripting XFA, ni ninguno de los eventos de host detrás de esos callbacks. m_pJsPlatform se conecta solo cuando V8FeaturesAvailable es true, XFA sigue deshabilitado salvo que RuntimeReady fuera true, y TPdf.XFA sigue reportando el tipo de formulario de FPDF_GetFormType sin importar lo que haya negociado el entorno. Un host que quiera saber si la XFA dinámica va a renderizar 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 inferir algo del campo de versión

procedure TMainForm.PdfXfaRuntimeMissing(Sender: TObject);
begin
  // Se dispara desde InitializeFormFill cuando el documento es XFA pero el pdfium.dll
  // cargado no puede correr el motor. El entorno de formulario igual abre,
  // porque la versión 2 se pasó de cualquier forma; solo el runtime XFA está apagado.
  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 común bajo pdfium.v8.dll
  if Pdf.XFA and not Pdf.XfaRuntimeAvailable then
    ShowStaticXfaWarning;
end;

La versión de protocolo y la disponibilidad de features son dos ejes distintos

La regla general que sale de este fix es que un campo de versión en una estructura de callbacks responde a la pregunta "qué tamaño tiene este registro y qué puede leer de él", mientras que la detección de features responde a "cuáles de esos espacios harán algo útil". Lo primero lo fija el binario nativo y la declaración Pascal contra la que usted compiló. Lo segundo varía por documento, por tabla de exportaciones de la DLL y por configuración del host. Colapsar los dos en un solo booleano es tentador porque el caso XFA resulta necesitar ambos, pero en el momento en que una compilación exige una versión mínima, el colapso se rompe para todo documento que no necesite la feature. Los formularios XFA, descritos en ISO 32000-1 §12.7.8 como un payload XML que vive junto al diccionario AcroForm, son aquí la feature; el diseño del registro es el protocolo, y PDFium tiene derecho a exigir el diseño antes de mirar siquiera el archivo. La misma forma aparece donde sea que una librería C versione 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. Declare el diseño más nuevo que entienda, límpielo por completo, ponga la versión que corresponda a ese diseño sin condición, y después deje que las comprobaciones de capacidad decidan qué espacios poblar. Si un header futuro de PDFium añade una versión 3, el cambio va en la declaración y en esa única asignación, no en una rama dependiente del documento que quedará mal para la combinación que nadie probó

Diagrama de PDFium Component que separa los dos ejes 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 features, donde RuntimeReady limita xfa_disabled, los diecisiete espacios 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é espacios hacen algo útil, y colapsar los dos en un solo booleano rompe la compilación que exige un mínimo

La inicialización de form fill corregida viene en el PDFium Component para Delphi, Lazarus y C++Builder, y aplica igual en Win32 y Win64, ya que ambas compilaciones comparten la misma declaración de registro. Si su aplicación ya selecciona pdfium.v8.dll para AcroForms manejados por JavaScript, este es el cambio que le permite abrir el resto de su archivo de PDFs a través del mismo binario sin tratar el entorno de formulario como caso especial