O HotPDF adiciona bookmarks (marcadores) a um PDF existente no Delphi por meio do método AddLoadedOutline, que cria entradas de estrutura de tópicos em qualquer nível de aninhamento em um documento carregado, além de ler e gravar destinos nomeados com o ResolveLoadedNamedDestination e AddLoadedNamedDestination. Carregue o arquivo, construa a árvore, chame o SaveLoadedDocument e a barra lateral do leitor, que antes ficava em branco, passará a exibir um índice funcional
O cenário que motiva tudo isso é lamentavelmente comum. Você mescla uma dúzia de contratos em um único pacote de revisão ou junta três manuais de produtos em um único arquivo de distribuição, e la saída é estruturalmente perfeita: todas as páginas presentes, todas as fontes intactas. Então, alguém o abre no Acrobat e o painel de bookmarks está vazio. Um documento de 400 páginas sem navegação está tecnicamente completo, mas é praticamente inutilizável. Até a versão 2.347.0, o HotPDF podia ler, modificar e excluir bookmarks em documentos carregados, mas nunca criá-los. Essa lacuna agora foi preenchida, e este artigo aborda a nova interface da API e um detalhe obscuro do PDF, a chave /Count, que causará problemas se você a tratar como um simples contador de entradas
O que realmente é a árvore de estrutura de tópicos
Uma estrutura de tópicos (outline) de PDF é uma árvore duplamente encadeada de dicionários, não uma lista linear, e essa estrutura explica cada parâmetro da API. A norma ISO 32000-1 §12.3.3 a define: o catálogo do documento aponta para uma raiz /Outlines, a raiz aponta para o seu primeiro e último filhos via /First e /Last, e os irmãos se encadeiam através de /Next e /Prev, enquanto cada entrada aponta de volta para cima via /Parent. Cada entrada carrega uma string /Title e, normalmente, um /Dest que indica para onde o clique deve levar o leitor
Os destinos vêm em dois tipos, e a norma ISO 32000-1 §12.3.2 os mantém deliberadamente separados. Um destino explícito é um array inline como [page /XYZ x y zoom]: uma referência direta a um objeto de página junto com uma especificação de visualização. Já um destino nomeado armazena uma string de nome, e o destino real reside no catálogo do documento sob a árvore de nomes /Names /Dests, um nível de indireção que permite que muitos links compartilhem um mesmo destino e permite que o destino mude de posição sem a necessidade de alterar os links. O HotPDF grava destinos /XYZ explícitos ao criar bookmarks e oferece chamadas separadas para ler e estender a árvore de nomes
Como adicionar bookmarks a um PDF existente no Delphi?
Chame AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) após o LoadFromFile: passe ParentIndex = -1 para uma entrada de nível superior, ou o índice baseado em zero de uma entrada de nível superior existente para aninhá-la sob esta. A função cria a raiz /Outlines caso o documento nunca tenha tido uma, gera o dicionário de entrada, grava um array /Dest [page /XYZ x y zoom] apontando para a DestPageIndex (baseada em zero) e vincula a nova entrada na cadeia de irmãos. Ela retorna 0 em caso de sucesso e -1 em caso de falha, por exemplo, se a DestPageIndex estiver fora do intervalo
Um detalhe de encadeamento importante para a ordenação: uma nova entrada é inserida no início da cadeia de filhos do seu pai, como o novo /First. Portanto, se você adicionar o "Capítulo 1" e depois o "Capítulo 2" como entradas de nível superior, o Capítulo 2 aparecerá acima do Capítulo 1 na barra lateral, e o Capítulo 1 será deslocado para o índice 1 do nível superior. O padrão prático é adicionar as entradas em ordem inversa de leitura, ou adicionar cada pai e imediatamente preencher seus filhos enquanto ele ainda estiver no índice 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Adiciona capítulos em ordem inversa de leitura: cada nova
// entrada de nível superior torna-se /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á agora no índice 0 do nível superior; aninha seções
// sob ele (os filhos também são inseridos no início, portanto, 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 são mapeados diretamente no destino /XYZ e todos têm valor padrão 0. O PDF define a origem das coordenadas no canto inferior esquerdo, de modo que Y = 792 aponta para o topo de uma página do tamanho Carta (US Letter) e, de acordo com a norma ISO 32000-1, um valor zero em um slot /XYZ indica ao visualizador que mantenha sua configuração atual, que é exatamente o que se deseja para o zoom na maioria dos casos. Tudo ocorre no grafo de objetos carregados na memória; nada é gravado no disco até que o SaveLoadedDocument seja executado, o mesmo modelo de editar e depois salvar que a API de documentos carregados usa para reescrever título, autor e metadados XMP em PDFs carregados
Redirecionando bookmarks: saltos de página versus ações de URI
Os bookmarks existentes podem ser reconectados sem a necessidade de recriá-los, e o HotPDF oferece duas chamadas distintas para esses casos porque as estruturas subjacentes do PDF são realmente diferentes. O método SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) substitui o /Dest da entrada de nível superior em Index por um novo array /XYZ, que é a ferramenta ideal para a falha clássica após a mesclagem onde cada bookmark continua apontando para os números de página do arquivo original antes da mesclagem. Por sua vez, o SetLoadedOutlineURI(Index, URI) faz algo estruturalmente diferente: anexa um dicionário de ação /A com /S /URI, transformando o bookmark em um link da web em vez de um salto interno no documento
// A mesclagem deslocou tudo em 10 páginas: aponta novamente o
// terceiro bookmark de nível superior para a página 12 (índice baseado em zero 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Transforma a última entrada em um link externo.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
Mantenha os dois mecanismos bem definidos ao projetar a árvore. Um /Dest é pura navegação interna no documento; uma ação de URI sai completamente do documento, e a norma ISO 32000-1 espera que uma entrada da estrutura de tópicos navegue por um mecanismo ou por outro, não por ambos. Portanto, reserve o SetLoadedOutlineURI para entradas que ainda não possuam um destino de página, como uma folha "Página inicial do produto" ou "Relatar um problema" no final da árvore, em vez de um título de capítulo convertido. A ação de URI gravada aqui é a mesma estrutura /S /URI usada para anotações de link no conteúdo de página, abordada em mais detalhes no guia para criar hiperlinks em documentos PDF com o HotPDF
Como funcionam os destinos nomeados em um PDF carregado?
Os destinos nomeados constituem o mecanismo de âncoras estáveis da norma ISO 32000-1 §12.3.2.3: em vez de incorporar uma referência de página em cada link, the documento mantém um mapa de nome para destino na árvore de nomes /Names /Dests do catálogo, e os links fazem referência às entradas por nome. O método ResolveLoadedNamedDestination(Name) pesquisa um nome nessa árvore e retorna o índice de página baseado em zero para o qual ele aponta, ou -1 se o nome estiver ausente. O resolvedor trata ambos os formatos de armazenamento que a especificação permite para o valor mapeado: um array de destino simples como [page /XYZ x y zoom] e a estrutura de dicionário que envolve o array sob uma chave /D
O método AddLoadedNamedDestination(Name, DestPageIndex, X, Y) faz o caminho inverso: ele registra uma nova âncora na árvore /Names /Dests, criando a própria árvore quando o documento não possui nenhuma, e retorna True em caso de sucesso. Juntas, as duas chamadas cobrem o fluxo de trabalho onde um documento mesclado deve honrar as âncoras gravadas pelas ferramentas anteriores e publicar novas para que links posteriores possam apontar para elas
var
PageIdx: Integer;
begin
// Honra uma âncora que outra ferramenta gravou em /Names /Dests.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Publica uma nova âncora para outros links apontarem.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
Os limites honestos: os destinos que o HotPDF grava, tanto para bookmarks quanto para âncoras nomeadas, são do tipo /XYZ. Os outros tipos explícitos definidos pela especificação, como /Fit, /FitH, /FitB e similares, não são gerados por essas chamadas, embora /XYZ com zoom zero atenda ao caso de uso principal de "ir para este ponto, mantendo o meu zoom". Além disso, o método GetLoadedBookmarkPageIndex(Title), a consulta conveniente que recebe o título de um bookmark e retorna sua página de destino, varre apenas o nível superior da árvore de estrutura de tópicos, portanto, utilize-o para verificar as entradas de nível de capítulo que você acabou de criar, e não folhas profundamente aninhadas
A armadilha do /Count: ele conta descendentes, não entradas
A chave /Count na raiz da estrutura de tópicos não significa o que a maioria dos desenvolvedores supõe, e a premissa incorreta produz falhas de teste que parecem corrupção de dados. A especificação ISO 32000-1 §12.3.3 define /Count como o número total de descendentes visíveis em todos os níveis, não a quantidade de entradas do nível superior. Em uma entrada individual, o sinal também carrega significado: um valor positivo indica que a entrada está aberta e essa quantidade de descendentes aparece na barra lateral, enquanto um valor negativo significa que a entrada está recolhida e |N| descendentes estão ocultos dentro dela
Isso foi identificado na própria suíte de testes de regressão do HotPDF durante o desenvolvimento das APIs de bookmark para documentos carregados. Um documento de teste continha três bookmarks de nível superior, um deles com um filho; após excluir uma entrada do nível superior, o método GetLoadedOutlineCount, que lê o /Count raiz, retornou 1 em caso de sucesso 2. Nada foi corrompido: o 3 inicial nunca havia sido "três entradas de nível superior" e o recálculo correto deve percorrer a cadeia do nível superior acumulando cada nó mais o seu /Count positivo, ignorando os descendentes de nós recolhidos. A lição para o seu código é direta: nunca faça asserções em variações de /Count após edições estruturais, porque o valor varia de forma não linear com os descendentes visíveis. Em vez disso, verifique a estrutura pelo conteúdo
// Após as edições, verifique por busca de título, não pelas variações do /Count.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
Onde isso se encaixa no conjunto de ferramentas de documentos carregados
A criação de bookmarks e destinos nomeados completa um panorama que a API de documentos carregados vem preenchendo há algum tempo: o mesmo ciclo de carregar, editar e salvar já reescreve os metadados do documento, adiciona anotações de link e realiza o fluxo completo de importação e exportação de anotações via XFDF. O padrão é uniforme: LoadFromFile, uma sequência de chamadas *Loaded* no grafo de objetos na memória, e em seguida um SaveLoadedDocument, o que torna simples integrar a construção da estrutura de tópicos em uma pipeline existente de mesclagem ou carimbagem como mais um passo antes de salvar
As APIs discutidas aqui, AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination e GetLoadedBookmarkPageIndex, são fornecidas no HotPDF Component padrão para Delphi e C++Builder, sem dependências externas e sem necessidade de runtime de visualizador separado