PDFlibPas, la biblioteca PDF nativa para Delphi y C++Builder, da a un documento PDF dos lugares independientes donde colgar comportamiento automático: acciones de ciclo de vida a nivel de documento como WillClose, WillSave, DidSave, WillPrint y DidPrint, almacenadas en el diccionario /AA del Catalog, y acciones de ciclo de vida a nivel de página, Open y Close, almacenadas en cambio en el propio diccionario /AA de cada objeto Page. Confundir los dos contenedores es la forma más común, y única, con diferencia, en que una acción de ciclo de vida no hace nada en silencio
Los casos que motivan esto son ordinarios. Un equipo de finanzas quiere una plantilla de extracto que estampe una marca de tiempo de impresión y registre quién lo imprimió en el momento en que la impresión realmente empieza, no cuando el archivo simplemente se abre. Un flujo de trabajo cargado de formularios necesita que los valores de campo se envíen automáticamente a un servidor antes de que se permita al cliente PDF del lector cerrar la ventana, para que una pestaña cerrada nunca signifique una edición perdida. Un informe de varias páginas quiere un banner específico de página que aparezca solo mientras esa página está en pantalla. PDF en realidad ofrece un tercer nivel por debajo de documento y página para este tipo de comportamiento, acciones adjuntas a la propia entrada /A de un campo de formulario o enlace individual, el tema de un artículo complementario sobre acciones de formulario interactivas y JavaScript, pero este artículo se queda en los dos niveles por encima: el documento entero, y una única página
¿Qué disparadores viven en el /AA del Catalog del documento?
Cinco disparadores viven en el diccionario Catalog /AA, y cada uno de ellos se dispara para un evento que afecta a todo el documento, no a una sola página. ISO 32000-1 §12.6.3 (Trigger Events) enumera las claves a nivel de documento como WC, WS, DS, WP y DP, los nombres literales de dos letras escritos en el diccionario /AA, para WillClose, WillSave, DidSave, WillPrint y DidPrint respectivamente, y PDFlibPas refleja ese conjunto exactamente en la enumeración TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction es el único punto de entrada que adjunta cualquiera de las cinco, y el parámetro ActionKind que recibe es una de las diez constantes PDF_ACTION_BUILDER_* compartidas en cada llamada de constructor de acción de la biblioteca, desde un simple URI hasta un script o un salto a destino. Qué hace realmente una acción GoTo, de archivo remoto, embebido o Launch una vez disparada es una pregunta distinta de dónde se adjunta, y ese es el tema de un artículo complementario sobre acciones GoTo, remotas, embebidas y de lanzamiento, este se queda con la pregunta del contenedor, Catalog o Page, en lugar de la pregunta del tipo de acción
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.AddStandardFont(4);
Lib.DrawText(40, 700, 'Quarterly statement');
Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
'https://example.com/audit/will-save', '', 0, 0);
Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_SUBMIT,
'https://example.com/forms/submit', 'CustomerName;OrderTotal', 0, 0);
Lib.SaveToFile('statement.pdf');
finally
Lib.Free;
end;
end;
¿En qué se diferencia un disparador a nivel de página de uno a nivel de documento?
Un disparador a nivel de página solo se dispara para el único objeto Page al que está adjunto, y PDFlibPas lo almacena en el propio diccionario /AA de esa página en lugar de en el del Catalog. Solo hay dos disparadores de página, Open y Close, correspondientes a las claves O y C que define ISO 32000-1 para el diccionario de acciones adicionales de una página, y PDFlibPas los expone como patOpen y patClose a través de SetPageAction, que se adjunta a cualquiera que sea la página seleccionada en ese momento mediante SelectPage, un detalle que importa la primera vez que recorréis un documento en bucle esperando que una llamada se aplique en todas partes, porque nunca lo hace. Adjuntar cualquiera de los dos tipos de disparador también eleva la versión mínima de PDF del archivo, y los dos contenedores piden suelos distintos: PDFlibPas eleva el documento a al menos PDF 1.4 la primera vez que escribe una entrada Catalog /AA, y a al menos PDF 1.5 la primera vez que escribe una entrada Page /AA, sin importar qué tipo de acción resida dentro. Eso es un requisito a nivel de contenedor superpuesto a lo que la propia acción necesite por sí sola, así que una simple acción URI que solo requeriría PDF 1.1 en solitario aun así arrastra todo el archivo hasta PDF 1.5 en cuanto se envuelve en un disparador de apertura de página
Lib.SelectPage(3);
Lib.SetPageAction(patOpen, PDF_ACTION_BUILDER_JAVASCRIPT,
'app.alert("Section 3: internal review only");', '', 0, 0);
Lib.SetPageAction(patClose, PDF_ACTION_BUILDER_WEB,
'https://example.com/analytics/page-3-closed', '', 0, 0);
Leer y eliminar acciones de ciclo de vida
GetDocumentActionInfo y GetPageActionInfo devuelven ambos un registro TPDFlibActionInfo, y el campo Kind vuelve como akNone siempre que ese disparador no tenga nada adjunto, así que comprobad Kind antes de confiar en cualquier otro campo del registro, URI, JavaScript, FileName y el resto solo tienen sentido para el único tipo de acción que Kind realmente reporta, ya que la misma forma de registro se reutiliza en cada tipo de acción que puede producir el constructor. RemoveDocumentAction y RemovePageAction limpian cada uno un único disparador y reportan 1 cuando encontraron algo que eliminar, 0 cuando el disparador ya estaba vacío; cuando la entrada eliminada era la última que quedaba en el diccionario /AA, PDFlibPas elimina el propio /AA ya vacío en lugar de dejar atrás un contenedor colgante y sin sentido en el Catalog o en la página
var
Info: TPDFlibActionInfo;
begin
Info := Lib.GetDocumentActionInfo(datWillSave);
if Info.Kind = akURI then
WriteLn('WillSave calls out to: ', string(Info.URI));
if Lib.RemoveDocumentAction(datWillSave) = 1 then
Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
'https://example.com/audit/will-save-v2', '', 0, 0);
end;
¿Permite PDF/A en absoluto las acciones de ciclo de vida?
No. La conformidad PDF/A rechaza todo el contenedor de acciones adicionales, no solo los tipos de acción que suenan arriesgados, porque ISO 19005 restringe el modelo de acción interactiva de PDF bajo la suposición de que un archivo de archivado debe renderizarse de la misma manera dentro de décadas, sin depender de un motor de scripts o de una conexión de red que quizá no exista para entonces. SetLifecycleAction, el constructor compartido detrás tanto de SetDocumentAction como de SetPageAction, comprueba PDFAMode antes de mirar siquiera ActionKind, así que una acción URI que simplemente abre una página web de empresa o una acción Named que solo significa ir a la página siguiente cae en la misma red que una peligrosa, nada que un revisor de seguridad normalmente marcaría, bloqueado de todos modos, porque la restricción es estructural y no caso por caso. El peligro práctico es que el rechazo es silencioso: tanto SetDocumentAction como SetPageAction devuelven 0 sin lanzar ninguna excepción, así que un punto de llamada que nunca comprueba el valor de retorno publica un documento que en silencio carece del disparador que se suponía que debía llevar
Lib.SetPDFAMode(2); // PDF/A-1b
if Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_NAMED,
'', '', 0, 0) = 0 then
// rejected: PDF/A-1b forbids Catalog /AA, even a plain Named action
WriteLn('lifecycle action not attached');
Merece la pena tener presente una asimetría. RemoveDocumentAction y RemovePageAction nunca comprueban PDFAMode, así que cargar un archivo que ya lleva acciones de ciclo de vida no conformes y eliminarlas de camino a un guardado conforme con PDF/A funciona exactamente como cabría esperar, solo la vía de escritura, adjuntar un disparador nuevo, está condicionada por el modo de conformidad
¿Dónde encaja imprimir-al-abrir sin un disparador WillOpen?
El diccionario Catalog /AA no tiene ninguna entrada WillOpen en absoluto, por diseño, el /AA a nivel de documento en ISO 32000-1 define exactamente cinco claves, WillClose, WillSave, DidSave, WillPrint y DidPrint, y nada en esa lista se dispara puramente porque se abrió un archivo. El gancho en el momento de apertura vive en una entrada Catalog aparte, /OpenAction, que PDFlibPas expone a través de su propia familia de llamadas, SetOpenActionJavaScript, SetOpenActionDestination y SetOpenActionNamedDestination entre ellas, ninguna de las cuales toca en absoluto el diccionario /AA ni la enumeración TPDFlibDocumentActionTrigger. Los dos mecanismos sí se componen, sin embargo, y eso es normalmente lo que realmente necesita una plantilla de imprimir-al-abrir: construid la plantilla de modo que su /OpenAction inicie el trabajo de impresión, típicamente una acción JavaScript que llame al propio comando de impresión del visor, y la propia impresión es lo que le da a WillPrint y DidPrint algo contra lo que ejecutarse, una marca de tiempo estampada antes de que las páginas entren en cola, una entrada de auditoría escrita en cuanto terminan
¿Cuán fiables son estos disparadores en los distintos visores de PDF?
No todos los visores los ejecutan, incluso fuera de PDF/A, así que tratad una acción de ciclo de vida como una petición y no como una garantía. Acrobat y la mayoría de los lectores de escritorio completos ejecutan el conjunto entero con fidelidad, pero una gran parte del consumo real de PDF nunca toca en absoluto un diccionario de acciones adicionales: los visores incrustados en navegador, la mayoría de los lectores móviles, y casi cualquier pipeline de renderizado o extracción de texto del lado del servidor o bien ignoran /AA de plano o solo respetan una porción estrecha de él, con WillPrint y DidPrint yendo típicamente peor ya que la conversión sin interfaz no tiene ninguna operación de impresión a la que engancharse. Si una acción de envío de formulario en WillClose es la única vía que captura los datos de formulario, no es una vía fiable, emparejadla con un botón de envío explícito, y tratad el disparador automático como una comodidad para los lectores que resulten admitirlo
Los disparadores de documento, página y campo son tres niveles de la misma maquinaria subyacente de diccionario de acción, y en cuanto el contenedor está claro, el resto es elegir la constante ActionKind correcta y comprobar el código de retorno. Estos disparadores de ciclo de vida, junto con la API de constructor de acciones más amplia que toca este artículo, forman parte de la biblioteca PDF PDFlibPas para Delphi estándar, con la referencia completa de disparadores y tipos de acción en la documentación del producto