O PDFlibPas dá aos programadores Delphi e C++Builder três tipos de ação para navegação que deixa a página atual para trás: o GoToR (Go To Remote) abre uma página específica noutro ficheiro PDF, o GoToE (Go To Embedded) abre um ficheiro PDF incorporado dentro do documento atual, e o Launch corre um programa externo ou abre um ficheiro através da shell do sistema operativo. Os três residem na ISO 32000-1 §12.6.4, a secção Action Types que também define a ação GoTo do dia a dia, e cada um transporta a sua própria armadilha para os incautos: um número de página que significa coisas diferentes consoante qual a chamada que o constrói, um alvo que é um nome em vez de um caminho de ficheiro, e um par de parâmetros de cadeia de texto que parecem idênticos mas servem dois visualizadores diferentes
Nada disto é hipotético. Um pacote de referência técnica — um manual principal, um PDF de especificações que um distribuidor atualiza no seu próprio calendário, um utilitário de calibração instalado a par de ambos — apoia-se exatamente neste tipo de ligação entre documentos: uma referência cruzada que tem de aterrar na página 5 do ficheiro de especificações, uma folha de dados que vale a pena distribuir dentro do manual em vez de ao lado dele, uma ligação que passa diretamente para a ferramenta de calibração. Este artigo é a imagem espelhada de ler de volta ações de marcador e de anotação de um PDF já existente: essa peça aborda consumir uma ação GoToR, Launch, ou GoToE que outro produtor já escreveu num ficheiro; esta aborda construir esses mesmos três tipos de ação do zero, incluindo as regras ao nível do campo que o PDFlibPas impõe antes de sequer confirmar um único byte
Três formas de uma ação de PDF deixar a página atual
O PDFlibPas separa a navegação local de tudo o resto na chave /S da ação, e GoToR, GoToE, e Launch são os três subtipos cujo alvo reside fora da página atual: GoToR ao abrigo da ISO 32000-1 §12.6.4.3, GoToE ao abrigo da §12.6.4.4, e Launch ao abrigo da §12.6.4.5, todos dentro da secção mais ampla §12.6.4 Action Types que também define a ação GoTo do dia a dia. O destino de uma simples ação GoTo nomeia um objeto de página que já existe dentro do documento, pelo que o PDFlibPas o consegue validar de imediato; o GoToR e o GoToE não conseguem fazê-lo da mesma forma, já que o ficheiro externo pode nem sequer existir nesta máquina e a contagem de páginas de um ficheiro incorporado não é algo que o documento anfitrião acompanhe, pelo que ambos transportam uma referência por resolver em vez de uma ligação rígida — uma especificação de ficheiro mais um destino para o GoToR, um nome de ficheiro incorporado mais uma página-alvo para o GoToE — enquanto o Launch descarta por completo o conceito de destino e limita-se a nomear algo para o sistema operativo correr ou abrir. Essa divisão manifesta-se como duas famílias de chamadas do lado da escrita: construtores de alto nível, de uma só vez, como AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF, e AddLinkToLocalFile criam em conjunto uma anotação de ligação de ponto ativo de página e a sua ação, cobrindo a maioria das disposições reais — uma linha de texto ou um ícone em que um leitor clica — enquanto definidores de nível mais baixo como SetActionRemoteDestinationEx, SetActionLaunchOptions, e os respetivos equivalentes AddActionNext* associam ou substituem uma ação sobre algo de que já se detenha um handle: um marcador existente, um gatilho de campo de formulário, ou um evento de ciclo de vida ao nível do documento ou da página. Ambas as famílias acabam por escrever as mesmas formas de dicionário; a diferença está em onde se está a partir de quando se chamam, e, como a secção seguinte aborda, no que um número de página significa quando o fazem
Como construir uma ligação GoToR que abre uma página noutro ficheiro PDF?
Uma ação GoToR precisa de duas coisas — uma especificação de ficheiro e um destino dentro desse ficheiro — e o PDFlibPas expõe duas chamadas diferentes para fornecer a segunda parte, cada uma com a sua própria convenção de numeração de página. O AddLinkToFile e o AddLinkToFileEx, os construtores de alto nível de ponto ativo de página, validam o seu argumento Page ou DestPage como maior que zero, a mesma numeração de base 1 que o PDFlibPas usa em todo o resto, incluindo SelectPage. O SetActionRemoteDestinationEx, o definidor de nível mais baixo usado para associar ou substituir uma ação GoToR sobre algo de que já se tenha um handle, valida antes DestPage como maior ou igual a zero e escreve-o diretamente no array de destino explícito da ação sem qualquer ajuste: quer o índice de página em bruto, de base zero, do documento-alvo, a numeração que a ISO 32000-1 especifica para um destino explícito remoto. Chamar o definidor de baixo nível com o mesmo número que se entregaria ao construtor de alto nível faz a ligação abrir uma página cedo demais
var
Lib: TPDFlib;
ActionID: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('manual.pdf', '') = 1 then
begin
Lib.SelectPage(12);
// Page is 1-based here, same as SelectPage above: this opens
// the fifth page of specs.pdf.
Lib.AddLinkToFile(72, 700, 200, 16, 'specs.pdf', 5, 0, 0, 0);
// A later maintenance pass repoints the same link at a
// reorganized file. SetActionRemoteDestinationEx edits the
// action directly, and DestPage here is the zero-based index
// PDF itself uses for a remote explicit destination -- "the
// fifth page" is now 4, not 5.
ActionID := Lib.GetAnnotActionID(1);
Lib.SetActionRemoteDestinationEx(ActionID, 'specs-2026.pdf',
4, Ord(dkFit), 0, 0, 0, 0, 0, 0, -1);
end;
finally
Lib.Free;
end;
end;
O resto dos argumentos de SetActionRemoteDestinationEx é igualmente literal. O ValueMask é um conjunto de bits — 1 para esquerda, 2 para topo, 4 para direita, 8 para fundo, 16 para zoom — e o PDFlibPas verifica-o contra DestType antes de escrever seja o que for: um destino dkFitR tem de fornecer exatamente 15 (os quatro limites, sem zoom), o dkFit e o dkFitB têm de fornecer 0, e o dkFitH/dkFitV só aceitam a sua única coordenada relevante. Os bits deixados por definir dentro de uma máscara de resto válida não são omitidos do array; são escritos como um null de PDF explícito, que a ISO 32000-1 trata como "manter o valor que o visualizador já tem" para essa coordenada — uma forma legítima de dizer "saltar para esta página, deixar o zoom em paz" em vez de um descuido. O próprio zoom é guardado como uma fração do valor passado, pelo que uma chamada a pedir 150 por cento entrega ao array um valor guardado de 1.5, e o intervalo de entrada válido é de 0 a 6400
Como ligar a um PDF incorporado dentro do próprio documento?
O AddLinkToEmbeddedPDF constrói a ação GoToE, e o seu argumento-alvo, EmbeddedFileName, é um nome, não um caminho: tem de corresponder à cadeia Title já passada a EmbedFile quando o anexo foi feito, porque esse título é a chave literal que o PDFlibPas guarda na árvore de nomes /EmbeddedFiles do documento, e o GoToE resolve procurando esse nome, não voltando a tocar no sistema de ficheiros. A função só verifica que EmbeddedFileName não está vazio e que TargetPage é pelo menos 1 — passe-se um nome que nunca chegou a ser efetivamente incorporado e a chamada continua a devolver sucesso, a ação continua a ser escrita, e a ligação simplesmente falha ao resolver-se para todos os leitores que nela cliquem
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
Lib.NewPage;
// The Title argument becomes the key PDFlibPas stores in the
// document's EmbeddedFiles name tree -- that string, not
// "datasheet.pdf", is the target GoToE resolves against.
if Lib.EmbedFile('Datasheet', 'datasheet.pdf', 'application/pdf') = 1 then
Lib.AddLinkToEmbeddedPDF(72, 700, 200, 16, 'Datasheet', 3, 0, 0);
Lib.SaveToFile('manual.pdf');
finally
Lib.Free;
end;
end;
Empilham-se aqui dois pisos de versão, não um. O EmbedFile precisa de PDF 1.4 para a árvore de nomes /EmbeddedFiles, e o AddLinkToEmbeddedPDF eleva separadamente o piso para PDF 1.6 para o próprio tipo de ação GoToE, pelo que o mínimo efetivo para qualquer documento que use esta funcionalidade é 1.6, não 1.4. Repare-se também que TargetPage aqui é de base 1, a convenção comum do PDFlibPas — um contraste deliberado com o DestPage de base zero que a secção anterior acabou de abordar, e um lembrete de que qual o esquema de numeração de página que se aplica depende do tipo de ação e da chamada específica, não de uma regra geral única. O dicionário-alvo da ação também pode transportar uma entrada /R de C para filho ou P para pai, suportando uma cadeia de dois saltos para dentro de um ficheiro incorporado ou de volta para o seu contentor, embora o AddLinkToEmbeddedPDF só alguma vez construa a direção de filho, já que é essa a que faz sentido a partir de um documento que está a fazer a incorporação, e não a ser incorporado
Ações Launch: um FileName, dois alvos de texto que não são intercambiáveis
O SetActionLaunchOptions escreve o alvo de ficheiro de uma ação Launch em duas chaves diferentes a partir de um único argumento FileName, e as duas chaves guardam dois tipos diferentes de cadeia de texto. A chave de topo /F recebe um dicionário de especificação de ficheiro, construído através da mesma conversão de caminho que o PDFlibPas usa para o GoToR, que é a forma portátil que a ISO 32000-1 §7.11.3 define para um dicionário de especificação de ficheiro. O subdicionário /Win, quando o PDFlibPas escreve um, recebe a sua própria chave /F definida com o valor FileName em bruto exatamente como foi passado, sem qualquer conversão, porque /Win /F está documentado na ISO 32000-1 §12.6.4.5 como uma simples cadeia de caminho Windows destinada apenas a ser lida por um visualizador Windows. Ao passar-se um caminho portátil já convertido esperando que ambas as chaves acabem idênticas, a cópia /Win vai transportar o que quer que se tenha entregue à função, sem alterações
var
Lib: TPDFlib;
ActionID: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('manual.pdf', '') = 1 then
begin
Lib.SelectPage(1);
Lib.AddLinkToLocalFile(72, 660, 220, 16, 'calibrate.exe', 0);
ActionID := Lib.GetAnnotActionID(1);
// Operation 0 leaves this as a normal open -- pass 1 to ask a
// Windows viewer to print instead. Parameters and
// DefaultDirectory only ever reach /Win /P and /Win /D, never
// the top-level /F.
Lib.SetActionLaunchOptions(ActionID, 'calibrate.exe',
'/silent /profile:default', 'C:\Tools\Calibration', 0, -1);
end;
finally
Lib.Free;
end;
end;
Trate-se o Launch como a ação de maior atrito das três, porque o seu propósito inteiro é correr um programa ou abrir um ficheiro fora da sandbox do PDF, e qualquer visualizador comum o trata em conformidade. A Enhanced Security do Adobe Acrobat bloqueia ou pergunta perante ações Launch por predefinição, a menos que o alvo esteja numa localização explicitamente de confiança, e a maioria das implantações empresariais do Acrobat deixa essa proteção ativada. Uma ação Launch num documento entregue ao público não é, portanto, um gatilho fiável: planeie-se que seja bloqueada, questionada, ou silenciosamente ignorada por qualquer que seja o visualizador que abra o ficheiro, e reserve-se para ambientes fechados onde também se controlem as definições de confiança do visualizador — um quiosque interno, uma implantação corporativa controlada, um documento que nunca sai de uma máquina gerida pela própria empresa
O portão do PDF/A: porque é que as chamadas GoToR e Launch podem devolver zero
O SetActionRemoteDestinationEx e o SetActionLaunchOptions recusam-se por completo quando o documento-alvo está em qualquer modo de conformidade PDF/A: ambos verificam o modo PDF/A do documento como a sua primeiríssima condição e saem com um resultado de 0 antes de tocar na ação, sem qualquer exceção levantada. Isto é deliberado. As restrições do PDF/A sobre ações interativas excluem especificamente o Launch, já que dar a um ficheiro de arquivo a capacidade de correr um programa arbitrário é exatamente o tipo de comportamento dependente do ambiente que os formatos de arquivo a longo prazo existem para prevenir, e o PDFlibPas aplica o mesmo portão conservador ao definidor de ir-para remoto no mesmo caminho de código. A consequência prática é fácil de passar despercebida durante o desenvolvimento: a chamada idêntica que funciona num PDF comum vai compilar, correr, e silenciosamente não fazer nada num documento carregado com um nível de conformidade PDF/A definido, pelo que se deve verificar o valor de retorno em vez de presumir sucesso — um 0 aqui não é um erro de entrada malformada, é a biblioteca a recusar um pedido que entra em conflito com a própria declaração de conformidade do documento
Onde se encaixam GoToR, GoToE, e Launch num fluxo de trabalho PDFlibPas mais amplo
Os três tipos de ação deste artigo não chegam todos aos mesmos sítios. O artigo relacionado sobre gatilhos de ação de ciclo de vida de documento e página aborda SetDocumentAction e SetPageAction, que conseguem associar uma ação GoToR ou Launch a um gatilho como WillClose através das constantes partilhadas PDF_ACTION_BUILDER_REMOTE_DESTINATION e PDF_ACTION_BUILDER_LAUNCH — o mesmo construtor que também cobre um simples gatilho URI ou JavaScript. O GoToE não tem nenhuma constante desse tipo e nenhum caminho para esse construtor genérico de todo; o AddLinkToEmbeddedPDF é a única forma de o PDFlibPas construir um, o que o torna estritamente uma ação de ponto ativo de página, nunca um gatilho ao nível do documento ou da página. Onde o GoToR e o Launch chegam de facto ao construtor genérico, a contrapartida é controlo: constrói um GoToR a apontar apenas para um destino remoto nomeado e uma ação Launch com apenas um nome de ficheiro e parâmetros, enquanto o endereçamento explícito de página e tipo de ajuste, e as opções de lançamento específicas do Windows aqui abordadas, só se alcançam diretamente através de SetActionRemoteDestinationEx e SetActionLaunchOptions
Vale a pena conhecer uma propriedade de segurança antes de construir uma ferramenta de manutenção à volta destes definidores. O SetActionRemoteDestinationEx e o SetActionLaunchOptions constroem primeiro toda a ação de substituição num dicionário de rascunho, e só apagam e copiam as chaves /F, /D ou /Win, e /NewWindow para a ação ativa assim que essa cópia de rascunho se valida — pelo que uma chamada que falhe a validação, seja por um ValueMask fora do intervalo ou um FileName vazio, deixa a ação original, e qualquer cadeia /Next já pendurada nela, completamente intocada em vez de meio sobrescrita. Isso importa porque as ações GoToR e Launch podem ambas estar dentro de uma cadeia /Next construída com AddActionNextRemoteDestinationEx, AddActionNextLaunchEx, ou o mais geral AddActionNextEx, deixando um único gatilho disparar uma entrada de registo JavaScript e depois um salto remoto em sequência. A construção de GoToR, GoToE, e Launch aqui descrita faz parte do PDFlibPas, a biblioteca de PDF nativa para Delphi e C++Builder