Artículo técnico

Ejecutar JavaScript de AcroForm en Delphi con PDFium Component

Un formulario cuyos totales se recalculan en Acrobat pero permanecen estáticos en su propio visor de Delphi casi nunca representa un error de renderizado. PDFium deshabilita todo el JavaScript del documento a menos que el host asocie una plataforma de JS, y PDFium Component conecta esa plataforma automáticamente a partir de la versión 2.13.2, por lo que el JavaScript de AcroForm, los scripts a nivel de documento y los cálculos de campos se ejecutan siempre que se cargue la DLL habilitada para V8. El host mantiene el control a través de un conjunto de eventos que pueden mostrar, redireccionar o vetar todo lo que el script intente realizar

Esa única frase resuelve una pregunta de soporte que hemos observado de diversas maneras: "mi formulario de pedido calcula los totales de línea en Acrobat pero no en mi aplicación", "app.alert nunca se activa", "el campo de fecha no tiene formato". Todas ellas se remontan al mismo acuerdo, y comprenderlo merece diez minutos debido a que representa además su límite de seguridad contra documentos hostiles

¿Por qué los cálculos de formularios PDF no funcionan en PDFium?

PDFium trata el JavaScript del documento de forma estrictamente opcional: si el miembro m_pJsPlatform del entorno de llenado de formularios es NULL, cada script en el documento se omite silenciosamente. Sin errores, sin líneas de registro, sin ejecución parcial. Los campos calculados conservan su último valor almacenado, app.alert desaparece y los scripts de formato nunca se ejecutan. Acrobat, que siempre incluye su propio motor de JavaScript, ejecuta el mismo archivo sin problemas, razón por la cual la discrepancia parece un error en su código cuando en realidad representa una opción predeterminada y deliberada en la biblioteca subyacente

El componente PDFium Component tenía una segunda capa histórica para esto. Hasta la versión 2.13.1, el enlace solo asociaba m_pJsPlatform dentro de la rama de inicialización de XFA, por lo que los documentos de AcroForm simples, que son la gran mayoría de los PDF con scripts, nunca obtenían una plataforma de JavaScript, incluso cuando la DLL habilitada para V8 estaba presente. La versión 2.13.2 retiró la asociación de la decisión de XFA: InitializeFormFill ahora construye la tabla IPDF_JsPlatform siempre que V8FeaturesAvailable devuelva True, independientemente de si el documento es XFA o no. El resultado práctico es que los scripts a nivel de documento, las cadenas de cálculo de campos y app.alert ahora se ejecutan también en documentos ordinarios de AcroForm

¿Qué DLL de V8 necesita el JavaScript de AcroForm en Delphi?

El JavaScript de AcroForm necesita un binario de PDFium compilado con el motor V8, que PDFium Component carga como pdfium.v8.dll cuando la bandera global EnableV8Engine es True antes de que la biblioteca se cargue por primera vez. Hay un problema aquí que vale la pena explicar claramente: el componente detecta automáticamente los documentos XFA preescaneando marcadores /XFA y activa EnableV8Engine por usted, pero un documento AcroForm simple con JavaScript no contiene ningún marcador XFA, por lo que no se realiza la autodetección. Si su visor debe ejecutar scripts de formulario, configure la bandera usted mismo, una vez, antes de que se cargue el primer documento; una vez cargado un archivo pdfium.dll simple, el proceso no puede cambiar de motor

uses PDFium;

procedure TViewerForm.FormCreate(Sender: TObject);
begin
  // Must run before the singleton PDFium library first loads;
  // once plain pdfium.dll is in the process it cannot be swapped
  EnableV8Engine := True;
end;

procedure TViewerForm.OpenDocument(const FileName: string);
begin
  FPdf.LoadDocument(FileName);
  if not V8FeaturesAvailable then
    StatusBar.SimpleText :=
      'JavaScript disabled: pdfium.v8.dll not deployed';
end;

V8FeaturesAvailable es la prueba de tiempo de ejecución real: informa si el binario cargado realmente resuelve las exportaciones dependientes de V8, por lo que la comprobación funciona sin importar qué DLL haya implementado un instalador. El mismo motor y la misma bandera también alimentan el renderizado dinámico de XFA; la información sobre cómo interactúan la detección de XFA y la selección automática de V8 se analiza en la detección de formularios XFA y la extracción de paquetes XFA con PDFium

Eventos del host para alertas, impresión, correo y envío de formularios

Cada acción visible que realiza un script se redirige de vuelta a su código a través de los eventos de TPdf, y cada uno de ellos se define de forma predeterminada como una operación vacía segura cuando no está asignado. OnJavaScriptAlert recibe el mensaje, título, tipo de botón e icono de app.alert y le permite devolver el resultado del botón presionado; OnJavaScriptResponse gestiona las solicitudes de entrada de app.response, incluyendo la bandera de contraseña; OnJavaScriptBeep, OnJavaScriptPrint, OnJavaScriptMail y OnJavaScriptSubmitForm exponen las solicitudes de pitido, impresión, correo y envío de formularios con sus conjuntos completos de parámetros. Un visor que no asigne ninguno de estos eventos seguirá renderizando y calculando correctamente; el documento simplemente no podrá abrir diálogos, imprimir ni enviar nada

procedure TViewerForm.PdfJavaScriptAlert(Sender: TObject;
  const Msg, Title: WString; ButtonType, Icon: Integer;
  var Result_: Integer; var Handled: Boolean);
begin
  // Route app.alert through the VCL so it is owned by our window
  Result_ := MessageBox(Handle, PWideChar(Msg), PWideChar(Title),
    MB_OK or MB_ICONINFORMATION);
  Handled := True;
end;

procedure TViewerForm.PdfJavaScriptSubmitForm(Sender: TObject;
  const Url: WString; const Data: TBytes);
begin
  // Nothing is transmitted unless this handler chooses to send it
  LogAudit(Format('Document requested form submit to %s (%d bytes)',
    [Url, Length(Data)]));
end;

La división del comportamiento predeterminado es importante para el modelado de amenazas. OnJavaScriptMail y OnJavaScriptSubmitForm son de estilo notificación: PDFium Component no realiza ninguna acción de red ni de MAPI por sí mismo, por lo que un documento malicioso que llame a this.submitForm o this.mailDoc en un host no preparado no logrará absolutamente nada. El host debe optar por participar asignando un controlador e implementando el transporte por sí mismo, lo que significa que la decisión de mover datos fuera de la máquina es siempre suya, tomada en su código, con la URL y el contenido en la mano

¿Cómo evitar que un PDF malicioso abra URL?

Las acciones de URI representan el único lugar donde el comportamiento predeterminado histórico era activo en lugar de pasivo, y por eso obtuvieron un veto dedicado. Antes de la versión 2.13.1, la devolución de llamada FFI_DoURIActionWithKeyboardModifier ejecutaba en el sistema (shell-executed) cualquier URI que proporcionara un campo XFA, de forma incondicional, lo que le entregaba a un documento malicioso la capacidad de abrir URL arbitrarias con un clic. OnXfaUriAction soluciona esto: el componente consulta el evento antes de la ejecución, y un controlador que defina Handled en True suprime la acción predeterminada de ShellExecuteW por completo. Dejar el evento sin asignar conserva el comportamiento anterior por compatibilidad, por lo que un visor preocupado por la seguridad siempre debería asignarlo

procedure TViewerForm.PdfXfaUriAction(Sender: TObject;
  const Uri: WString; Modifiers: Integer; var Handled: Boolean);
begin
  // Veto anything that is not plain https; the default would
  // otherwise ShellExecuteW the URI as-is
  if not SameText(Copy(Uri, 1, 8), 'https://') then
  begin
    LogAudit('Blocked URI action: ' + Uri);
    Handled := True;
  end;
end;

Una lista de permitidos en el esquema es lo mínimo; un visor más estricto confirma con el usuario o redirige todo a través de su propio componente de navegador aislado (sandboxed). La misma filosofía de veto se extiende a todo el conjunto de eventos de la versión v2.13.1: OnXfaEmail, OnXfaHttpRequest y OnXfaOpenFile exponen cada uno los parámetros de texto de la acción solicitada mientras el componente en sí no realiza ningún transporte de red o de archivos. Cómo encajan estas protecciones en una estrategia más amplia para documentos no confiables, incluyendo el aislamiento de renderizado, es el tema de la construcción de una vista previa segura de PDF en Delphi

Escribir en el campo enfocado con SetFocusedFormFieldText

TPdf.SetFocusedFormFieldText, añadido en la versión 2.13.2, es el compañero de escritura de FocusedFormFieldText: selecciona el contenido actual del campo enfocado mediante FORM_SelectAllText y lo sobrescribe a través de FORM_ReplaceSelection, exactamente como si el usuario hubiera escrito el reemplazo. Debido a que el texto ingresa a través de la ruta de edición interactiva, cualquier script de pulsación de tecla, formato y cálculo asociado al campo se activa de la misma manera que para la entrada manual, lo que lo convierte en el método correcto para el llenado programático en un visor que mantenga JavaScript activo. El recorrido de los campos y la gestión del enfoque a su alrededor se analizan en la navegación por campos de formulario con PDFium Component

if FPdf.FocusedFormFieldIndex >= 0 then
begin
  if not FPdf.SetFocusedFormFieldText('42.50') then
    ShowMessage('No focused field accepts text on this page');
  // AcroForm: value commits to /V when focus leaves the field.
  // XFA: the write stays in the in-memory edit buffer only and
  // will not survive a save
end;

La asimetría de persistencia en ese comentario es un límite real, no una advertencia superficial. Para los campos de texto y de lista desplegable (combo) de AcroForm, el valor editado se envía a la entrada /V del campo al perder el enfoque y persiste al guardar. Para los campos de texto de XFA, la escritura se realiza únicamente en el búfer de edición en memoria, debido a que PDFium no expone ninguna API pública para conciliar los valores de los widgets de vuelta al paquete de conjuntos de datos (datasets) de XFA; guardar el documento no conservará el cambio. Si el requisito es la edición duradera de datos XFA, edite el XML de datasets propiamente dicho en lugar del widget

Límites que vale la pena conocer antes del lanzamiento

Quedan dos límites claros. En primer lugar, la API de JavaScript que implementa PDFium es un subconjunto funcional del modelo de objetos de Acrobat, centrado en el núcleo de la creación de scripts de formulario: acceso a campos, eventos de cálculo y formato, app.alert y app.response, impresión, correo y solicitudes de envío. Los documentos que dependen de objetos exóticos exclusivos de Acrobat se degradarán, normalmente de forma silenciosa, así que pruebe los formularios reales que manejan sus usuarios en lugar de asumir paridad. En segundo lugar, habilitar V8 significa distribuir pdfium.v8.dll, un binario sustancialmente más grande que la compilación básica; un visor que nunca necesite scripting o XFA puede conservar legítimamente la DLL más pequeña y dejar m_pJsPlatform sin asociar, lo cual representa una postura de seguridad válida en sí misma

El patrón resultante de todo esto es gratamente predecible: configure EnableV8Engine al inicio, asigne los eventos de alerta y respuesta para que los diálogos se observen nativos, asigne OnXfaUriAction y registre en la auditoría los eventos de correo y envío, y deje todo lo demás en su comportamiento predeterminado sin operación hasta que un requisito indique lo contrario. La referencia completa de eventos y la superficie de la API de llenado de formularios están documentadas en la página del producto PDFium Component