Un campo de formulario PDF por sí solo es solo una casilla que contiene un valor. Lo que hace que un formulario se comporte como una pequeña aplicación es la acción asociada a él: un clic que oculta una sección, recupera valores guardados de un archivo, salta a la última página o ejecuta un script que suma una columna. Nada de eso reside en el campo. Reside en un diccionario de acciones, e ISO 32000-1 organiza toda la familia en el §12.6. Este artículo recorre las acciones a las que un programa en Delphi recurre con más frecuencia y muestra cómo PDFlibPas conecta cada una a un campo o a un enlace.
El modelo mental que vale la pena conservar 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 al campo sobre el que opera por su título, no por su índice, por lo que el título que le dé a un campo es el identificador que toda acción posterior utilizará para encontrarlo. Una vez que esta división está clara, la API deja de parecer una bolsa de sorpresas llena de llamadas y comienza a parecerse a un único patrón aplicado a cuatro tipos de verbos.
Acciones con nombre: navegación sin número de página
Las acciones más simples no llevan ningún parámetro en absoluto. La norma ISO 32000-1 §12.6.4.11, Tabla 194, define las acciones con nombre: el visor interpreta un nombre simbólico en tiempo de ejecución en lugar de seguir un destino almacenado. Hay cuatro nombres universalmente compatibles, 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 todas las páginas sin necesidad de calcular un objetivo.
En PDFlibPas, una acción con nombre se adjunta a un rectángulo de punto activo en la página actual. El cuarto y quinto argumento entero seleccionan el verbo y la apariencia.
// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1); // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1); // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1); // jump to last page
No hay ningún destino que mantener sincronizado, que es de lo que se trata. Una acción con nombre sobrevive a la inserción y eliminación de páginas porque en primer lugar nunca nombra una página. Confronte esto con un enlace explícito de ir a, que almacena un índice de página de destino que debe volver a numerar en el momento en que el documento crece.
La acción Hide y su trampa con las matrices
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 necesidad de scripts, y es lo que se 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 booleano /H que decide la dirección: ocultar cuando es verdadero, mostrar cuando es falso.
La sutileza radica completamente en cómo se codifica ese objetivo, y es el tipo de detalle que produce un formulario que funciona en su máquina pero falla en la del cliente. Cuando la acción nombra a un solo campo, /T se escribe como una cadena de texto. Cuando nombra a varios, /T se escribe como una matriz de cadenas de texto. Los visores más antiguos no tratan una matriz de un solo elemento de la misma manera que tratan una cadena simple, por lo que la codificación debe bifurcarse según el recuento: un único nombre debe emitirse como una cadena, no como una matriz de longitud uno, si se quiere que la más amplia gama de lectores lo respete. PDFlibPas toma esa decisión por usted. Pasa los nombres de los campos separados por comas, puntos y comas o saltos de línea, y el escritor emite una sola cadena para un nombre y una matriz para dos o más.
// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
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, sigue siendo compatible con PDF/A. Los nombres que pasa son títulos de campos completamente calificados, por lo que un campo secundario dentro de un grupo debe direccionarse a través de su ruta completa separada por puntos en lugar de solo su nombre de hoja básico.
ImportData: prellenado desde FDF
Mientras que la acción Hide reorganiza lo que ya está en la página, la acción de 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 rellena el AcroForm desde un archivo Forms Data Format en disco. Esta es la acción detrás de un control Recargar datos de muestra o Restablecer valores predeterminados, donde un archivo FDF se envía junto al PDF y contiene los valores de campo canónicos. La llamada refleja las demás, tomando el rectángulo del punto activo, la ruta al FDF y una máscara de bits de apariencia: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). No es necesario que el archivo exista cuando se compila 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 de PDF por usted.
Vale la pena exponer claramente una restricción porque suele ser una sorpresa frecuente. Una acción de importar datos 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 añade nada en lugar de producir un archivo que falle la validación. Si su flujo de trabajo tiene como objetivo la salida de archivo, el prellenado debe realizarse en el momento de la generación escribiendo los valores de los campos directamente, no posponiéndolos a un clic.
JavaScript: paquetes globales y scripts por acción
Para una lógica que va más allá de mostrar, ocultar e importar, la familia de acciones recurre al JavaScript a nivel de documento. Hay dos lugares distintos donde puede residir un script, y la diferencia importa. Un paquete de JavaScript a nivel de documento se almacena una vez para todo el archivo y se ejecuta cuando se abre el documento, lo que lo convierte en el lugar adecuado para definiciones de funciones y estados compartidos. Un script por acción se adjunta a un enlace o campo y se ejecuta solo cuando se activa ese objeto, lo que lo convierte en el lugar adecuado para la única línea que llama a una función que el paquete ya definió.
PDFlibPas expone ambos. AddGlobalJavaScript almacena un paquete con nombre a nivel de documento; reutilizar un nombre reemplaza lo que estuviera almacenado bajo él. AddLinkToJavaScript adjunta un script a un punto activo para que un clic lo ejecute.
// Document-level package: define a reusable function once.
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);' +
'}');
// Per-action script on a link: just call the shared function.
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 necesite, y significa que un visor con scripts deshabilitados simplemente no hará nada al hacer clic en lugar de ahogarse con un bloque incrustado malformado. También mantiene pequeñas las entradas por acción, lo que mantiene el archivo legible cuando lo inspeccione más tarde.
Campos, campos secundarios y congelación del resultado
Las acciones necesitan campos sobre los cuales actuar, por lo que ayuda ver cómo nace un campo. NewFormField crea un campo en la página actual y devuelve su índice; el tipo entero selecciona el tipo, donde 1 es Text, 2 is Pushbutton, 3 es Checkbox, 4 es Radiobutton, 5 es Choice, 6 es Signature y 7 es un Parent que posee elementos secundarios pero no dibuja nada por sí mismo. El título que pase no puede contener un punto, porque el punto es el separador en los nombres completamente calificados que las acciones usan para dirigirse a los campos secundarios.
Los grupos de opciones y los formularios jerárquicos se construyen dando campos secundarios a un campo principal. NewChildFormField añade un elemento secundario bajo un principal con nombre, y para los casos de opciones y selección, AddFormFieldSub añade las opciones individuales y devuelve un índice temporal que se utiliza para posicionar cada una. Cuando termina la fase interactiva y desea congelar un campo para que su apariencia actual se convierta en contenido permanente de la página, FlattenFormField dibuja el campo sobre la página y lo elimina del formulario. Después de un aplanamiento, los índices de los campos posteriores se desplazan hacia abajo en uno, que es lo único que se debe recordar si se aplanan varios campos en un bucle.
var
Pdf: TPDFlib;
FldShip: Integer;
begin
Pdf := TPDFlib.Create;
try
Pdf.SetOrigin(1); // top-left origin
Pdf.SetPageSize('A4');
Pdf.NewPage;
// A text field the Hide action will target by its title.
FldShip := Pdf.NewFormField('ShippingAddress', 1);
Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
Pdf.SetFormFieldValue(FldShip, '');
// Wire a Hide link and a navigation link to this page.
Pdf.DrawText(40, 110, 'Toggle shipping block:');
Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1); // Last page
// A document-level script available to every event in the file.
Pdf.AddGlobalJavaScript('OnOpen',
'app.alert("Form ready", 3);');
// Freeze the field if the output should no longer be 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 aplanado está comentada a propósito. Déjela fuera y el documento se enviará como un formulario activo cuyas acciones se disparan en el lector. Habilítela y el campo se representará como marcas estáticas, que es lo que se desea cuando el formulario se ha completado y el resultado debe viajar como un registro fijo. El mismo campo, el mismo código, dos documentos muy diferentes dependiendo de si lo congela.
Elegir el verbo correcto
Las cuatro acciones se dividen claramente por lo que tocan. Una acción con nombre mueve la vista y no necesita ningún campo. Una acción Hide cambia la visibilidad y necesita títulos de campo, con la codificación de cadena versus matriz manejada por usted. Una acción de importar datos accede a un archivo en el disco y, por lo tanto, está fuera de los límites en PDF/A. Una acción de JavaScript ejecuta una lógica arbitraria y se divide mejor entre un paquete global de funciones y pequeñas llamadas por acción. Utilice la más simple que haga el trabajo: una acción Hide es más portátil que un script que establece una bandera oculta, y una acción con nombre es más duradera que un destino de página almacenado porque no hay ningún número que mantener.
A partir de aquí, dos temas relacionados completan el panorama. Si el formulario es parte de un documento accesible, el árbol de estructura que recorren los lectores de pantalla se cubre en nuestro artículo sobre PDF etiquetado y estructura de accesibilidad. Cuando el formulario completado debe bloquearse y firmarse, el flujo de trabajo se describe en el tutorial sobre el entorno de trabajo de cumplimiento y firma. Los tres se basan en el mismo motor, que se distribuye como la librería PDF para Delphi junto con las API de creación, formulario y firma cubiertas en otras partes de este blog.