Hiperlinks de PDF são anotações de URI: um retângulo cobrindo alguma área da página que, quando clicado, diz ao visualizador para abrir uma URL. A anotação e o texto sob ela são objetos completamente independentes. O PrintHyperlink do HotPDF agrupa ambos em uma chamada, desenhando o texto e calculando o retângulo de anotação a partir das métricas de texto renderizadas. Essa conveniência esconde um detalhe que vale a pena entender antes de escrever o código de produção. Também não é a história toda: o AddURILink coloca uma área clicável sobre o conteúdo que você mesmo desenhou, e o AddGoToLink lida com a navegação interna — ambos abordados abaixo
Como o PrintHyperlink funciona
O PrintHyperlink reside no THPDFPage e recebe quatro argumentos: as coordenadas X e Y (em pontos, origem no canto superior esquerdo, Y aumentando para baixo, com o Y marcando a borda superior do rótulo), a string de rótulo para desenhar e o destino da URL. Internamente ele chama TextOut na cor atual do hiperlink, e então calcula imediatamente o retângulo da anotação a partir de TextWidth e TextHeight nas métricas da fonte atuais. Isso significa que a fonte e o tamanho devem ser definidos antes da chamada, e eles não devem mudar entre o desenho do rótulo e a colocação da anotação, pois ambos são resolvidos na mesma chamada
A cor padrão é clBlue. O SetRGBHyperlinkColor a altera apenas para chamadas subsequentes; ele não atualiza retroativamente anotações já escritas. Se você precisar de cores diferentes para grupos de links diferentes na mesma página, chame o SetRGBHyperlinkColor antes de cada grupo e o redefina em seguida
Aqui está um documento mínimo que escreve três links com duas cores diferentes:
procedure CreateLinkedReport(const FileName: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := FileName;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
// Azul padrão para links informativos
Pdf.CurrentPage.TextOut(50, 50, 0, 'Reference links:');
Pdf.CurrentPage.PrintHyperlink(50, 80, 'Product page', 'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');
Pdf.CurrentPage.PrintHyperlink(50, 105, 'Online manual', 'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');
// Vermelho para o link de ação
Pdf.CurrentPage.SetRGBHyperlinkColor(clRed);
Pdf.CurrentPage.PrintHyperlink(50, 140, 'Purchase license', 'https://www.loslab.com/en-us/buy-hotpdf-fastspring.html');
Pdf.CurrentPage.SetRGBHyperlinkColor(clBlue); // restaura o padrão
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
A armadilha das coordenadas
As chamadas de desenho de página do HotPDF usam uma origem no canto superior esquerdo com o Y crescendo para baixo, em pontos (1/72 polegada), e só convertem para o user space PDF de baixo para cima quando gravam a página. Uma página A4 tem 595 x 842 pt; uma página Carta dos EUA tem 612 x 792 pt. O Y=50 fica próximo ao topo de qualquer uma das duas páginas, e o Y=750 fica próximo à margem inferior de uma página A4. Quem porta código a partir de coordenadas PDF cruas, em que o Y cresce para cima a partir da borda inferior, assume o oposto e coloca a primeira linha de link perto do pé da página
O retângulo de anotação que o PrintHyperlink calcula usa o mesmo sistema de coordenadas. Se mais tarde você rotacionar a página, escalá-la ou alterar o tamanho da página sem recalcular seus valores de X/Y, o texto visível e o retângulo clicável se afastarão. O link "funciona" no sentido de que clicar em algum lugar perto do texto aciona a URL, mas a zona de acesso não corresponde mais ao que o leitor vê. Faça o teste com o tamanho da página e o nível de zoom reais que você entrega, não apenas na máquina de desenvolvimento a 100%
Um caso onde o desvio é garantido: se você chamar o PrintHyperlink com as coordenadas apropriadas para uma página A4 e então mudar para uma página de formato estreito personalizado sem ajustar os valores de X/Y, a anotação pode acabar totalmente fora da página. O objeto da anotação ainda é escrito no PDF; a maioria dos visualizadores o recorta silenciosamente, portanto o link simplesmente desaparece sem nenhum erro
Texto do rótulo versus destino da URL
Os argumentos Text e Link são independentes. Você pode desenhar "Baixar o PDF da fatura" enquanto o destino é uma URL HTTPS totalmente qualificada com parâmetros de consulta. Essa separação é intencional; o rótulo visível deve ser legível por humanos e a URL pode ser longa ou gerada dinamicamente
O que cria problemas é quando o rótulo é a própria URL bruta, especialmente uma longa. Se a URL quebra visualmente em duas linhas mas o retângulo da anotação foi calculado para uma string de linha única, só a primeira linha é clicável. O PrintHyperlink calcula um retângulo para uma linha de texto, então mantenha o rótulo curto o bastante para caber em uma linha no tamanho de fonte e na largura de página atuais, use um rótulo descritivo curto com a URL completa como destino, ou aplique a abordagem por linha mostrada na próxima seção
Para os documentos que serão arquivados ou distribuídos sem uma conexão de internet ativa, considere também se a própria URL deve aparecer de forma impressa em algum lugar no corpo do documento, não apenas como um metadado de anotação. Um leitor que imprime o PDF em papel não obtém nada de uma anotação de URI
Tornando um rótulo de várias linhas clicável
Quando um rótulo de link realmente precisa ocupar mais de uma linha — uma URL longa impressa verbatim, ou uma frase quebrada que deveria ser clicável de ponta a ponta — a abordagem é parar de tratá-lo como um link só e tratá-lo como um link por linha. Cada chamada do PrintHyperlink calcula seu retângulo a partir do texto que desenha, então várias chamadas compartilhando o mesmo destino Link produzem várias anotações dimensionadas corretamente que todas abrem a mesma URL. O leitor não consegue perceber a diferença; toda linha responde ao clique
procedure PrintWrappedHyperlink(Page: THPDFPage; X, TopY, LineStep: Single;
const Lines: array of AnsiString; const Link: AnsiString);
var
I: Integer;
begin
for I := 0 to High(Lines) do
Page.PrintHyperlink(X, TopY + I * LineStep, Lines[I], Link);
end;
// Uso: quebre o rótulo nas posições onde o seu layout o quebra
Pdf.CurrentPage.SetFont('Arial', [], 10);
PrintWrappedHyperlink(Pdf.CurrentPage, 50, 400, 14,
['https://www.loslab.com/en-us/pdf-library/',
'delphi-pdf-component.html'],
'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');
A divisão da string é responsabilidade sua: quebre-a nas mesmas posições onde ela faria a quebra visual na fonte e na largura da coluna atuais, usando TextWidth para testar cada linha candidata. A alternativa é você mesmo desenhar o texto com quebra por meio de chamadas puras de TextOut e então colocar um retângulo AddURILink sobre cada linha — que é a melhor rota quando o texto já é produzido por sua própria lógica de quebra de palavra, o que nos leva a essa função
AddURILink: áreas clicáveis sobre qualquer coisa que você tenha desenhado
O PrintHyperlink é um invólucro de conveniência: ele desenha seu próprio rótulo e deriva o retângulo a partir das métricas desse rótulo. O AddURILink é a metade de nível mais baixo exposta diretamente:
function AddURILink(Rectangle: TRect; const URL: AnsiString;
const Description: AnsiString = ''): THPDFDictionaryObject;
Ele escreve apenas a anotação — nenhum texto é desenhado e as cores não mudam. O Rectangle é interpretado no mesmo espaço de coordenadas de topo esquerdo que as suas chamadas de desenho, de modo que o Top é o menor Y exatamente como em um TRect da VCL, e você pode reutilizar os exatos valores X/Y que você passou para o TextOut ou uma chamada de imagem. Isso faz com que essa seja a ferramenta certa sempre que o conteúdo visível já existir: um hotspot de imagem, uma célula de tabela, um bloco de texto desenhado anteriormente, ou uma linha de um parágrafo quebrado como na solução alternativa acima. A anotação carrega uma borda de largura zero, portanto nada que seja visível muda; a região clicável é exatamente o retângulo que você especificar
A função retorna o dicionário de anotação como um THPDFDictionaryObject. A maioria dos chamadores descarta o resultado, mas mantê-lo permite que você ajuste as entradas da anotação antes que o documento seja escrito
Dois detalhes de conformidade já vêm embutidos. Nos modos PDF/A o print flag da anotação é definido como esses padrões exigem. Sob PDFUACompliance o parâmetro Description deve ser uma string não vazia — ela se torna a entrada /Contents da anotação, que é o que a tecnologia assistiva anuncia para o link — e a chamada levanta uma exceção em vez de emitir silenciosamente um arquivo fora de conformidade. O PrintHyperlink não anexa descrição, então para saída em PDF/UA desenhe o rótulo com TextOut e posicione a anotação com AddURILink mais uma descrição com significado
A regra de decisão é simples: use PrintHyperlink quando o link for um texto curto que você ainda não desenhou; use AddURILink quando a região clicável for definida pelo conteúdo que você mesmo desenha ou mede
Navegação interna com o AddGoToLink
As URLs externas são apenas a metade do que as anotações de link fazem. A outra metade é a navegação dentro do documento — um índice que salta para capítulos, referências cruzadas entre as seções. O HotPDF expõe isso por meio de AddGoToLink:
procedure AddGoToLink(Rectangle: TRect; TargetPageIndex: Integer;
YPos: Single = -1; const Description: AnsiString = '');
Vale a pena declarar três semânticas com precisão, uma vez que nenhuma é adivinhável a partir da assinatura. O TargetPageIndex é baseado em zero: a primeira página do documento é a página 0, correspondendo ao CurrentPageNumber. Um índice negativo faz o procedimento retornar sem adicionar uma anotação — nenhuma exceção, nenhum link, nenhum aviso. Em um documento novo, uma página de destino que ainda não existe não é um erro: o link fica retido e é resolvido quando o EndDoc roda, de modo que um sumário pode apontar para frente em direção a páginas adicionadas depois. Se essa página nunca aparecer, a anotação ainda é escrita, mas perde a ação — um retângulo que não faz nada ao clicar, de novo sem aviso; em um documento carregado, o destino já deve existir. Criar as páginas primeiro e depois voltar, como o exemplo abaixo faz, mantém cada link verificável no momento em que você o adiciona
O YPos seleciona a posição vertical na página de destino, no mesmo espaço de coordenadas que as suas chamadas de desenho. O padrão de -1 (qualquer valor negativo) grava uma coordenada de destino nula, dizendo ao visualizador para manter a sua posição vertical atual quando ele pousar na página de destino. Passe um valor não negativo e a visualização rola para que a posição fique no topo da janela — use a coordenada Y do cabeçalho ao qual você está se vinculando. O zoom sempre é mantido inalterado. Assim como com o AddURILink, o Description não deve estar vazio sob o PDFUACompliance e se torna o texto alternativo do link
procedure BuildLinkedTOC(const FileName: string);
const
Chapters: array[0..2] of string =
('Introduction', 'Installation', 'API Reference');
var
Pdf: THotPDF;
I, Y: Integer;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := FileName;
Pdf.BeginDoc; // a página 0 se torna a página de sumário
// Cria as páginas de capítulo primeiro para que os alvos dos links existam
for I := 0 to High(Chapters) do
begin
Pdf.AddPage; // páginas 1..3
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 50, 0, Chapters[I]);
end;
// Volta para a página 0 e desenha as entradas do sumário com seus links
Pdf.CurrentPageNumber := 0;
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 40, 0, 'Contents');
Pdf.CurrentPage.SetFont('Arial', [], 11);
Y := 80;
for I := 0 to High(Chapters) do
begin
Pdf.CurrentPage.TextOut(70, Y, 0, Chapters[I]);
Pdf.CurrentPage.AddGoToLink(
Rect(70, Y - 3, 300, Y + 14), // cobre a entrada com um espaçamento extra
I + 1, // baseado em zero: os capítulos são as páginas 1..3
50, // chega com o título no topo
AnsiString('Go to ' + Chapters[I]));
Y := Y + 25;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Cada entrada obtém um retângulo mais largo do que o texto para que a linha inteira responda ao ponteiro, e todos os links chegam com o cabeçalho do capítulo (desenhado em Y=50) no topo da janela. Se mais tarde você inserir uma página antes dos capítulos, cada TargetPageIndex muda em um; calcule os índices a partir do seu loop de criação de páginas, em vez de codificá-los manualmente
Um exemplo completo de geração de documento
O padrão abaixo mostra um cenário mais realista: gerar um relatório curto com uma seção de cabeçalho, corpo de texto e uma linha de rodapé com links, tudo a partir do código, em vez de um formulário com campos TEdit:
procedure GenerateProductSheet(
const FileName, ProductName, ProductURL, SupportURL: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Compression := cmFlateDecode;
Pdf.BeginDoc;
// Cabeçalho
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 50, 0, WideString(ProductName));
// Placeholder do parágrafo de corpo
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 90, 0, 'See the links below for full documentation.');
// Links de rodapé
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 750, 0, 'Links:');
Pdf.CurrentPage.PrintHyperlink(50, 770, 'Product page', ProductURL);
Pdf.CurrentPage.PrintHyperlink(200, 770, 'Support', SupportURL);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Observe que o SetFont é chamado antes de cada grupo de chamadas de texto. A fonte não persiste no AddPage, e se você esquecer de configurá-la antes do PrintHyperlink em uma nova página, o retângulo de anotação será calculado em relação às métricas padrão da página, o que pode diferir do que você espera
Onde o tratamento de anotações varia entre os visualizadores
As anotações de URI de PDF estão definidas na ISO 32000-1 §12.6.4.7, e todo visualizador em conformidade deve segui-las. Na prática, alguns comportamentos diferem de acordo com o visualizador. O Adobe Acrobat exibe um prompt de segurança no primeiro clique para URLs que não constam da lista de domínios confiáveis; muitos navegadores e leitores leves não fazem isso. Alguns visualizadores de PDF corporativos em ambientes bloqueados desabilitam as anotações de URI inteiramente por política; portanto, um clique não faz nada, sem erro visível. Os aplicativos de PDF em dispositivos móveis variam em abrir links na visualização da web do aplicativo ou em transferi-los para o navegador do sistema
Nenhum desses são bugs que você pode corrigir do lado da geração; são decisões de políticas do visualizador. O que você pode fazer é escrever rótulos de link que tornem a URL visível também no corpo do documento; assim, um leitor num ambiente restrito ainda poderá copiar o endereço manualmente. A anotação é a conveniência; o texto é a contingência
Um detalhe a mais que vale a pena conhecer: as anotações URI de PDF não carregam nenhum sublinhado visual por padrão. O sublinhado que você vê na maioria dos visualizadores é desenhado pelo próprio visualizador com base no tipo de anotação, não por um glifo no fluxo de conteúdo. Se você precisa de um sublinhado físico que sobreviva à impressão para um renderizador não interativo ou para uma conversão de PDF para imagem, desenhe-o explicitamente com LineTo e Stroke no deslocamento Y apropriado, abaixo da linha de base do texto. Trata-se de uma operação de desenho separada, não de algo que o PrintHyperlink controla por você
A API de hiperlinks mostrada aqui faz parte do Componente HotPDF para Delphi e C++Builder