PDF 超連結是 URI 註解:一個覆蓋某塊頁面區域的矩形,在被點選時告訴檢視器開啟一個 URL。註解與其下方的文字是完全獨立的物件。HotPDF 的 PrintHyperlink 把兩者綁進一次呼叫,繪製文字並從繪製後的文字度量計算註解矩形。那個便利隱藏了一個在撰寫正式程式碼之前值得理解的細節。它也不是全貌:AddURILink 把一個可點選區域放在你自己繪製的內容上方,而 AddGoToLink 處理內部導覽——兩者都在下面涵蓋
PrintHyperlink 如何運作
PrintHyperlink 位於 THPDFPage 上,接受四個引數:X 與 Y 座標(以點為單位,左下角為原點,Y 向上遞增)、要繪製的標籤字串,以及 URL 目標。它在內部以目前的超連結顏色呼叫 TextOut,然後立即從目前字型度量下的 TextWidth 與 TextHeight 計算註解矩形。這意味著字型與大小必須在呼叫之前設定好,而且不能在繪製標籤與放置註解之間改變,因為兩者是在同一次呼叫中解析的
預設顏色是 clBlue。SetRGBHyperlinkColor 只改變之後呼叫的顏色;它不會回頭更新已寫入的註解。如果你需要在同一頁上為不同的連結群組使用不同顏色,請在每個群組之前呼叫 SetRGBHyperlinkColor,之後再重設
以下是一份以兩種不同顏色寫入三個連結的最小文件:
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 向上增長的座標系統,以點為單位(1/72 英吋)。一份 A4 頁面是 595 x 842 點;一份 US Letter 頁面是 612 x 792 點。Y=750 位於 A4 頁面的接近頂端,而 Y=50 會接近底部邊界。任何來自螢幕繪圖或 HTML 的人都會假設相反,並把第一列連結直接放到可見區域之外
PrintHyperlink 計算的註解矩形使用同一套座標系統。如果你之後旋轉頁面、縮放它,或在未重新計算 X/Y 值的情況下改變頁面大小,可見文字與可點選矩形就會漂離。連結在某種意義上「能用」——點選文字附近的某處會觸發 URL——但熱區不再符合讀者所見。請在你出貨的實際頁面大小與縮放層級上測試,而不只是在開發機上以 100% 測試
有一種情況漂離是保證的:如果你以適合 A4 頁面的座標呼叫 PrintHyperlink,然後切換到自訂的窄格式頁面而不調整 X/Y 值,註解可能完全落到頁面之外。註解物件仍會被寫進 PDF;大多數檢視器會悄悄裁切它,所以連結就這樣消失而沒有任何錯誤
標籤文字與 URL 目標
Text 與 Link 引數是獨立的。你可以繪製「Download invoice PDF」,而目標是一個帶有查詢參數的完整 HTTPS URL。這種分離是刻意的;可見標籤應該是易讀的,而 URL 可以很長或動態產生
造成問題的是,當標籤本身就是原始 URL,尤其是一個長的。如果 URL 視覺上跨兩列換行,但註解矩形是為單列字串計算的,就只有第一列可點選。PrintHyperlink 不處理多列流向;請讓標籤短到能在目前字型大小與頁面寬度下放進一列、使用簡短的描述性標籤而以完整 URL 作為目標,或套用下一節的逐列替代做法
對於會在沒有活躍網路連線的情況下封存或散布的文件,也請考慮 URL 本身是否應該以列印形式出現在文件主體的某處,而不僅僅作為註解中繼資料。一個把 PDF 列印到紙上的讀者,從 URI 註解什麼也得不到
繞過多列限制
當一個連結標籤確實必須跨一列以上——一個逐字列印的長 URL,或一個應該頭尾都可點選的換行句子——修復方式是停止把它當作一個連結,而是把它當作每一列一個連結。每一次 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 呼叫自己繪製換行的文字,然後在每一列上方鋪一個 AddURILink 矩形——當文字已經由你自己的自動換列邏輯產生時,這是更好的路徑,而這也把我們帶到那個函式
AddURILink:在你繪製的任何東西上的可點選區域
PrintHyperlink 是一個便利的包裝器:它繪製自己的標籤並從該標籤的度量推導矩形。AddURILink 則是直接公開的較低層一半:
function AddURILink(Rectangle: TRect; const URL: AnsiString;
const Description: AnsiString = ''): THPDFDictionaryObject;
它只寫入註解——不繪製文字,也不改變顏色。Rectangle 以與你的繪製呼叫相同的座標空間來詮釋,所以你可以重用你傳給 TextOut 或某個影像呼叫的精確 X/Y 值。這使得每當可見內容已經存在時——一個影像熱點、一個表格儲存格、一塊先前繪製的文字,或如上面替代做法中換行段落的一列——它就是正確的工具。註解帶有零寬度的框線,所以沒有任何可見的變化;可點選的區域正是你指定的矩形
該函式以 THPDFDictionaryObject 回傳註解字典。大多數呼叫者丟棄結果,但保留它能讓你在文件寫入之前調整註解的項目
有兩個合規細節是內建的。在 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 = '');
有三個語意值得精確說明,因為沒有一個能從簽章猜得出來。TargetPageIndex 是從零起算的:文件的第一頁是第 0 頁,與 CurrentPageNumber 一致。目標頁面在你進行呼叫時必須已經存在;如果索引超出範圍,程序會返回而不加入註解——沒有例外、沒有連結、沒有警告。對於一份向前指的目錄,請先建立所有頁面,再切回去加入連結
YPos 選擇目標頁面上的垂直位置,使用與你的繪製呼叫相同的座標空間。預設值 -1(任何負值)寫入一個空的目的地座標,告訴檢視器在落到目標頁面時保持它目前的垂直位置。傳入一個非負值,檢視器就會捲動使該位置位於視窗頂端——使用你要連結過去的標題的 Y 座標。縮放層級始終保持不變。如同 AddURILink,Description 在 PDFUACompliance 之下必須非空,並成為該連結的替代文字
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 都會偏移一;請從你的頁面建立迴圈計算索引,而不是把它們寫死
一份完整的文件產生範例
以下模式顯示一個更真實的情境:產生一份帶有標頭區段、主體文字與一列頁尾連結的簡短報表,全部來自程式碼,而非來自一個帶有 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 也在文件主體中可見的連結標籤,好讓一個在受限環境中的讀者仍能手動複製位址。註解是便利;文字是後援
還有一個細節值得知道:PDF URI 註解預設不帶任何視覺底線。你在大多數檢視器中看到的底線,是檢視器本身依據註解型別繪製的,而非內容串流中的字元。如果你需要一條能在列印到非互動繪製器、或 PDF 轉影像轉換中存活的實體底線,請在文字基準線下方適當的 Y 偏移處,用 LineTo 與 Stroke 明確繪製它。那是一次分開的繪製操作,不是 PrintHyperlink 替你處理的東西
此處展示的超連結 API,是供 Delphi 與 C++Builder 使用的HotPDF Component 的一部分