Artículo técnico

Formularios PDF interactivos en Delphi: Acciones y JavaScript

Un campo de formulario PDF por sí solo es solo un cuadro que contiene un valor. Lo que hace que un formulario se comporte como una pequeña aplicación es la acción que se le adjunta: un clic que oculta una sección, extrae valores guardados de un archivo, salta a la última página o ejecuta un script que totaliza una columna. Nada de eso vive en el campo. Vive en un diccionario de acciones, y la norma ISO 32000-1 organiza a toda la familia en §12.6. Este artículo recorre las acciones a las que un programa de Delphi recurre más a menudo y muestra cómo PDFlibPas vincula cada una a un campo o a un enlace

El modelo mental que vale la pena mantener es que un campo y una acción son objetos separados unidos por una referencia. Una anotación de widget o una anotación de enlace lleva una acción en su entrada /A. La acción nombra el campo en el que opera por título, no por índice, por lo que el título que le da a un campo es el identificador (handle) que cada acción posterior usa para encontrarlo. Una vez que esa división está clara, la API deja de parecer una mezcla de llamadas y comienza a parecerse a un patrón aplicado a cuatro tipos de verbos

Acciones nombradas: navegación sin un número de página

Las acciones más simples no llevan ningún parámetro. La norma ISO 32000-1 §12.6.4.11, Tabla 194, define las acciones nombradas: el visor interpreta un nombre simbólico en tiempo de ejecución en lugar de seguir un destino almacenado. Cuatro nombres son soportados universalmente, y son exactamente los que un lector espera de una barra de herramientas: NextPage, PrevPage, FirstPage y LastPage. Debido a que el destino es relativo a cualquier página que el visor esté mostrando actualmente, un botón Siguiente construido de esta manera funciona en cada página sin que usted calcule un objetivo

En PDFlibPas, una acción nombrada se adjunta a un rectángulo activo (hotspot) en la página actual. El cuarto y quinto argumentos enteros seleccionan el verbo y la apariencia

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// El bit 0 de Opciones (valor 1) dibuja un borde alrededor del rectángulo activo (hotspot)
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1);   // Siguiente
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1);    // Anterior
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1);   // saltar a la última página

No hay ningún destino que mantener sincronizado, que es precisamente la idea. Una acción nombrada sobrevive a la inserción y eliminación de páginas porque nunca nombra una página en primer lugar. Contraste eso con un enlace ir-a (go-to) explícito, que almacena un índice de página de destino que tiene que volver a numerar en el momento en que el documento crece

La acción Hide (Ocultar) y el detalle problemático de su arreglo (array)

La acción Hide, ISO 32000-1 §12.6.4.10, Tabla 196, alterna la visibilidad de uno o más campos. Es la forma más limpia de construir un comportamiento de mostrar y ocultar sin usar secuencias de comandos (scripting), y es lo que desea para un enlace de Mostrar detalles o para dos paneles mutuamente excluyentes donde revelar uno oculta el otro. La acción lleva un objetivo en su entrada /T y un valor booleano /H que decide la dirección: ocultar cuando es verdadero, mostrar cuando es falso

La sutileza está completamente en cómo se codifica ese objetivo, y es el tipo de detalle que produce un formulario que funciona en su máquina y falla en la de un cliente. Cuando la acción nombra un solo campo, /T se escribe como una cadena de texto. Cuando nombra varios, /T se escribe como un arreglo (array) de cadenas de texto. Los visores más antiguos no tratan un arreglo de un elemento de la misma manera que tratan una cadena desnuda, por lo que la codificación tiene que bifurcarse en el conteo: un solo nombre debe emitirse como una cadena, no como un arreglo de longitud uno, si se desea que la gama más amplia de lectores lo respete. PDFlibPas toma esa decisión por usted. Usted pasa nombres de campo separados por comas, puntos y comas o saltos de línea, y el escritor emite una sola cadena para un nombre y un arreglo para dos o más

// HideFlag distinto de cero oculta los campos enumerados (/H true); cero los muestra.
// Un nombre -> /T es una cadena de texto. Dos o más -> /T es un arreglo de cadenas.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

Debido a que la acción no hace referencia a ningún recurso externo, se mantiene compatible con PDF/A. Los nombres que pasa son títulos de campos totalmente calificados, por lo que un campo secundario (child) dentro de un grupo tiene que ser abordado a través de su ruta completa con puntos en lugar de su nombre de hoja desnudo

ImportData: rellenando previamente desde FDF

Donde la acción Hide reorganiza lo que ya está en la página, la acción import-data (importar datos) trae valores desde fuera de ella. La norma ISO 32000-1 §12.6.4.8, Tabla 198, la define como una acción que completa el AcroForm desde un archivo de Formato de Datos de Formularios (FDF) en el disco. Esta es la acción detrás de un control de Recargar datos de muestra o Restablecer a valores predeterminados, donde un archivo FDF se envía junto al PDF y contiene los valores canónicos de los campos. La llamada refleja a las demás, tomando el rectángulo activo (hotspot), la ruta al FDF y una máscara de bits de apariencia: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). El archivo no necesita existir cuando se construye el PDF, pero debe estar presente cuando el usuario hace clic, y cualquier barra invertida en la ruta se reescribe a la forma de barra canónica del PDF por usted

Vale la pena indicar claramente una limitación porque es una sorpresa frecuente. Una acción de import-data apunta a un archivo externo, por lo que no está permitida en PDF/A. Cuando el documento está en modo PDF/A, la llamada devuelve cero y no agrega nada en lugar de producir un archivo que falla en la validación. Si su proceso está destinado a la salida de archivo, el pre-rellenado tiene que ocurrir en el tiempo de generación escribiendo los valores de campo directamente, no aplazándolos a un clic

JavaScript: paquetes globales y scripts por acción

Para la lógica que va más allá de mostrar, ocultar e importar, la familia de acciones llega hasta el JavaScript a nivel de documento. Hay dos lugares distintos donde un script puede vivir, y la diferencia importa. Un paquete de JavaScript a nivel de documento se almacena una vez para todo el archivo y se ejecuta cuando el documento se abre, lo que lo convierte en el lugar correcto para las definiciones de funciones y el estado compartido. Un script por acción se adjunta a un enlace o campo y se ejecuta solo cuando ese objeto se activa, lo que lo convierte en el lugar correcto para la única línea que llama a una función que el paquete ya definió

PDFlibPas expone ambos. AddGlobalJavaScript almacena un paquete nombrado a nivel de documento; reutilizar un nombre reemplaza cualquier cosa que estuviera almacenada bajo él. AddLinkToJavaScript adjunta un script a un rectángulo activo (hotspot) para que un clic lo ejecute

// Paquete a nivel de documento: definir una función reutilizable una vez.
Pdf.AddGlobalJavaScript('Totals',
  'function recalcTotal() {' +
  '  var net = this.getField("Net").value;' +
  '  var tax = this.getField("Tax").value;' +
  '  this.getField("Gross").value = Number(net) + Number(tax);' +
  '}');

// Script por acción en un enlace: solo llame a la función compartida.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

Mantener la función en el paquete global y la llamada en el enlace no es una preferencia de estilo. Evita duplicar el mismo cuerpo en cada control que lo necesita, y significa que un visor con los scripts deshabilitados simplemente no hace nada al hacer clic en lugar de ahogarse con un bloque en línea (inline blob) mal formado. También mantiene pequeñas las entradas por acción, lo que mantiene el archivo legible cuando lo inspecciona más tarde

Campos, campos secundarios (children) y congelación del resultado

Las acciones necesitan campos sobre los que actuar, por lo que ayuda ver cómo surge un campo. NewFormField crea un campo en la página actual y devuelve su índice; el tipo entero selecciona la clase, donde 1 es Text (Texto), 2 es Pushbutton (Botón de presión), 3 es Checkbox (Casilla de verificación), 4 es Radiobutton (Botón de opción), 5 es Choice (Elección), 6 es Signature (Firma), y 7 es un Parent (Padre) que posee elementos secundarios (children) pero no dibuja nada en sí mismo. El título que pase no puede contener un punto, porque el punto es el separador en los nombres totalmente calificados que las acciones usan para dirigirse a los elementos secundarios (children)

Los grupos de opciones (radio) y los formularios jerárquicos se construyen dándole campos secundarios a un campo padre. NewChildFormField agrega un campo secundario bajo un padre nombrado, y para los casos de opción (radio) y elección (choice), AddFormFieldSub agrega las opciones individuales y devuelve un índice temporal que usted usa para posicionar cada una. Cuando la fase interactiva ha terminado y desea congelar un campo para que su apariencia actual se convierta en contenido de página permanente, FlattenFormField dibuja el campo en la página y lo elimina del formulario. Después de un acoplamiento (flatten), los índices de los campos posteriores se desplazan hacia abajo en uno, que es lo único que debe recordar si acopla varios campos en un bucle

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // origen arriba a la izquierda
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // Un campo de texto al que apuntará la acción Hide por su título.
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Conectar un enlace Hide y un enlace de navegación a esta página.
    Pdf.DrawText(40, 110, 'Toggle shipping block:');
    Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
    Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1);  // Última página

    // Un script a nivel de documento disponible para cada evento en el archivo.
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Congele el campo si la salida ya no debería ser editable.
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

La llamada de flatten (acoplamiento) está comentada a propósito. Déjela fuera y el documento se enviará como un formulario en vivo cuyas acciones se disparan en el lector. Habilítela y el campo se renderiza en marcas estáticas, que es lo que desea cuando se ha completado el formulario y el resultado debería viajar como un registro fijo. El mismo campo, el mismo código, dos documentos muy diferentes dependiendo de si lo congela

Eligiendo el verbo correcto

Las cuatro acciones se dividen limpiamente por lo que tocan. Una acción nombrada mueve el viewport y no necesita campo. Una acción Hide (Ocultar) cambia la visibilidad y necesita títulos de campos, con la codificación de cadena frente a arreglo (string-versus-array) gestionada por usted. Una acción import-data alcanza un archivo en el disco y por lo tanto está fuera de los límites en PDF/A. Una acción de JavaScript ejecuta lógica arbitraria y se divide mejor entre un paquete global de funciones y pequeñas llamadas por acción. Utilice el más simple que haga el trabajo: una acción Hide es más portátil que un script que establece una bandera de oculto, y una acción nombrada es más duradera que un destino de página almacenado porque no hay un número que mantener

A partir de aquí, dos temas vecinos completan el cuadro. Si el formulario es parte de un documento accesible, el árbol de estructura que los lectores de pantalla recorren está cubierto en nuestro artículo sobre PDF etiquetado y estructura de accesibilidad. Cuando el formulario completado tiene que ser bloqueado y firmado, el flujo de trabajo se describe en el recorrido del entorno de trabajo de cumplimiento y firma. Los tres se basan en el mismo motor, que se envía como la Biblioteca PDF para Delphi junto con las APIs de creación, formularios y firmas cubiertas en otros lugares de este blog