技術記事

HotPDF Delphiのハイパーリンク: PrintHyperlink注釈のヒント

PDFのハイパーリンクはURI注釈です。これは、ページ上の一部を覆う四角形で、クリックされるとビューアにURLを開くよう指示します。注釈とその下のテキストは、完全に独立したオブジェクトです。HotPDFの PrintHyperlink は、両方を1つの呼び出しにまとめ、テキストを描画し、レンダリングされたテキストのメトリックから注釈の四角形を計算します。この利便性は、本番コードを記述する前に理解しておく価値のある詳細を隠しています。また、これがすべてではありません。 AddURILink は、自分で描画したコンテンツ上にクリック可能な領域を配置し、 AddGoToLink は内部ナビゲーションを処理します。これらは両方とも以下で説明します

PrintHyperlinkの仕組み

PrintHyperlinkTHPDFPage にあり、4つの引数を取ります。X座標とY座標(ポイント単位、左下原点、Yは上に向かって増加)、描画するラベル文字列、およびURLターゲットです。内部的には、現在のハイパーリンクの色で TextOut を呼び出し、次に現在のフォントメトリックでの TextWidthTextHeight から注釈の四角形をすぐに計算します。つまり、呼び出しの前にフォントとサイズを設定する必要があり、ラベルの描画と注釈の配置の間でそれらを変更してはなりません。両方とも同じ呼び出しで解決されるためです

デフォルトの色は clBlue です。 SetRGBHyperlinkColor は後続の呼び出しにのみこれを変更します。既に書き込まれた注釈を遡って更新することはありません。同じページ上の異なるリンクグループに異なる色が必要な場合は、各グループの前に SetRGBHyperlinkColor を呼び出し、後でリセットしてください

以下は、2つの異なる色で3つのリンクを書き込む最小限のドキュメントです

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は、ポイント単位(1/72インチ)で、Yが上に向かって増加する左下原点を使用します。A4ページは595 x 842ポイント、USレターページは612 x 792ポイントです。Y=750はA4ページの上部近くにあり、Y=50は下部マージンの近くになります。画面グラフィックスやHTMLの経験がある人は、その逆を想定し、最初のリンク行を可視領域のすぐ外側に配置します

PrintHyperlink が計算する注釈の四角形は、同じ座標系を使用します。後でページを回転させたり、スケーリングしたり、X/Y値を再計算せずにページサイズを変更したりすると、表示されるテキストとクリック可能な四角形がずれてしまいます。テキストの近くをクリックするとURLがトリガーされるという意味でリンクは「機能」しますが、ホットゾーンは読者が見ているものと一致しなくなります。開発マシンの100%だけでなく、実際に出荷するページサイズとズームレベルでテストしてください

ずれが確実に発生するケースが1つあります。A4ページに適した座標で PrintHyperlink を呼び出し、X/Y値を調整せずにカスタムの幅の狭いページサイズに切り替えた場合、注釈がページから完全にはみ出してしまう可能性があります。注釈オブジェクトは依然としてPDFに書き込まれますが、ほとんどのビューアはそれを暗黙のうちにクリップするため、リンクはエラーなしに単に消えてしまいます

ラベルテキストとURLターゲット

Text 引数と Link 引数は独立しています。ターゲットがクエリパラメータを持つ完全修飾のHTTPS URLである間に、「請求書PDFをダウンロード」を描画できます。この分離は意図的なものです。表示されるラベルは人間が読めるものである必要があり、URLは長くしたり動的に生成したりできます

問題を引き起こすのは、ラベルが生のURL自体である場合、特に長い場合です。URLが視覚的に2行にまたがって折り返されているのに、注釈の四角形が1行の文字列に対して計算された場合、最初の行のみがクリック可能です。 PrintHyperlink は複数行のフローを処理しません。現在のフォントサイズとページ幅で1行に収まるようにラベルを短く保つか、完全なURLをターゲットとした短い説明ラベルを使用するか、次のセクションで示す行ごとの回避策を適用してください

アクティブなインターネット接続なしでアーカイブまたは配布されるドキュメントの場合は、URL自体も、注釈メタデータとしてだけでなく、ドキュメントの本文のどこかに印刷された形式で表示する必要があるかどうかも検討してください。紙にPDFを印刷した読者は、URI注釈から何も得られません

複数行の制限の回避

リンクのラベルが純粋に複数行にまたがる必要がある場合(そのまま印刷された長いURL、または端から端までクリック可能にする必要がある折り返された文)、修正は、それを1つのリンクとして扱うのをやめ、行ごとに1つのリンクとして扱うことです。各 PrintHyperlink 呼び出しは、描画するテキストから四角形を計算するため、同じ Link ターゲットを共有する複数の呼び出しは、すべて同じURLを開く適切にサイズ設定された複数の注釈を生成します。読者は違いを区別できません。すべての行がクリックに応答します

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

文字列の分割はあなたの責任です。各候補行をテストするために TextWidth を使用して、現在のフォントと列の幅で視覚的に折り返されるのと同じ位置で分割します。別の方法は、プレーンな TextOut 呼び出しで折り返されたテキストを自分で描画し、次に上記の回避策のように各行の上に1つの AddURILink の四角形を配置することです。テキストが独自のワードラップロジックによって既に生成されている場合は、このルートの方が適しています

AddURILink: 描画した任意の要素の上のクリック可能な領域

PrintHyperlink は便利なラッパーです。独自のラベルを描画し、そのラベルのメトリックから四角形を導出します。 AddURILink は、直接公開されている低レベルの半分です

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

これは注釈のみを書き込みます。テキストは描画されず、色の変更もありません。 Rectangle は、描画呼び出しと同じ座標空間で解釈されるため、 TextOut または画像呼び出しに渡したのとまったく同じX/Y値を再利用できます。これにより、画像ホットスポット、テーブルセル、以前に描画されたテキストブロック、または上記の回避策のような折り返された段落の1行など、可視コンテンツが既に存在する場合は常に適切なツールになります。注釈には幅ゼロの境界線があるため、目に見えるものは何も変わりません。クリック可能な領域は、指定した四角形とまったく同じです

この関数は、注釈辞書を THPDFDictionaryObject として返します。ほとんどの呼び出し元は結果を破棄しますが、それを保持しておくと、ドキュメントが書き込まれる前に注釈のエントリを調整できます

2つのコンプライアンスの詳細が組み込まれています。PDF/Aモードでは、それらの標準が要求するように、注釈の印刷フラグが設定されます。 PDFUACompliance では、 Description パラメータは空でない文字列である必要があります(これは注釈の /Contents エントリになり、支援技術がリンクについて読み上げるものです)。また、非準拠のファイルを暗黙のうちに出力するのではなく、呼び出しによって例外が発生します。 PrintHyperlink はそのルールの前から存在しており、説明を添付しないため、PDF/UA出力の場合は、 TextOut でラベルを描画し、意味のある説明とともに AddURILink を使用して注釈を配置します

決定ルールは単純です。リンクがまだ描画していない短いテキストの場合は PrintHyperlink を使用します。クリック可能な領域が、自分で描画または測定したコンテンツによって定義されている場合は AddURILink を使用します

AddGoToLinkを使用した内部ナビゲーション

外部URLは、リンク注釈が実行することの半分にすぎません。残りの半分は、ドキュメント内のナビゲーション(章にジャンプする目次、セクション間の相互参照)です。HotPDFはこれを AddGoToLink を介して公開します

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

シグネチャからは推測できないため、3つのセマンティクスを正確に述べる価値があります。 TargetPageIndex はゼロベースです。ドキュメントの最初のページはページ0であり、 CurrentPageNumber と一致します。呼び出しを行う際、ターゲットページは既に存在している必要があります。インデックスが範囲外の場合、プロシージャは注釈を追加せずに戻ります(例外なし、リンクなし、警告なし)。前方を指す目次を作成する場合は、最初にすべてのページを作成し、後で戻ってリンクを追加します

YPos は、ターゲットページ上の垂直位置を、描画呼び出しと同じ座標空間で選択します。デフォルトの-1(任意の負の値)はnullの宛先座標を書き込み、ターゲットページに到達したときに現在の垂直位置を維持するようにビューアに指示します。負でない値を渡すと、ビューアはスクロールして、その位置がウィンドウの上部にくるようにします。リンクしている見出しのY座標を使用します。ズームは常に変更されません。 AddURILink と同様に、 PDFUACompliance では Description を空にすることはできず、リンクの代替テキストになります

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;

各エントリには、テキストよりも広い四角形が割り当てられるため、行全体がポインタに応答し、すべてのリンクが(Y=780に描画された)章の見出しとともにウィンドウのトップに着地します。後で章の前にページを挿入した場合、すべての TargetPageIndex は1つずつシフトします。インデックスをハードコーディングするのではなく、ページ作成ループからインデックスを計算してください

ドキュメント生成の完全な例

以下のパターンは、 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;

テキスト呼び出しの各グループの前に SetFont が呼び出されていることに注意してください。フォントは AddPage 全体で持続しません。新しいページで PrintHyperlink の前に設定するのを忘れた場合、注釈の四角形はページのデフォルトのメトリックに対して計算され、予想とは異なる場合があります

注釈の処理がビューア間で異なる箇所

PDFのURI注釈はISO 32000-1 §12.6.4.7で定義されており、準拠しているすべてのビューアはそれに従う必要があります。実際には、いくつかの動作はビューアによって異なります。Adobe Acrobatは、信頼済みドメインリストにないURLを最初にクリックしたときにセキュリティプロンプトを表示しますが、多くのブラウザや軽量のリーダーは表示しません。ロックダウンされた環境にある一部のエンタープライズPDFビューアは、ポリシーによってURI注釈を完全に無効にするため、クリックしても何も起こらず、目に見えるエラーもありません。モバイルPDFアプリは、アプリのWebビュー内でリンクを開くか、システムブラウザに渡すかが異なります

これらはどれも生成側から修正できるバグではありません。それらはビューアのポリシー決定です。できることは、URLがドキュメントの本文にも表示されるようなリンクラベルを書き、制限された環境の読者でも手動でアドレスをコピーできるようにすることです。注釈は便利機能であり、テキストはフォールバックです

さらに知っておく価値のある詳細が1つあります。デフォルトでは、PDFのURI注釈には視覚的な下線は付きません。ほとんどのビューアで表示される下線は、コンテンツストリーム内のグリフによってではなく、注釈のタイプに基づいてビューア自体によって描画されます。非対話型レンダラーへの印刷やPDFから画像への変換で残る物理的な下線が必要な場合は、テキストベースラインの下の適切なYオフセットで LineToStroke を使用して明示的に描画します。それは独立した描画操作であり、 PrintHyperlink が代わりに処理するものではありません

ここに示されているハイパーリンクAPIは、DelphiおよびC++Builder用のHotPDFコンポーネントの一部です