Artigo Técnico

Hiperlinks no HotPDF Delphi: Dicas de Anotação PrintHyperlink

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 inferior esquerda, Y aumentando para cima), 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

Anatomia de uma chamada PrintHyperlink do HotPDF gravando dois objetos PDF independentes: os glifos do rótulo visível desenhados por TextOut e um retângulo de anotação de link URI calculado a partir de TextWidth e TextHeight
Os glifos do rótulo e o retângulo do URI são objetos PDF separados, e é por isso que fonte e cor do hyperlink precisam estar definidas antes de uma única chamada escrever ambos

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, 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 o link 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);  // restaura o padrão

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

A armadilha das coordenadas

O HotPDF usa uma origem no canto inferior esquerdo com o Y crescendo para cima, em pontos (1/72 polegadas). Uma página A4 tem 595 x 842 pt; uma página Carta tem 612 x 792 pt. O Y=750 fica próximo ao topo de uma página A4, e o Y=50 ficaria próximo à margem inferior. Qualquer pessoa que venha de gráficos de tela ou de HTML assume o oposto e coloca a primeira linha de link direto fora da área visível

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 gera problemas é quando o rótulo é a própria URL original, especialmente uma longa. Se a URL quebrar visualmente em duas linhas, mas o retângulo de anotação tiver sido calculado para uma string de uma única linha, apenas a primeira linha será clicável. O PrintHyperlink não trata o fluxo de múltiplas linhas; mantenha o rótulo curto o suficiente para caber em uma linha com o tamanho de fonte e a largura da página atuais, use um rótulo curto e descritivo com a URL completa como o destino, ou aplique a solução alternativa 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

Contornando a limitação de várias linhas

Quando um rótulo de link genuinamente tem que se estender por mais de uma linha — uma URL longa impressa textualmente, ou uma frase com quebra que deva ser clicável de ponta a ponta — a solução é parar de tratá-lo como um link e tratá-lo como um link por linha. Cada chamada de PrintHyperlink calcula o seu retângulo a partir do texto que ele desenha, então várias chamadas que compartilham o mesmo destino de Link produzem várias anotações dimensionadas corretamente onde todas abrem a mesma URL. O leitor não consegue perceber a diferença; cada linha responde a um clique

Comparação de um rótulo de hyperlink do HotPDF com quebra de linha recebendo uma anotação que cobre apenas sua primeira linha versus uma chamada PrintHyperlink por linha renderizada compartilhando o mesmo destino de URL
Um retângulo calculado para uma linha abandona toda continuação com quebra de linha, enquanto chamadas por linha compartilham um alvo e mantêm o bloco inteiro clicável
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 que as suas chamadas de desenho, portanto 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 são incorporados. Nos modos PDF/A, o sinalizador de impressão da anotação é definido como esses padrões exigem. Sob PDFUACompliance, o parâmetro Description deve ser uma string que não esteja vazia — ele se torna a entrada /Contents da anotação, que é o que a tecnologia assistiva anuncia para o link — e a chamada lança uma exceção em vez de emitir silenciosamente um arquivo não conforme. O PrintHyperlink é anterior a essa regra e não anexa nenhuma descrição; portanto, para saídas PDF/UA, desenhe o rótulo com TextOut e coloque a anotação com AddURILink mais uma descrição que tenha 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. A página de destino já deve existir quando você faz a chamada; se o índice estiver fora do intervalo, o procedimento retorna sem adicionar uma anotação — nenhuma exceção, nenhum link, nenhum aviso. Para um índice que aponte para a frente, crie todas as páginas primeiro, depois retorne e adicione os links

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

HotPDF: sumário com links construído com AddGoToLink mostrando saltos TargetPageIndex base zero da página de sumário às páginas de capítulo onde cada título aterrissa no topo da janela
Os retângulos se estendem além do texto para que linhas inteiras respondam, e um Y fixo de pouso coloca cada título de capítulo no topo da janela
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, 780, 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, 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 um espaçamento extra
        I + 1,                           // baseado 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 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=780) 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, 750, 0, WideString(ProductName));

    // Placeholder do parágrafo de corpo
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');

    // Links 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;

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