Artigo Técnico

Ações de Ciclo de Vida de PDF: Catalog /AA versus Page /AA em Delphi

O PDFlibPas, a biblioteca PDF nativa para Delphi e C++Builder, dá a um documento PDF dois lugares separados para pendurar comportamento automático: ações de ciclo de vida em nível de documento, como WillClose, WillSave, DidSave, WillPrint e DidPrint, armazenadas no dicionário /AA do Catalog, e ações de ciclo de vida em nível de página — Open e Close — armazenadas no próprio dicionário /AA de cada objeto Page, em vez disso. Confundir os dois contêineres é a forma mais comum, sozinha, de uma ação de ciclo de vida silenciosamente não fazer nada

Os casos motivadores são comuns. Uma equipe financeira quer um modelo de extrato que carimba um timestamp de impressão e registra quem o imprimiu no instante em que a impressão de fato começa, não quando o arquivo meramente abre. Um fluxo de trabalho pesado em formulários precisa que valores de campo sejam enviados a um servidor automaticamente antes de o cliente PDF do leitor ter permissão de fechar a janela, de modo que uma aba fechada nunca signifique uma edição perdida. Um relatório de múltiplas páginas quer um banner específico de página que aparece só enquanto aquela página está na tela. O PDF na verdade oferece um terceiro nível abaixo de documento e página para esse tipo de comportamento — ações anexadas à própria entrada /A de um campo de formulário individual ou link, o assunto de um artigo complementar sobre ações de formulário interativo e JavaScript — mas este artigo permanece nos dois níveis acima dele: todo o documento, e uma única página

Quais gatilhos vivem no /AA do Catalog do documento?

Cinco gatilhos vivem no dicionário /AA do Catalog, e cada um deles dispara para um evento que afeta todo o documento, não uma única página. A ISO 32000-1 §12.6.3 (Trigger Events) lista as chaves em nível de 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 respectivamente, e o PDFlibPas espelha esse conjunto exatamente na enumeração TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction é o único ponto de entrada que anexa qualquer uma das cinco, e o parâmetro ActionKind que recebe é uma das dez constantes PDF_ACTION_BUILDER_* compartilhadas por toda chamada de construtor de ação na biblioteca, de um URI simples a um script a um salto de destino. O que uma ação GoTo, arquivo remoto, arquivo embutido ou Launch de fato faz quando disparada é uma questão diferente de onde é anexada, e esse é o assunto de um artigo complementar sobre ações GoTo, remotas, embutidas e launch — este permanece na questão do contêiner, 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;

Como um gatilho em nível de página é diferente de um em nível de documento?

Um gatilho em nível de página dispara apenas para o único objeto Page ao qual está anexado, e o PDFlibPas o armazena no próprio dicionário /AA daquela página, em vez 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 os expõe como patOpen e patClose por meio de SetPageAction, que anexa a qualquer página que esteja atualmente selecionada via SelectPage — um detalhe que importa na primeira vez que você percorre um documento em loop esperando que uma chamada se aplique em todo lugar, porque nunca acontece. Anexar qualquer um dos tipos de gatilho também eleva a versão mínima de PDF do arquivo, e os dois contêineres pedem pisos diferentes: o PDFlibPas eleva o documento para pelo menos PDF 1.4 na primeira vez que escreve uma entrada /AA do Catalog, e para pelo menos PDF 1.5 na primeira vez que escreve uma entrada /AA de Page, independentemente de qual tipo de ação senta dentro. Essa é uma exigência em nível de contêiner colocada em camada sobre o que a própria ação precisa por si só, de modo que uma ação de URI simples que só exigiria PDF 1.1 sozinha ainda puxa o arquivo inteiro para PDF 1.5 uma vez que é envolvida em um 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);

Lendo e removendo ações de ciclo de vida

GetDocumentActionInfo e GetPageActionInfo ambos retornam um registro TPDFlibActionInfo, e o campo Kind volta como akNone sempre que aquele gatilho não tem nada anexado, então verifique Kind antes de confiar em qualquer outro campo do registro — URI, JavaScript, FileName e o resto só são significativos para o único tipo de ação que Kind de fato reporta, já que a mesma forma de registro é reutilizada em todo tipo de ação que o construtor consegue produzir. RemoveDocumentAction e RemovePageAction cada um limpa um único gatilho e reporta 1 quando encontrou 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 exclui o próprio /AA agora vazio, em vez de deixar um contêiner solto e sem sentido para trás 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 ações de ciclo de vida de forma alguma?

Não. A conformidade PDF/A rejeita todo o contêiner de ações adicionais, não apenas os tipos de ação que soam arriscados, porque a ISO 19005 restringe o modelo de ação interativa do PDF sob a suposição de que um arquivo arquivístico deve renderizar da mesma forma décadas a partir de agora, sem depender de um motor de script ou uma conexão de rede que talvez não exista até lá. SetLifecycleAction, o construtor compartilhado por trás tanto de SetDocumentAction quanto de SetPageAction, verifica PDFAMode antes mesmo de olhar para ActionKind, de modo que uma ação de URI que só abre uma página web da empresa ou uma ação Named que só significa ir para a próxima página é capturada na mesma rede que uma perigosa — nada que um revisor de segurança normalmente sinalizaria, bloqueado de qualquer forma, porque a restrição é estrutural, não caso a caso. O perigo prático é que a rejeição é silenciosa: SetDocumentAction e SetPageAction ambos retornam 0 sem levantar uma exceção, de modo que um ponto de chamada que nunca verifica o valor de retorno lança um documento silenciosamente faltando o gatilho que deveria carregar

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');

Uma assimetria vale a pena manter em mente. RemoveDocumentAction e RemovePageAction nunca verificam PDFAMode, de modo que carregar um arquivo que já carrega ações de ciclo de vida não conformes e removê-las a caminho de um salvamento em conformidade PDF/A funciona exatamente como esperado — só o caminho de escrita, anexar um novo gatilho, é controlado pelo modo de conformidade

Onde a impressão-ao-abrir se encaixa sem um gatilho WillOpen?

O dicionário /AA do Catalog não tem nenhuma entrada WillOpen de forma alguma, por design — o /AA em nível de documento na ISO 32000-1 define exatamente cinco chaves, WillClose, WillSave, DidSave, WillPrint e DidPrint, e nada nessa lista dispara puramente porque um arquivo foi aberto. O gancho de tempo de abertura vive em uma entrada separada do Catalog, /OpenAction, que o PDFlibPas expõe por meio de sua própria família de chamadas, SetOpenActionJavaScript, SetOpenActionDestination e SetOpenActionNamedDestination entre elas, nenhuma das quais toca o dicionário /AA ou a enumeração TPDFlibDocumentActionTrigger de forma alguma. Os dois mecanismos, porém, se compõem, e isso geralmente é o que um modelo de impressão-ao-abrir de fato precisa: construa o modelo de modo que seu /OpenAction inicie o trabalho de impressão, tipicamente uma ação JavaScript chamando o próprio comando de impressão do visualizador, e a própria impressão é o que dá a WillPrint e DidPrint algo contra o que rodar — um timestamp carimbado antes de as páginas serem enfileiradas, uma entrada de auditoria escrita uma vez que terminam

Quão confiáveis são esses gatilhos em diferentes visualizadores de PDF?

Nem todo visualizador os executa, mesmo fora do PDF/A, então trate uma ação de ciclo de vida como um pedido, não uma garantia. O Acrobat e a maioria dos leitores de desktop completos executam o conjunto inteiro fielmente, mas uma grande fatia do consumo real de PDF nunca toca um dicionário de ações adicionais de forma alguma: visualizadores embutidos em navegador, a maioria dos leitores móveis, e quase todo pipeline de renderização ou extração de texto do lado do servidor ou ignora /AA completamente ou honra apenas uma fatia estreita dele, com WillPrint e DidPrint tipicamente se saindo pior, já que conversão headless não tem nenhuma operação de impressão para eles se conectarem. Se uma ação de submeter-formulário no WillClose é o único caminho que captura dados de formulário, não é um caminho confiável — pareie-a com um botão de envio explícito, e trate o gatilho automático como uma conveniência para os leitores que por acaso o suportam

Gatilhos de documento, página e campo são três níveis da mesma maquinaria de dicionário de ação subjacente, e uma vez que o contêiner está claro, o resto é escolher a constante ActionKind certa e verificar o código de retorno. Esses gatilhos de ciclo de vida, junto com a API de construtor de ação mais ampla que este artigo toca, vêm como parte da biblioteca PDF Delphi PDFlibPas padrão, com a referência completa de gatilho e tipo de ação na documentação do produto