技术文章

HotPDF Delphi 超链接:PrintHyperlink 注释技巧

PDF 超链接是 URI 注释,实际是覆盖在页面区域上的矩形。用户点击该区域后,阅读器会打开指定 URL。注释与显示文本是独立对象。PrintHyperlink 在一次调用里帮你把文字和注释矩形一起完成,省去手工对齐,但不是唯一方式。它更适合只绘制短链接文本的场景,而要为已有内容加热点或做文内跳转时,请改用 AddURILinkAddGoToLink

PrintHyperlink 如何工作

PrintHyperlink 挂在 THPDFPage 上,参数为 X 与 Y(点单位,原点在左下角,Y 向上)、标签文本和 URL。它先按当前超链接颜色调用 TextOut,再用同一次调用里的 TextWidthTextHeight 计算矩形。这意味着字体、字号和颜色必须在调用前就设置好,且文本绘制与注释生成之间不能变化参数

默认颜色是 clBlueSetRGBHyperlinkColor 只影响后续调用,不会回改旧注释。需要不同颜色分组时,在每组前设置颜色并在之后恢复

以下是一个最小例子,演示三条不同颜色链接

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;

坐标陷阱

HotPDF 使用左下角原点、Y 向上的点坐标。A4 是 595 x 842 点,US Letter 是 612 x 792 点。若你把坐标按 A4 计算后直接用于窄页,Y=750 一类的值会跑到页边缘外面,或直接不可见。更重要的是:旋转、缩放、切换页面尺寸后如果不重算 X/Y,文本和注释可能脱节,点中区域在视觉上不对齐

这类问题最早在重试发布阶段出现,因为保存后的 PDF 表面上看起来“能打开”,只是点击落点和显示文本不一致。请按实际页面尺寸和缩放级别验证,而不是只在开发机 100% 缩放下过测

PrintHyperlink 的注释矩形和绘图共用同一坐标体系。稍后你如果旋转页面、缩放显示或改了页边界尺寸而没有同步更新 X/Y,文字位置和点击区会错位。可点区域仍可能触发正确 URL,但点中的不再是读者看到的同一处内容

标签文本与 URL 目标

TextLink 不关联。你可以显示“下载发票 PDF”这种友好文案,而目标却是完整 HTTPS URL。反过来把原始长 URL 当文案会让可读性和可点击区域都变差

如果长 URL 自动换行,但 PrintHyperlink 按单行文本计算矩形,通常只有第一行可点击。应对办法是保持标签在一行,或改用后文的“逐行覆盖”方式

对于长地址,建议避免把完整 URL 作为标签文本。可以保留“下载 PDF 文档”这类短说明作为显示文案,再把真实地址作为目标参数传入

如果这份文档需要归档或离线流转,也应在正文里保留可见 URL 文本作为后备。打印成纸质件时,注释元数据本身不可见,读者至少能看到地址去手动打开

多行情况下的解决方案

当链接文案确实需要多行时,把它按显示行拆成多个 PrintHyperlink 调用,每行共用同一个 Link。这样每段都会有正确矩形,且全部行都可点击

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

拆分行时按真实可见宽度计算换行点。另一种等价方式是自己先用 TextOut 画好每行,再用 AddURILink 套在每行上,这在你已有排版输出时更直观

AddURILink:为已绘制内容加交互区域

PrintHyperlink 更适合“顺带画文本”,它会同步根据标签文本绘制区域。你已手动绘制过内容、要给图片、表格或段落已有文本添加点击范围时,通常改用 AddURILink

function AddURILink(Rectangle: TRect; const URL: AnsiString;
  const Description: AnsiString = ''): THPDFDictionaryObject;

它只写注释,不会改文字或颜色。矩形坐标与其他绘图调用使用同一坐标体系,所以可以复用 TextOut 或图片绘制时的 X/Y。这样每个交互区域与现有内容天然对齐

函数返回值是 THPDFDictionaryObject,多数调用者可以忽略,但在需要时保留引用后可在入稿前继续补充字典项

PDF/A 会按标准写入注释打印标记;而 PDFUACompliance 模式下 Description 不能为空,它会作为注释的 /Contents 参与读屏提示。缺少描述会触发异常,不可静默通过

要输出 PDF/UA 时通常建议先用 TextOut 画出标签,再用 AddURILink 并传入可读描述。如果目标就是短纯文本,优先选择 PrintHyperlink,否则直接用 AddURILink 定义区域

AddGoToLink 与文内导航

AddGoToLink 用于文内跳转

procedure AddGoToLink(Rectangle: TRect; TargetPageIndex: Integer;
  YPos: Single = -1; const Description: AnsiString = '');

TargetPageIndex 是 0 基准,首页是 0,和 CurrentPageNumber 一致。目标页必须先存在;索引越界时不抛异常也不写注释,直接返回。先生成所有目标页再回到目录页加链接通常更安全

YPos 在目标页上设置进入位置,负值表示保持当前纵向位置。非负值会把目标 Y 对齐到窗口顶部。方向和缩放不会改动。PDFUAComplianceDescription 同样不能为空

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;

每行链接常用比行文字稍宽的矩形,使整行可点。若目录前置了空白页,后续 TargetPageIndex 会整体偏移,建议用创建页序列时的索引直接派生,而不是硬编码

完整文档生成示例

下面给出一个更贴近生产的例子:一个含页首、正文和底部链接区的报表模板

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;

同一组文字前先设置一次 SetFont,因为字体不会在 AddPage 间自动保留。漏掉这一步会导致新页的注释矩形按默认指标计算,和预期错位

不同阅读器上的差异

ISO 32000-1 §12.6.4.7 规定了 URI 注释,但不同阅读器执行细节会不同。Adobe Acrobat 对第一次打开非受信任域 URL 会有安全提示,而一些轻量客户端不显示。企业环境可能会直接禁用 URI 注释。移动端可能在应用内浏览,也可能跳到系统浏览器

这些不能在生成端兜底。可用文字副本把 URL 可见化,保留阅读器策略导致点击失效时的降级路径。视觉下划线多数是阅读器在显示层补画的;如需打印或转图像后仍显示下划线,可在文本下方额外用 LineToStroke 绘制一条

本文涉及的链接 API 归属于 HotPDF Component(Delphi 与 C++Builder),含加载、编辑、加密与签名 API 的完整能力同样在该组件里