O PDFlibPas, a biblioteca de PDF nativa para Delphi e C++Builder, oferece a um documento PDF dois locais separados onde pendurar comportamento automático: ações de ciclo de vida ao nível do documento, como WillClose, WillSave, DidSave, WillPrint e DidPrint, guardadas no dicionário /AA do Catalog, e ações de ciclo de vida ao nível da página — Open e Close — guardadas antes no próprio dicionário /AA de cada objeto Page. Confundir os dois contentores é a forma mais comum, de longe, de uma ação de ciclo de vida silenciosamente não fazer nada
Os casos motivadores são comuns. Uma equipa financeira quer um modelo de extrato que carimbe uma data/hora de impressão e registe quem imprimiu no momento em que a impressão efetivamente começa, não quando o ficheiro é apenas aberto. Um fluxo de trabalho carregado de formulários precisa que os valores dos campos sejam enviados automaticamente para um servidor antes de o cliente PDF do leitor ter permissão para fechar a janela, para que fechar um separador nunca signifique uma edição perdida. Um relatório de várias páginas quer uma faixa específica de página que só aparece enquanto essa página está no ecrã. O PDF oferece, na verdade, um terceiro nível abaixo de documento e página para este tipo de comportamento — ações associadas à própria entrada /A de um campo de formulário ou ligação individual, tema de um artigo relacionado sobre ações de formulário interativas e JavaScript — mas este artigo mantém-se nos dois níveis acima: o documento inteiro, e uma única página
Que gatilhos residem no /AA do Catalog do documento?
Cinco gatilhos residem no dicionário /AA do Catalog, e cada um deles dispara para um evento que afeta o documento inteiro, não uma única página. A ISO 32000-1 §12.6.3 (Trigger Events) lista as chaves ao nível do documento como WC, WS, DS, WP e DP — os nomes literais de duas letras escritos no dicionário /AA — para WillClose, WillSave, DidSave, WillPrint e DidPrint respetivamente, e o PDFlibPas espelha esse conjunto exatamente na enumeração TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. O SetDocumentAction é o único ponto de entrada que associa qualquer uma das cinco, e o parâmetro ActionKind que recebe é uma de dez constantes PDF_ACTION_BUILDER_* partilhadas em cada chamada de construtor de ações na biblioteca, de um simples URI a um script a um salto de destino. O que uma ação GoTo, de ficheiro remoto, de ficheiro incorporado, ou Launch efetivamente faz uma vez despoletada é uma questão diferente de onde é associada, e esse é o tema de um artigo relacionado sobre ações GoTo, remotas, incorporadas, e de lançamento — este mantém-se na questão do contentor, Catalog ou Page, em vez da questão do tipo de ação
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;
Em que difere um gatilho ao nível da página de um ao nível do documento?
Um gatilho ao nível da página só dispara para o único objeto Page a que está associado, e o PDFlibPas guarda-o no próprio dicionário /AA dessa página, em vez de no do Catalog. Há apenas dois gatilhos de página, Open e Close, correspondendo às chaves O e C que a ISO 32000-1 define para o dicionário de ações adicionais de uma página, e o PDFlibPas expõe-nos como patOpen e patClose através de SetPageAction, que se associa a qualquer que seja a página atualmente selecionada via SelectPage — um pormenor que importa na primeira vez que se percorre um documento em ciclo esperando que uma chamada se aplique a tudo, porque nunca se aplica. Associar qualquer um dos dois tipos de gatilho também eleva a versão mínima de PDF do ficheiro, e os dois contentores pedem pisos diferentes: o PDFlibPas eleva o documento para, no mínimo, PDF 1.4 na primeira vez que escreve uma entrada /AA do Catalog, e para, no mínimo, PDF 1.5 na primeira vez que escreve uma entrada /AA de Page, independentemente de qual o tipo de ação lá dentro. Esse é um requisito ao nível do contentor sobreposto ao que a própria ação por si só precisa, pelo que uma simples ação URI que, isolada, só exigiria PDF 1.1 continua a puxar o ficheiro inteiro para PDF 1.5 assim que é envolvida num gatilho de abertura 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);
Ler e remover ações de ciclo de vida
Tanto GetDocumentActionInfo como GetPageActionInfo devolvem um registo TPDFlibActionInfo, e o campo Kind volta como akNone sempre que esse gatilho não tenha nada associado, pelo que se deve verificar Kind antes de confiar em qualquer outro campo do registo — URI, JavaScript, FileName e o resto só têm significado para o único tipo de ação que Kind efetivamente reporta, já que a mesma forma de registo é reutilizada em cada tipo de ação que o construtor consegue produzir. O RemoveDocumentAction e o RemovePageAction limpam cada um um único gatilho e reportam 1 quando encontraram algo para remover, 0 quando o gatilho já estava vazio; quando a entrada removida era a última que restava no dicionário /AA, o PDFlibPas apaga o próprio /AA agora vazio em vez de deixar para trás um contentor pendente e sem significado no Catalog ou na 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;
O PDF/A permite de todo ações de ciclo de vida?
Não. A conformidade PDF/A rejeita o contentor de ações adicionais por inteiro, não apenas os tipos de ação que soam a risco, porque a ISO 19005 restringe o modelo de ação interativa do PDF partindo do princípio de que um ficheiro de arquivo tem de ser apresentado da mesma forma daqui a décadas, sem depender de um motor de scripts ou de uma ligação de rede que possa já não existir nessa altura. O SetLifecycleAction, o construtor partilhado por trás tanto de SetDocumentAction como de SetPageAction, verifica PDFAMode antes sequer de olhar para ActionKind, pelo que uma ação URI que se limita a abrir a página web de uma empresa, ou uma ação Named que só significa ir para a página seguinte, é apanhada na mesma rede que uma perigosa — nada que um revisor de segurança normalmente assinalaria, bloqueado de qualquer forma, porque a restrição é estrutural, não caso a caso. O perigo prático é que a rejeição é silenciosa: tanto SetDocumentAction como SetPageAction devolvem 0 sem levantar qualquer exceção, pelo que um ponto de chamada que nunca verifique o valor de retorno distribui um documento silenciosamente sem o gatilho que era suposto transportar
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');
Vale a pena ter em conta uma assimetria. O RemoveDocumentAction e o RemovePageAction nunca verificam PDFAMode, pelo que carregar um ficheiro que já transporta ações de ciclo de vida não conformes e removê-las a caminho de uma gravação conforme com PDF/A funciona exatamente como esperado — só o caminho de escrita, associar um novo gatilho, está condicionado ao modo de conformidade
Onde se encaixa a impressão ao abrir sem um gatilho WillOpen?
O dicionário /AA do Catalog não tem, de todo, nenhuma entrada WillOpen, por conceção — o /AA ao nível do documento na ISO 32000-1 define exatamente cinco chaves, WillClose, WillSave, DidSave, WillPrint e DidPrint, e nada nessa lista dispara apenas porque um ficheiro foi aberto. O gancho de tempo de abertura reside numa entrada separada do Catalog, /OpenAction, que o PDFlibPas expõe através da sua própria família de chamadas, SetOpenActionJavaScript, SetOpenActionDestination e SetOpenActionNamedDestination entre elas, nenhuma das quais toca de todo no dicionário /AA nem na enumeração TPDFlibDocumentActionTrigger. Os dois mecanismos compõem-se, contudo, e isso é normalmente o que um modelo de impressão ao abrir efetivamente precisa: construir o modelo de forma a que o seu /OpenAction inicie a tarefa de impressão, tipicamente uma ação JavaScript que chama o próprio comando de impressão do visualizador, e a própria impressão é o que dá ao WillPrint e ao DidPrint algo contra que correr — uma data/hora carimbada antes de as páginas entrarem na fila, uma entrada de auditoria escrita assim que terminam
Quão fiáveis são estes gatilhos entre visualizadores de PDF?
Nem todos os visualizadores os correm, mesmo fora do PDF/A, pelo que se deve tratar uma ação de ciclo de vida como um pedido, não como uma garantia. O Acrobat e a maioria dos leitores de ambiente de trabalho completos executam o conjunto inteiro fielmente, mas uma grande parte do consumo real de PDF nunca chega a tocar num dicionário de ações adicionais: visualizadores embutidos em navegador, a maioria dos leitores móveis, e quase todos os pipelines de renderização ou extração de texto do lado do servidor, ou ignoram /AA por completo, ou respeitam apenas uma fatia restrita dele, com WillPrint e DidPrint tipicamente a sair-se pior, já que a conversão sem interface gráfica não tem qualquer operação de impressão a que se possam ligar. Se uma ação de submissão de formulário no WillClose for o único caminho que captura dados de formulário, não é um caminho fiável — emparelhe-se com um botão de submissão explícito, e trate-se o gatilho automático como uma conveniência para os leitores que calhem suportá-lo
Os gatilhos de documento, página, e campo são três níveis da mesma maquinaria subjacente de dicionário de ações, e assim que o contentor está claro, o resto é escolher a constante ActionKind certa e verificar o código de retorno. Estes gatilhos de ciclo de vida, juntamente com a API de construtor de ações mais ampla que este artigo aborda, fazem parte da biblioteca de PDF PDFlibPas para Delphi padrão, com a referência completa de gatilhos e tipos de ação na documentação do produto