O HotPDF permite adicionar marcadores a ficheiros PDF existentes em Delphi através do método AddLoadedOutline, que cria entradas no índice em qualquer nível de aninhamento num documento carregado. O componente também lê e escreve destinos nomeados (named destinations) via ResolveLoadedNamedDestination e AddLoadedNamedDestination. Após carregar o ficheiro, definir a estrutura e invocar SaveLoadedDocument, a barra lateral do visualizador passa a exibir um índice ativo
A necessidade destas operações é frequente: ao fundir uma dezena de contratos num pacote de revisão ou juntar manuais de produto num único ficheiro, o resultado gráfico apresenta-se correto, com todas as páginas e tipos de letra preservados. Contudo, ao carregar o ficheiro no Acrobat, o painel de marcadores encontra-se vazio. Um documento com 400 páginas sem navegação torna-se disfuncional. Até à versão 2.347.0, o HotPDF permitia ler, alterar e eliminar marcadores em documentos carregados, mas não criá-los do zero. Esta limitação foi superada; este artigo descreve a nova API e detalha uma particularidade da especificação PDF, o marcador /Count, que não deve ser tratado como um simples contador de linhas
A estrutura da árvore de índices (outlines)
O índice (outline) num PDF é estruturado como uma árvore de dicionários duplamente ligada (e não uma lista linear), o que justifica os argumentos da API. A norma ISO 32000-1 §12.3.3 define este comportamento: o catálogo do documento aponta para a raiz /Outlines, que por sua vez referencia o primeiro e o último nós filhos através de /First e /Last, ligando-se os elementos irmãos via /Next e /Prev, apontando cada entrada para o respetivo elemento pai por /Parent. Cada nó contém a string /Title e, habitualmente, a propriedade /Dest com as coordenadas de destino do clique
Os destinos dividem-se em dois tipos, especificados na secção 12.3.2 de ISO 32000-1. Um destino explícito consiste num array em linha (como [page /XYZ x y zoom]) com a referência direta ao objeto da página e respetivos parâmetros de visualização. Por outro lado, um destino nomeado guarda uma string de identificação, e o destino real reside no catálogo do documento sob a árvore /Names /Dests; esta camada de indireção permite que múltiplos links partilhem o mesmo destino, e que este seja alterado sem atualizar as referências individuais. O HotPDF escreve destinos explícitos /XYZ ao criar marcadores, disponibilizando métodos específicos para ler e expandir a árvore de nomes
Como adicionar marcadores a um PDF existente em Delphi?
Invoque o método AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) após a chamada a LoadFromFile: passe ParentIndex = -1 para criar uma entrada na raiz, ou o índice baseado em 0 de uma entrada existente para a aninhar. A função cria a raiz /Outlines caso esta não exista, monta o dicionário correspondente, escreve o array /Dest [page /XYZ x y zoom] apontando para o índice zero de DestPageIndex e insere o nó na hierarquia. A chamada devolve 0 em caso de sucesso e -1 em caso de falha (por exemplo, se DestPageIndex for inválido)
A ordenação das entradas segue uma regra específica: cada novo nó é inserido no início da lista de filhos, assumindo o valor /First. Por conseguinte, se adicionar o "Capítulo 1" e depois o "Capítulo 2" na raiz, o Capítulo 2 será exibido acima do Capítulo 1, passando este último para o índice 1. A boa prática consiste em inserir as entradas por ordem inversa de leitura, ou em adicionar o elemento pai e preencher imediatamente os respetivos filhos enquanto este se encontrar no índice 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Adicionar capítulos por ordem inversa: cada nova
// entrada na raiz assume a posição /First (índice 0)
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// O Capítulo 1 está no índice 0; aninhar secções
// sob este (filhos também são inseridos no início, logo ordem inversa)
Pdf.AddLoadedOutline(0, 12, 'License activation', 0, 792, 0);
Pdf.AddLoadedOutline(0, 3, 'System requirements', 0, 792, 0);
Pdf.SaveLoadedDocument('merged-manual-toc.pdf');
end;
finally
Pdf.Free;
end;
end;
Os parâmetros X, Y e Zoom correspondem aos argumentos do destino /XYZ e são inicializados com 0. A especificação PDF situa a origem de coordenadas no canto inferior esquerdo; logo, Y = 792 aponta para o topo de uma página Carta americana (Letter), e de acordo com a norma ISO 32000-1 o valor zero nestes argumentos instrui o visualizador a manter a escala atual do ecrã. O processamento ocorre inteiramente na árvore de objetos em memória, gravando-se no disco apenas com a chamada a SaveLoadedDocument, de acordo com o modelo de edição abordado no artigo sobre edição de metadados XMP em PDFs
Redirecionar marcadores: saltos de página e ações URI
É possível atualizar os marcadores existentes sem os recriar. O HotPDF disponibiliza dois métodos distintos uma vez que a estrutura interna do PDF difere para ambos os cenários. A chamada SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) substitui a propriedade /Dest do nó indicado em Index por um novo array /XYZ (resolvendo falhas comuns pós-fusão em que os marcadores apontavam para os índices dos ficheiros de origem). Por outro lado, SetLoadedOutlineURI(Index, URI) associa um dicionário de ação /A com /S /URI, convertendo o marcador numa ligação externa em vez de um salto interno
// A fusão deslocou os índices em 10 páginas: reposicionar o
// terceiro marcador da raiz para a página 12 (índice 11)
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Converter a última entrada numa ligação externa
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
Mantenha a distinção entre os dois fluxos na sua aplicação. O parâmetro /Dest serve a navegação interna; uma ação URI remete para o exterior do ficheiro, e a especificação ISO 32000-1 prevê que cada marcador utilize apenas um dos mecanismos. Assim, reserve SetLoadedOutlineURI para entradas que não possuam destinos internos (como links para "Página do produto" ou "Suporte" ao final do índice, em vez de títulos de secções). O comportamento das ações URI é equivalente ao das anotações de hiperligação descritas no guia de criação de hiperligações com o HotPDF
Como funcionam os destinos nomeados num PDF carregado?
Os destinos nomeados constituem o mecanismo de ancoragem estável definido em ISO 32000-1 §12.3.2.3: em vez de codificar referências de página em cada ligação, o documento mantém uma tabela de mapeamento no catálogo sob /Names /Dests, e as ligações referenciam estes registos por nome. A chamada ResolveLoadedNamedDestination(Name) pesquisa na árvore e devolve o índice da página correspondente (ou -1 se o nome for inexistente). O método interpreta ambas as representações previstas na norma: um array de destino simples (como [page /XYZ x y zoom]) ou a variante em dicionário que encapsula o array sob o identificador /D
O método AddLoadedNamedDestination(Name, DestPageIndex, X, Y) realiza a operação inversa: regista uma nova âncora em /Names /Dests (criando a árvore caso esta não exista) e devolve True em caso de sucesso. Esta dupla permite validar as âncoras gravadas por outros utilitários e publicar novos destinos para indexação
var
PageIdx: Integer;
begin
// Respeitar uma âncora gravada anteriormente em /Names /Dests
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Publicar uma nova âncora para indexação por outros links
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
Limitações técnicas: os destinos gerados pelo HotPDF baseiam-se em coordenadas /XYZ. Variantes explícitas previstas na especificação (como /Fit, /FitH e /FitB) não são emitidas por estas chamadas, embora a definição /XYZ com zoom zero resolva o requisito comum de focar uma coordenada mantendo a escala. Adicionalmente, a função de conveniência GetLoadedBookmarkPageIndex(Title) (que pesquisa a página de destino de um marcador pelo título) valida apenas o nível superior do índice, sendo indicada para testar capítulos e não sub-secções aninhadas
A armadilha do parâmetro /Count: contagem de descendentes e não de entradas
O valor contido na chave /Count da raiz do índice não representa o que a maioria dos programadores assume; conceções erradas nesta lógica geram falhas em testes simulando corrupção de dados. A norma ISO 32000-1 §12.3.3 define /Count como a contagem de todos os descendentes visíveis em todos os níveis e não apenas o número de entradas primárias. Em nós individuais, o sinal matemático também tem significado: valores positivos indicam que o nó está expandido e exibe esse número de sub-elementos na barra lateral; valores negativos indicam que o nó está contraído e oculta |N| sub-elementos
Este comportamento foi detetado durante os testes de regressão das APIs de marcadores do HotPDF. Um documento com três marcadores primários, um dos quais com um nó filho, reportava o valor 1 em GetLoadedOutlineCount (que lê a propriedade /Count) após a eliminação de um item primário, em vez dos 2 esperados. A estrutura não estava corrompida: o valor inicial 3 nunca representou o total de entradas primárias; o cálculo correto deve percorrer a raiz somando cada nó e respetivos valores de /Count positivos, ignorando descendentes de nós contraídos. Evite, portanto, efetuar validações com base em diferenças de /Count após alterações estruturais, dado que o valor varia de forma não linear em função da visibilidade. Valide a integridade com base no conteúdo:
// Após edições, valide por pesquisa de título e não por diferenças de /Count
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Integração na API de edição de documentos carregados
As APIs descritas neste artigo (AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination e GetLoadedBookmarkPageIndex) são fornecidas com a versão padrão do HotPDF Component para Delphi e C++Builder, sem dependências externas nem necessidade de visualizadores adicionais