ไฮเปอร์ลิงก์ใน PDF คือคำอธิบายประกอบชนิด URI: สี่เหลี่ยมที่คลุมพื้นที่ส่วนหนึ่งของหน้า ซึ่งเมื่อถูกคลิกจะบอกโปรแกรมอ่านให้เปิด URL ตัวคำอธิบายประกอบกับข้อความที่อยู่ใต้มันเป็นออบเจกต์ที่แยกขาดจากกันโดยสิ้นเชิง PrintHyperlink ของ HotPDF รวมทั้งสองอย่างไว้ในการเรียกครั้งเดียว โดยวาดข้อความแล้วคำนวณสี่เหลี่ยมของคำอธิบายประกอบจากมาตรวัดของข้อความที่เรนเดอร์ออกมา ความสะดวกนั้นซ่อนรายละเอียดที่ควรเข้าใจก่อนจะเขียนโค้ดขึ้นระบบจริง และมันยังไม่ใช่เรื่องทั้งหมด: 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);
// สีน้ำเงินเริ่มต้นสำหรับลิงก์ให้ข้อมูล
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');
// สีแดงสำหรับลิงก์ที่ต้องการให้ลงมือ
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); // คืนค่าเริ่มต้น
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
กับดักเรื่องพิกัด
HotPDF ใช้จุดกำเนิดที่มุมซ้ายล่างโดยค่า Y เพิ่มขึ้นเมื่อขึ้นบน หน่วยเป็นพอยต์ (1/72 นิ้ว) หน้า A4 มีขนาด 595 x 842 pt ส่วนหน้า US Letter มีขนาด 612 x 792 pt ค่า Y=750 อยู่ใกล้ขอบบนของหน้า A4 และ Y=50 จะอยู่ใกล้ระยะขอบล่าง ใครก็ตามที่มาจากกราฟิกบนจอหรือจาก HTML จะสมมติตรงกันข้าม แล้ววางบรรทัดลิงก์แรกทะลุออกนอกพื้นที่ที่มองเห็นไปเลย
สี่เหลี่ยมของคำอธิบายประกอบที่ PrintHyperlink คำนวณใช้ระบบพิกัดชุดเดียวกัน ถ้าภายหลังคุณหมุนหน้า ย่อขยายมัน หรือเปลี่ยนขนาดหน้าโดยไม่คำนวณค่า X/Y ใหม่ ข้อความที่มองเห็นกับสี่เหลี่ยมที่คลิกได้จะเลื่อนออกจากกัน ลิงก์ยัง "ทำงาน" ในความหมายที่ว่าการคลิกแถว ๆ ข้อความยังเรียก URL ได้ แต่พื้นที่ร้อนไม่ตรงกับสิ่งที่ผู้อ่านเห็นแล้ว ทดสอบบนขนาดหน้าและระดับซูมที่คุณส่งมอบจริง ไม่ใช่แค่บนเครื่องพัฒนาที่ 100%
มีกรณีหนึ่งที่การเลื่อนเกิดขึ้นแน่นอน: ถ้าคุณเรียก PrintHyperlink ด้วยพิกัดที่เหมาะกับหน้า A4 แล้วเปลี่ยนไปใช้หน้าแบบกำหนดเองที่แคบกว่าโดยไม่ปรับค่า X/Y คำอธิบายประกอบอาจไปจบนอกหน้ากระดาษทั้งอัน ออบเจกต์คำอธิบายประกอบยังถูกเขียนลงใน PDF อยู่ดี โปรแกรมอ่านส่วนใหญ่ตัดมันทิ้งอย่างเงียบ ๆ ลิงก์จึงหายไปเฉย ๆ โดยไม่มีข้อผิดพลาดใด
ข้อความป้ายกำกับกับ URL ปลายทาง
อาร์กิวเมนต์ Text กับ Link เป็นอิสระต่อกัน คุณวาดคำว่า "Download invoice PDF" ได้ในขณะที่ปลายทางเป็น URL แบบ HTTPS เต็มรูปพร้อมพารามิเตอร์คิวรี การแยกกันนี้เป็นความตั้งใจ ป้ายกำกับที่มองเห็นควรอ่านออกโดยมนุษย์ ส่วน URL จะยาวหรือถูกสร้างขึ้นแบบไดนามิกก็ได้
สิ่งที่ก่อปัญหาคือเมื่อป้ายกำกับเป็นตัว URL ดิบเสียเอง โดยเฉพาะ URL ที่ยาว ถ้า URL ตัดขึ้นบรรทัดใหม่ในเชิงสายตาเป็นสองบรรทัด แต่สี่เหลี่ยมคำอธิบายประกอบถูกคำนวณสำหรับสตริงบรรทัดเดียว จะมีเพียงบรรทัดแรกที่คลิกได้ PrintHyperlink ไม่จัดการการไหลข้ามหลายบรรทัด ให้คุมป้ายกำกับให้สั้นพอจะอยู่ในบรรทัดเดียวที่ขนาดฟอนต์และความกว้างหน้าปัจจุบัน หรือใช้ป้ายกำกับสั้น ๆ ที่สื่อความโดยให้ URL เต็มเป็นปลายทาง หรือใช้ทางแก้แบบรายบรรทัดที่แสดงในหัวข้อถัดไป
สำหรับเอกสารที่จะถูกจัดเก็บถาวรหรือแจกจ่ายโดยไม่มีการเชื่อมต่ออินเทอร์เน็ต ควรพิจารณาด้วยว่าตัว URL เองควรปรากฏในรูปแบบที่พิมพ์ออกมาได้ที่ใดที่หนึ่งในเนื้อเอกสารหรือไม่ ไม่ใช่มีอยู่แค่ในฐานะ metadata ของคำอธิบายประกอบ ผู้อ่านที่พิมพ์ 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;
// วิธีใช้: ตัดป้ายกำกับตรงตำแหน่งที่เลย์เอาต์ของคุณตัดบรรทัดจริง
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 ถูกตีความในปริภูมิพิกัดเดียวกับการเรียกวาดของคุณ คุณจึงนำค่า X/Y ที่ส่งให้ TextOut หรือการเรียกวางภาพมาใช้ซ้ำได้ตรง ๆ นั่นทำให้มันเป็นเครื่องมือที่ถูกต้องเมื่อใดก็ตามที่เนื้อหาที่มองเห็นมีอยู่แล้ว: พื้นที่คลิกบนภาพ เซลล์ในตาราง บล็อกข้อความที่วาดไว้ก่อนหน้า หรือหนึ่งบรรทัดของย่อหน้าที่ตัดบรรทัดอย่างในทางแก้ข้างต้น คำอธิบายประกอบนี้มีเส้นขอบความหนาศูนย์ จึงไม่มีอะไรที่มองเห็นเปลี่ยนไป พื้นที่ที่คลิกได้คือสี่เหลี่ยมที่คุณระบุพอดี
ฟังก์ชันนี้คืนพจนานุกรมของคำอธิบายประกอบมาในรูป THPDFDictionaryObject ผู้เรียกส่วนใหญ่ทิ้งค่าที่คืนมา แต่การเก็บไว้ช่วยให้คุณปรับรายการต่าง ๆ ในคำอธิบายประกอบได้ก่อนที่เอกสารจะถูกเขียน
มีรายละเอียดด้านความสอดคล้องสองข้อที่ถูกฝังไว้ในตัว ในโหมด PDF/A ธงสั่งพิมพ์ของคำอธิบายประกอบถูกตั้งตามที่มาตรฐานเหล่านั้นเรียกร้อง ส่วนภายใต้ PDFUACompliance พารามิเตอร์ Description ต้องเป็นสตริงที่ไม่ว่าง — มันกลายเป็นรายการ /Contents ของคำอธิบายประกอบ ซึ่งเป็นสิ่งที่เทคโนโลยีช่วยเหลืออ่านออกเสียงแทนลิงก์ — และการเรียกจะโยน exception แทนที่จะปล่อยไฟล์ที่ไม่สอดคล้องออกไปเงียบ ๆ 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 หน้าปลายทางต้องมีอยู่แล้วตอนที่คุณเรียก ถ้าดัชนีอยู่นอกช่วง โพรซีเจอร์จะคืนกลับโดยไม่เพิ่มคำอธิบายประกอบใด ๆ ไม่มี exception ไม่มีลิงก์ ไม่มีคำเตือน สำหรับสารบัญที่ชี้ไปข้างหน้า ให้สร้างทุกหน้าก่อน แล้วค่อยย้อนกลับมาเพิ่มลิงก์
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; // หน้า 0 กลายเป็นหน้าสารบัญ
// สร้างหน้าบทก่อน เพื่อให้ปลายทางของลิงก์มีอยู่จริง
for I := 0 to High(Chapters) do
begin
Pdf.AddPage; // หน้า 1..3
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
end;
// ย้อนกลับไปหน้า 0 แล้ววาดรายการสารบัญพร้อมลิงก์ของมัน
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), // คลุมรายการพร้อมระยะเผื่อ
I + 1, // เริ่มจากศูนย์: บทต่าง ๆ คือหน้า 1..3
780, // ให้หัวข้อมาหยุดที่ขอบบน
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;
// ส่วนหัว
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 750, 0, WideString(ProductName));
// ที่วางย่อหน้าเนื้อความ
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');
// ลิงก์ท้ายหน้า
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 บนหน้าใหม่ สี่เหลี่ยมคำอธิบายประกอบจะถูกคำนวณเทียบกับมาตรวัดเริ่มต้นของหน้านั้น ซึ่งอาจต่างจากที่คุณคาดไว้
จุดที่การจัดการคำอธิบายประกอบต่างกันไปตามโปรแกรมอ่าน
คำอธิบายประกอบชนิด URI ของ PDF ถูกนิยามไว้ใน ISO 32000-1 §12.6.4.7 และโปรแกรมอ่านที่สอดคล้องกับมาตรฐานทุกตัวควรทำตามนั้น ในทางปฏิบัติ มีพฤติกรรมไม่กี่อย่างที่ต่างกันไปตามโปรแกรมอ่าน Adobe Acrobat แสดงคำเตือนด้านความปลอดภัยตอนคลิกครั้งแรกสำหรับ URL ที่ไม่อยู่ในรายการโดเมนที่เชื่อถือได้ ขณะที่เบราว์เซอร์และโปรแกรมอ่านขนาดเบาจำนวนมากไม่แสดง โปรแกรมอ่าน PDF ระดับองค์กรบางตัวในสภาพแวดล้อมที่ล็อกดาวน์ปิดคำอธิบายประกอบชนิด URI ทั้งหมดตามนโยบาย การคลิกจึงไม่เกิดอะไรขึ้น โดยไม่มีข้อผิดพลาดให้เห็น ส่วนแอป PDF บนมือถือก็ต่างกันไปว่าจะเปิดลิงก์ในเว็บวิวของแอปเองหรือส่งต่อให้เบราว์เซอร์ของระบบ
ไม่มีข้อใดในนี้เป็นบั๊กที่คุณแก้ได้จากฝั่งการสร้างเอกสาร มันคือการตัดสินใจเชิงนโยบายของโปรแกรมอ่าน สิ่งที่คุณทำได้คือเขียนป้ายกำกับลิงก์ที่ทำให้ URL ปรากฏในเนื้อเอกสารด้วย ผู้อ่านในสภาพแวดล้อมที่ถูกจำกัดจึงยังคัดลอกที่อยู่ด้วยมือได้ คำอธิบายประกอบคือความสะดวก ส่วนข้อความคือทางถอย
อีกรายละเอียดหนึ่งที่ควรรู้: คำอธิบายประกอบชนิด URI ของ PDF ไม่มีเส้นใต้เชิงสายตาติดมาโดยค่าเริ่มต้น เส้นใต้ที่คุณเห็นในโปรแกรมอ่านส่วนใหญ่ถูกวาดโดยตัวโปรแกรมอ่านเองตามชนิดของคำอธิบายประกอบ ไม่ใช่โดยกลีฟในสตรีมเนื้อหา ถ้าคุณต้องการเส้นใต้จริงที่รอดจากการพิมพ์ผ่านตัวเรนเดอร์ที่ไม่โต้ตอบหรือการแปลง PDF เป็นภาพ ให้วาดมันอย่างชัดเจนด้วย LineTo และ Stroke ที่ระยะ Y ที่เหมาะสมใต้เส้นฐานของข้อความ นั่นเป็นการวาดคนละรายการ ไม่ใช่สิ่งที่ PrintHyperlink จัดการให้คุณ
API ไฮเปอร์ลิงก์ที่แสดงไว้ที่นี่เป็นส่วนหนึ่งของ HotPDF Delphi Component สำหรับ Delphi และ C++Builder