As hiperligações PDF são anotações URI: um retângulo que cobre uma área da página e que, quando clicado, indica ao visualizador para abrir um URL. A anotação e o texto por baixo dela são objetos completamente independentes. O PrintHyperlink do HotPDF reúne ambos numa única chamada, desenhando o texto e calculando o retângulo da anotação a partir das métricas do texto processado. Essa conveniência esconde um pormenor que vale a pena compreender antes de escrever código de produção. E não é toda a história: o AddURILink coloca uma área clicável sobre conteúdo desenhado pelo próprio programador, e o AddGoToLink trata da navegação interna — ambos abordados a seguir
Como funciona o PrintHyperlink
O PrintHyperlink reside em THPDFPage e recebe quatro argumentos: as coordenadas X e Y (em pontos, com origem no canto inferior esquerdo, Y a crescer para cima), a string do rótulo a desenhar e o URL de destino. Internamente, chama o TextOut com a cor de hiperligação atual e, de seguida, calcula de imediato o retângulo da anotação a partir de TextWidth e TextHeight segundo as métricas do tipo de letra atual. Isto significa que o tipo de letra e o tamanho têm de ser definidos antes da chamada, e não podem mudar entre o desenho do rótulo e a colocação da anotação, porque ambos são resolvidos na mesma chamada
A cor predefinida é clBlue. O SetRGBHyperlinkColor altera-a apenas para as chamadas seguintes; não atualiza retroativamente as anotações já escritas. Se forem necessárias cores diferentes para grupos de ligações distintos na mesma página, deve chamar-se o SetRGBHyperlinkColor antes de cada grupo e repor o valor depois
Segue-se um documento mínimo que escreve três ligações 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 predefinido para ligações informativas
Pdf.CurrentPage.TextOut(50, 750, 0, 'Reference links:');
Pdf.CurrentPage.PrintHyperlink(50, 720, 'Product page', 'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');
Pdf.CurrentPage.PrintHyperlink(50, 695, 'Online manual', 'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');
// Vermelho para a ligação de ação
Pdf.CurrentPage.SetRGBHyperlinkColor(clRed);
Pdf.CurrentPage.PrintHyperlink(50, 660, 'Purchase license', 'https://www.loslab.com/en-us/buy-hotpdf-fastspring.html');
Pdf.CurrentPage.SetRGBHyperlinkColor(clBlue); // repor predefinição
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
A armadilha das coordenadas
O HotPDF utiliza uma origem no canto inferior esquerdo, com o Y a crescer para cima, em pontos (1/72 de polegada). Uma página A4 tem 595 x 842 pt; uma página US Letter tem 612 x 792 pt. Y=750 fica perto do topo de uma página A4, e Y=50 ficaria perto da margem inferior. Quem vem de gráficos de ecrã ou de HTML assume o oposto e coloca a primeira linha de ligação completamente fora da área visível
O retângulo da anotação calculado pelo PrintHyperlink utiliza o mesmo sistema de coordenadas. Se a página for depois rodada, escalada ou tiver o tamanho alterado sem recalcular os valores X/Y, o texto visível e o retângulo clicável vão desalinhar-se. A ligação "funciona" no sentido em que clicar perto do texto ainda ativa o URL, mas a zona sensível já não corresponde ao que o leitor vê. Deve testar-se com o tamanho de página e o nível de zoom reais que serão distribuídos, não apenas na máquina de desenvolvimento a 100%
Há um caso em que o desalinhamento é garantido: se o PrintHyperlink for chamado com coordenadas adequadas a uma página A4 e depois se mudar para uma página personalizada de formato estreito sem ajustar os valores X/Y, a anotação pode acabar completamente fora da página. O objeto de anotação continua a ser escrito no PDF; a maioria dos visualizadores corta-o silenciosamente, pelo que a ligação simplesmente desaparece sem qualquer erro
Texto do rótulo versus URL de destino
Os argumentos Text e Link são independentes. É possível desenhar "Transferir fatura em PDF" enquanto o destino é um URL HTTPS totalmente qualificado com parâmetros de consulta. Essa separação é deliberada; o rótulo visível deve ser legível por humanos e o URL pode ser longo ou gerado dinamicamente
O que cria problemas é quando o próprio rótulo é o URL em bruto, sobretudo se for longo. Se o URL quebrar visualmente em duas linhas mas o retângulo da anotação tiver sido calculado para uma string de uma só linha, apenas a primeira linha fica clicável. O PrintHyperlink não trata o fluxo em várias linhas; convém manter o rótulo suficientemente curto para caber numa linha, dado o tamanho de tipo de letra e a largura de página atuais, usar um rótulo descritivo curto com o URL completo como destino, ou aplicar a solução linha a linha apresentada na secção seguinte
Para documentos que serão arquivados ou distribuídos sem ligação ativa à internet, convém também considerar se o próprio URL deve aparecer sob forma impressa em algum ponto do corpo do documento, e não apenas como metadados da anotação. Um leitor que imprima o PDF em papel não obtém nada de uma anotação URI
Contornar a limitação de múltiplas linhas
Quando um rótulo de ligação tem mesmo de ocupar mais do que uma linha — um URL longo impresso literalmente, ou uma frase quebrada que deve ficar clicável do início ao fim — a solução passa por deixar de o tratar como uma única ligação e passar a tratá-lo como uma ligação por linha. Cada chamada a PrintHyperlink calcula o seu retângulo a partir do texto que desenha, pelo que várias chamadas com o mesmo destino Link produzem várias anotações corretamente dimensionadas que abrem todas o mesmo URL. O leitor não nota qualquer diferença; cada linha responde a um 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;
// Utilização: quebrar o rótulo nas posições onde o esquema 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 é da responsabilidade do programador: deve quebrar-se nas mesmas posições onde o texto quebraria visualmente para o tipo de letra e a largura de coluna atuais, usando TextWidth para testar cada linha candidata. A alternativa é desenhar o texto quebrado com chamadas simples a TextOut e depois sobrepor um retângulo AddURILink a cada linha — o caminho preferível quando o texto já é produzido pela lógica própria de quebra de linha, o que nos leva a essa função
AddURILink: áreas clicáveis sobre qualquer conteúdo desenhado
O PrintHyperlink é um invólucro de conveniência: desenha o 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;
Escreve apenas a anotação — não é desenhado texto nem há alteração de cor. O Rectangle é interpretado no mesmo espaço de coordenadas das chamadas de desenho, pelo que é possível reutilizar exatamente os valores X/Y passados a TextOut ou a uma chamada de imagem. Isso torna-o a ferramenta certa sempre que o conteúdo visível já existe: uma área sensível de uma imagem, uma célula de tabela, um bloco de texto desenhado anteriormente, ou uma linha de um parágrafo quebrado como na solução acima. A anotação tem um contorno de largura zero, pelo que nada visível muda; a região clicável é exatamente o retângulo especificado
A função devolve o dicionário da anotação como um THPDFDictionaryObject. A maioria dos chamadores descarta o resultado, mas guardá-lo permite ajustar as entradas da anotação antes de o documento ser escrito
Há dois pormenores de conformidade incorporados. Nos modos PDF/A, a flag de impressão da anotação é definida conforme exigido por essas normas. Sob PDFUACompliance, o parâmetro Description tem de ser uma string não vazia — torna-se a entrada /Contents da anotação, que é o que a tecnologia de apoio anuncia para a ligação — e a chamada gera uma exceção em vez de emitir silenciosamente um ficheiro não conforme. O PrintHyperlink é anterior a essa regra e não anexa qualquer descrição, pelo que, para saída PDF/UA, deve desenhar-se o rótulo com TextOut e colocar a anotação com AddURILink mais uma descrição significativa
A regra de decisão é simples: usar PrintHyperlink quando a ligação é um pequeno trecho de texto ainda não desenhado; usar AddURILink quando a região clicável é definida por conteúdo desenhado ou medido pelo próprio programador
Navegação interna com AddGoToLink
Os URLs externos são apenas metade do que as anotações de ligação fazem. A outra metade é a navegação dentro do documento — um índice que salta para capítulos, referências cruzadas entre secções. O HotPDF expõe isto através de AddGoToLink:
procedure AddGoToLink(Rectangle: TRect; TargetPageIndex: Integer;
YPos: Single = -1; const Description: AnsiString = '');
Vale a pena precisar três semânticas, uma vez que nenhuma delas é dedutível a partir da assinatura. O TargetPageIndex começa em zero: a primeira página do documento é a página 0, correspondendo a CurrentPageNumber. A página de destino tem de existir já no momento da chamada; se o índice estiver fora do intervalo, o procedimento retorna sem adicionar qualquer anotação — sem exceção, sem ligação, sem aviso. Para um índice que aponta para a frente, deve criar-se primeiro todas as páginas e só depois voltar atrás e adicionar as ligações
O YPos seleciona a posição vertical na página de destino, no mesmo espaço de coordenadas das chamadas de desenho. O valor predefinido de -1 (qualquer valor negativo) escreve uma coordenada de destino nula, indicando ao visualizador para manter a posição vertical atual ao chegar à página de destino. Se for passado um valor não negativo, o visualizador desloca-se de modo a que essa posição fique no topo da janela — deve usar-se a coordenada Y do título para o qual se está a criar a ligação. O zoom nunca é alterado. Tal como com AddURILink, o Description tem de ser não vazio sob PDFUACompliance e torna-se o texto alternativo da ligação
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 torna-se a página do índice
// Criar primeiro as páginas dos capítulos para que os destinos das ligações 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, 780, 0, Chapters[I]);
end;
// Voltar à página 0 e desenhar as entradas do índice com as respetivas ligações
Pdf.CurrentPageNumber := 0;
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Contents');
Pdf.CurrentPage.SetFont('Arial', [], 11);
Y := 720;
for I := 0 to High(Chapters) do
begin
Pdf.CurrentPage.TextOut(70, Y, 0, Chapters[I]);
Pdf.CurrentPage.AddGoToLink(
Rect(70, Y + 14, 300, Y - 3), // cobre a entrada com espaçamento
I + 1, // começa em zero: os capítulos são as páginas 1..3
780, // 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 recebe um retângulo mais largo do que o texto, para que toda a linha responda ao ponteiro, e cada ligação chega com o título do capítulo (desenhado em Y=780) no topo da janela. Se mais tarde for inserida uma página antes dos capítulos, cada TargetPageIndex desloca-se uma unidade; convém calcular os índices a partir do ciclo de criação de páginas em vez de os fixar diretamente no código
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 secção de cabeçalho, texto de corpo e uma linha de rodapé com ligações, tudo a partir de código em vez de a partir 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, 750, 0, WideString(ProductName));
// Marcador de posição do parágrafo de corpo
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');
// Ligações de rodapé
Pdf.CurrentPage.SetFont('Arial', [], 10);
Pdf.CurrentPage.TextOut(50, 80, 0, 'Links:');
Pdf.CurrentPage.PrintHyperlink(50, 60, 'Product page', ProductURL);
Pdf.CurrentPage.PrintHyperlink(200, 60, 'Support', SupportURL);
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Note-se que SetFont é chamado antes de cada grupo de chamadas de texto. O tipo de letra não persiste entre chamadas a AddPage, e se não for definido antes de PrintHyperlink numa página nova, o retângulo da anotação será calculado com base nas métricas predefinidas da página, que podem diferir do esperado
Onde o tratamento de anotações varia entre visualizadores
As anotações URI em PDF estão definidas na norma ISO 32000-1 §12.6.4.7, e qualquer visualizador conforme deve segui-las. Na prática, alguns comportamentos variam consoante o visualizador. O Adobe Acrobat mostra um aviso de segurança no primeiro clique para URLs que não constem da lista de domínios de confiança; muitos browsers e leitores leves não o fazem. Alguns visualizadores PDF empresariais, em ambientes bloqueados, desativam por completo as anotações URI por política, pelo que um clique não produz qualquer efeito, sem erro visível. As aplicações PDF móveis variam quanto a abrirem as ligações dentro da vista web da própria aplicação ou a passá-las para o browser do sistema
Nenhum destes casos é um erro corrigível do lado da geração; são decisões de política do visualizador. O que se pode fazer é escrever rótulos de ligação que também tornem o URL visível no corpo do documento, para que um leitor num ambiente restrito ainda consiga copiar o endereço manualmente. A anotação é a conveniência; o texto é o recurso alternativo
Há mais um pormenor que vale a pena conhecer: as anotações URI em PDF não têm, por predefinição, qualquer sublinhado visual. O sublinhado visto na maioria dos visualizadores é desenhado pelo próprio visualizador com base no tipo de anotação, e não por um glifo no fluxo de conteúdo. Se for necessário um sublinhado físico que sobreviva à impressão para um renderizador não interativo ou à conversão de PDF para imagem, deve desenhar-se explicitamente com LineTo e Stroke no deslocamento Y apropriado abaixo da linha de base do texto. Essa é uma operação de desenho separada, que o PrintHyperlink não trata automaticamente
A API de hiperligações aqui apresentada faz parte do HotPDF Delphi Component para Delphi e C++Builder