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

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);

    // Default blue for informational links
    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');

    // Red for the action link
    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);  // restore default

    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

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;

// Usage: break the label at the positions where your layout wraps it
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

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;                        // page 0 becomes the TOC page

    // Create the chapter pages first so the link targets exist
    for I := 0 to High(Chapters) do
    begin
      Pdf.AddPage;                       // pages 1..3
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
      Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
    end;

    // Switch back to page 0 and draw the TOC entries with their 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),    // covers the entry with padding
        I + 1,                           // zero-based: chapters are pages 1..3
        780,                             // land with the heading at the top
        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;

    // Header
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 750, 0, WideString(ProductName));

    // Body paragraph placeholder
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');

    // Footer links
    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