HotPDF เพิ่มบุ๊กมาร์กไปยังไฟล์ PDF ที่มีอยู่แล้วใน Delphi ผ่านฟังก์ชัน AddLoadedOutline ซึ่งช่วยสร้างรายการเค้าร่าง (outline entries) ในระดับการซ้อนใดๆ บนเอกสารที่โหลดขึ้นมา และยังอ่านกับเขียนข้อมูลปลายทางระบุชื่อ (named destinations) ผ่านทาง ResolveLoadedNamedDestination และ AddLoadedNamedDestination โหลดไฟล์, สร้างแผนผังโครงสร้างต้นไม้, เรียก SaveLoadedDocument จากนั้นแผงแถบด้านข้างของผู้อ่านที่เคยว่างเปล่าก็จะมีสารบัญที่พร้อมใช้งานทันที
สถานการณ์ที่ทำให้เกิดปัญหานี้เป็นเรื่องปกติที่น่าปวดหัวอย่างยิ่ง คุณรวมสัญญาเป็นสิบฉบับเข้าไว้ในแฟ้มตรวจทานฉบับเดียว หรือรวบรวมคู่มือผลิตภัณฑ์ 3 เล่มเป็นไฟล์ที่แจกจ่ายได้ไฟล์เดียว และผลลัพธ์ที่ได้ก็มีโครงสร้างที่ดี: มีครบทุกหน้า, ฟอนต์ครบถ้วนดี แต่เมื่อมีคนเปิดไฟล์ใน Acrobat แผงบุ๊กมาร์กกลับว่างเปล่า เอกสาร 400 หน้าที่ไม่มีระบบนำทางนั้นแม้จะสมบูรณ์ในทางเทคนิคแต่ใช้งานไม่ได้จริงในเชิงปฏิบัติ และจนกระทั่งเวอร์ชัน 2.347.0 HotPDF สามารถอ่าน, แก้ไข และลบบุ๊กมาร์กในเอกสารที่โหลดได้ แต่ไม่สามารถสร้างบุ๊กมาร์กใหม่ได้ ช่องว่างนั้นได้รับการแก้ไขแล้ว และบทความนี้จะพาไปดูส่วนติดต่อโปรแกรม (API surface) ใหม่ รวมถึงเรื่องลับๆ ของ PDF อย่างคีย์ /Count ที่จะทำให้คุณสับสนได้หากมองเป็นเพียงเครื่องมือนับรายการทั่วไป
แท้จริงแล้วแผนผังโครงสร้างเค้าร่างคืออะไร
เค้าร่าง (outline) ของ PDF คือทรีพจนานุกรมที่มีการลิงก์สองทิศทาง (doubly linked tree) ไม่ใช่รายการแบบแบนราบ และโครงสร้างดังกล่าวนี่เองที่อธิบายทุกพารามิเตอร์ใน API ข้อกำหนด ISO 32000-1 §12.3.3 นิยามไว้ว่า: แคตตาล็อกเอกสารจะชี้ไปยังรูท /Outlines, รูทจะชี้ไปยังลูกคนแรกและคนสุดท้ายผ่านทาง /First และ /Last และพี่น้องจะเชื่อมโยงกันผ่าน /Next และ /Prev ขณะที่แต่ละรายการจะชี้กลับขึ้นไปด้านบนผ่าน /Parent แต่ละรายการจะมีสตริงชื่อเรื่อง /Title และมักจะมีค่า /Dest ซึ่งระบุตำแหน่งที่การคลิกจะนำผู้อ่านไปยังจุดนั้น
ปลายทาง (Destinations) มี 2 รูปแบบ และข้อกำหนด ISO 32000-1 §12.3.2 ได้แยกพวกมันออกจากกันโดยเจตนา ปลายทางที่ระบุชัดเจน (explicit destination) คืออาร์เรย์แบบอินไลน์ เช่น [page /XYZ x y zoom] ซึ่งอ้างอิงโดยตรงไปยังวัตถุของหน้าและการระบุรูปแบบมุมมอง ปลายทางระบุชื่อ (named destination) จะจัดเก็บเป็นสตริงชื่อแทน และเป้าหมายที่แท้จริงจะอยู่ในแคตตาล็อกเอกสารภายใต้โครงสร้างทรีชื่อ /Names /Dests ซึ่งเป็นระดับการอ้างอิงทางอ้อม (indirection) ที่ช่วยให้ลิงก์จำนวนมากสามารถแชร์เป้าหมายร่วมกันได้ และช่วยให้เปลี่ยนเป้าหมายได้โดยไม่ต้องแตะต้องลิงก์เหล่านั้น HotPDF จะเขียนปลายทาง /XYZ ที่ระบุชัดเจนเมื่อสร้างบุ๊กมาร์ก และเตรียมฟังก์ชันแยกเฉพาะสำหรับการอ่านและการขยายโครงสร้างทรีชื่อ
จะเพิ่มบุ๊กมาร์กใน PDF ที่มีอยู่แล้วด้วย Delphi ได้อย่างไร?
เรียกใช้ AddLoadedOutline(ParentIndex, DestPageIndex, Title, X, Y, Zoom) หลังการเรียก LoadFromFile: โดยส่งค่า ParentIndex = -1 สำหรับรายการระดับบนสุด หรือดัชนีฐาน 0 ของรายการระดับบนสุดที่มีอยู่เพื่อซ้อนรายการย่อยไว้ใต้รายการนั้น ฟังก์ชันจะสร้างรูท /Outlines หากเอกสารไม่เคยมีมาก่อน, สร้างพจนานุกรมรายการ, เขียนอาร์เรย์ /Dest [page /XYZ x y zoom] ชี้ไปยังดัชนีหน้าปลายทาง DestPageIndex แบบอิง 0 และลิงก์รายการใหม่เข้ากับสายสัมพันธ์พี่น้อง ฟังก์ชันจะส่งกลับ 0 เมื่อสำเร็จ และส่งกลับ -1 เมื่อล้มเหลว เช่น เมื่อ DestPageIndex อยู่นอกช่วงข้อมูลที่ถูกต้อง
รายละเอียดการลิงก์จุดหนึ่งที่มีความสำคัญต่อการเรียงลำดับ: รายการใหม่จะถูกแทรกไว้ที่ ส่วนหัว ของรายการย่อยของโหนดแม่ ในฐานะโหนด /First ใหม่ ดังนั้นหากคุณเพิ่ม "Chapter 1" แล้วเพิ่ม "Chapter 2" เป็นรายการระดับบนสุด Chapter 2 จะแสดงขึ้นมาอยู่เหนือ Chapter 1 ในแถบข้าง และ Chapter 1 จะเลื่อนไปอยู่ที่ดัชนีระดับบนสุดที่ 1 รูปแบบการปฏิบัติงานจริงคือควรเพิ่มรายการในทิศทางย้อนกลับของลำดับการอ่าน หรือเพิ่มโหนดแม่แต่ละอันแล้วกรอกรายการลูกทันทีในขณะที่โหนดยังคงอยู่ที่ดัชนี 0
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('merged-manual.pdf', '') > 0 then
begin
// Add chapters in reverse reading order: each new
// top-level entry becomes /First (index 0).
Pdf.AddLoadedOutline(-1, 40, 'Chapter 2: Configuration', 0, 792, 0);
Pdf.AddLoadedOutline(-1, 0, 'Chapter 1: Installation', 0, 792, 0);
// Chapter 1 is now top-level index 0; nest sections
// under it (children also prepend, so reverse order).
Pdf.AddLoadedOutline(0, 12, 'License activation', 0, 792, 0);
Pdf.AddLoadedOutline(0, 3, 'System requirements', 0, 792, 0);
Pdf.SaveLoadedDocument('merged-manual-toc.pdf');
end;
finally
Pdf.Free;
end;
end;
พารามิเตอร์ X, Y และ Zoom จะจับคู่โดยตรงไปยังปลายทาง /XYZ และทั้งหมดมีค่าเริ่มต้นเป็น 0 PDF กำหนดพิกัดเริ่มต้นไว้ที่มุมล่างซ้าย ดังนั้น Y = 792 จะตรงกับขอบบนสุด of หน้าขนาด US Letter และตามข้อกำหนด ISO 32000-1 ค่าศูนย์ในช่อง /XYZ จะบอกให้โปรแกรมอ่านเก็บค่ามุมมองปัจจุบันไว้ ซึ่งเป็นสิ่งที่คุณต้องการสำหรับการซูมในเกือบทุกกรณี ทุกกระบวนการจะดำเนินการในโครงสร้างวัตถุในหน่วยความจำ โดยไม่มีการบันทึกลงดิสก์จนกว่าจะรันฟังก์ชัน SaveLoadedDocument ซึ่งเป็นรูปแบบแก้ไขแล้วเซฟแบบเดียวกับที่ใช้ใน API เขียนชื่อเรื่อง, ผู้เขียน และข้อมูลเมตา XMP ของ PDF ที่โหลดขึ้นมาใหม่
การกำหนดเป้าหมายบุ๊กมาร์กใหม่: การข้ามหน้าเทียบกับการทำงานของ URI (URI Actions)
บุ๊กมาร์กที่มีอยู่สามารถสลับปลายทางใหม่ได้โดยไม่ต้องสร้างใหม่ และ HotPDF ได้เตรียมฟังก์ชันการเรียก 2 แบบแยกจากกันสำหรับกรณีดังกล่าว เนื่องจากโครงสร้าง PDF เบื้องหลังมีความแตกต่างกันอย่างสิ้นเชิง SetLoadedOutlineDestination(Index, DestPageIndex, X, Y, Zoom) จะแทนที่ /Dest ของรายการระดับบนสุดที่ Index ด้วยอาร์เรย์ /XYZ ใหม่ ซึ่งเป็นเครื่องมือสำหรับแก้ไขปัญหาการรวมไฟล์ล้มเหลวแบบคลาสสิกที่บุ๊กมาร์กทุกอันยังคงชี้ไปที่เลขหน้าของไฟล์ต้นฉบับก่อนการรวม SetLoadedOutlineURI(Index, URI) จะทำงานในโครงสร้างที่ต่างออกไป: โดยจะแนบพจนานุกรมการทำงาน /A ร่วมกับ /S /URI เพื่อเปลี่ยนบุ๊กมาร์กให้เป็นลิงก์เว็บแทนการข้ามหน้าภายในเอกสาร
// The merge shifted everything by 10 pages: repoint the
// third top-level bookmark at page 12 (zero-based 11).
Pdf.SetLoadedOutlineDestination(2, 11, 0, 792, 0);
// Turn the last entry into an external link.
Pdf.SetLoadedOutlineURI(3, 'https://www.loslab.com/');
Pdf.SaveLoadedDocument('retargeted.pdf');
ควรทำความเข้าใจกลไกทั้งสองแบบให้ชัดเจนเมื่อคุณออกแบบแผนผังโครงสร้าง /Dest คือการนำทางภายในเอกสารอย่างแท้จริง ส่วนการทำงานของ URI จะเป็นการออกจากเอกสารอย่างสมบูรณ์ และข้อกำหนด ISO 32000-1 กำหนดให้รายการเค้าร่างนำทางผ่านกลไกใดกลไกหนึ่งเท่านั้น ไม่ใช้ทั้งสองอย่าง ดังนั้นให้สงวนฟังก์ชัน SetLoadedOutlineURI ไว้สำหรับรายการที่ไม่มีปลายทางหน้าเอกสารอยู่แล้ว เช่น โหนดปลายทาง "หน้าแรกของผลิตภัณฑ์" หรือ "รายงานปัญหา" ที่ตอนท้ายของทรี แทนที่จะนำไปใช้กับหัวข้อบทที่ต้องการแปลง การทำงานของ URI ที่เขียนที่นี่มีโครงสร้าง /S /URI แบบเดียวกับที่ใช้ในคำอธิบายประกอบลิงก์บนเนื้อหาหน้า ซึ่งครอบคลุมรายละเอียดไว้แล้วในคู่มือการสร้างไฮเปอร์ลิงก์ในเอกสาร PDF ด้วย HotPDF
ปลายทางระบุชื่อทำงานอย่างไรบน PDF ที่โหลดมา?
ปลายทางระบุชื่อ (Named destinations) คือกลไกยึดตำแหน่งที่มั่นคงตามข้อกำหนด ISO 32000-1 §12.3.2.3: แทนที่จะฝังการอ้างอิงหน้าไว้ในทุกลิงก์ เอกสารจะจัดเก็บแผนผังคู่ชื่อต่อปลายทางไว้ในโครงสร้างทรีชื่อ /Names /Dests ของแคตตาล็อก และลิงก์ต่างๆ จะอ้างอิงรายการด้วยชื่อ ResolveLoadedNamedDestination(Name) จะค้นหาชื่อในโครงสร้างทรีนั้นและส่งคืนค่าดัชนีหน้าแบบอิง 0 ที่มันชี้ไป หรือส่งคืน -1 หากไม่พบชื่อนั้น ตัวคลี่คลาย (resolver) จะจัดการรูปแบบการจัดเก็บทั้ง 2 แบบที่ข้อกำหนดอนุญาตสำหรับค่าแผนผัง: อาร์เรย์ปลายทางเปล่าอย่าง [page /XYZ x y zoom] และรูปแบบพจนานุกรมที่ห่อหุ้มอาร์เรย์ไว้ภายใต้คีย์ /D
AddLoadedNamedDestination(Name, DestPageIndex, X, Y) จะทำงานในทางตรงกันข้าม: โดยจะลงทะเบียนจุดยึด (anchor) ใหม่ในทรี /Names /Dests พร้อมสร้างโครงสร้างทรีขึ้นมาเมื่อเอกสารไม่มีอยู่ และส่งคืนค่า True เมื่อสำเร็จ การทำงานร่วมกันของทั้งคู่จะครอบคลุมกระบวนการทำงานที่เอกสารควบรวมต้องยอมรับจุดยึดที่เครื่องมือต้นทางเขียนไว้ และเผยแพร่จุดยึดใหม่ให้ลิงก์ปลายทางสามารถกำหนดเป้าหมายมาได้
var
PageIdx: Integer;
begin
// Honor an anchor another tool wrote into /Names /Dests.
PageIdx := Pdf.ResolveLoadedNamedDestination('chapter.3.figures');
if PageIdx >= 0 then
Pdf.AddLoadedOutline(-1, PageIdx, 'Chapter 3: Figures', 0, 792, 0);
// Publish a new anchor for other links to target.
if Pdf.AddLoadedNamedDestination('appendix.glossary', 42, 0, 792) then
Pdf.SaveLoadedDocument('anchored.pdf');
end;
ข้อจำกัดทางตรง: ปลายทางที่ HotPDF เขียนขึ้น ทั้งสำหรับบุ๊กมาร์กและจุดยึดระบุชื่อ จะเป็นปลายทางประเภท /XYZ ส่วนประเภทเฉพาะเจาะจงอื่นๆ ที่ข้อกำหนดกำหนดไว้ เช่น /Fit, /FitH, /FitB และประเภทอื่นๆ จะไม่ถูกนำออกมาจากการเรียกฟังก์ชันเหล่านี้ แม้ว่า /XYZ ที่มีค่าการซูมเป็นศูนย์จะครอบคลุมการใช้งานหลักของ "ไปที่จุดนี้และรักษาระดับการซูมของฉัน" ได้แล้วก็ตาม และฟังก์ชัน GetLoadedBookmarkPageIndex(Title) ซึ่งช่วยค้นหาที่อำนวยความสะดวกโดยระบุชื่อเรื่องบุ๊กมาร์กและรับค่าหน้าปลายทางกลับมา จะสแกนเฉพาะระดับบนสุดของทรีเค้าร่างเท่านั้น ดังนั้นให้ใช้เพื่อตรวจสอบรายการระดับบทที่คุณเพิ่งสร้างขึ้น แทนที่จะใช้กับโหนดย่อยที่ซ้อนอยู่ลึกๆ
กับดัก /Count: มันนับโหนดลูกหลาน ไม่ใช่นับรายการ
คีย์ /Count บนรูทของเค้าร่างไม่ได้หมายถึงสิ่งที่คุณคิด และสมมติฐานที่ผิดจะส่งผลให้การทดสอบล้มเหลวซึ่งดูเหมือนการทุจริตของข้อมูล ข้อกำหนด ISO 32000-1 §12.3.3 นิยาม /Count ว่าเป็นผลรวมของ โหนดลูกหลานที่มองเห็นได้ (visible descendants) ในทุกระดับชั้น ไม่ใช่จำนวนรายการระดับบนสุด ในแต่ละรายการตัวเครื่องหมายยังบอกความหมายด้วย: ค่าบวกหมายถึงรายการนั้นกำลังเปิดอยู่และจะแสดงจำนวนโหนดลูกในแถบข้าง ขณะที่ค่าลบหมายถึงรายการนั้นถูกย่อเก็บไว้ และมีโหนดลูก |N| รายการถูกซ่อนอยู่ภายใน
สิ่งนี้ถูกพบจากชุดทดสอบการถดถอย (regression suite) ของ HotPDF ในขณะที่สร้าง APIs บุ๊กมาร์กสำหรับเอกสารที่โหลดขึ้นมา เอกสารทดสอบมีบุ๊กมาร์กเดี่ยวระดับบนสุด 3 รายการ และมีรายการย่อย 1 รายการ; หลังจากลบรายการระดับบนสุดออก 1 รายการ ฟังก์ชัน GetLoadedOutlineCount ซึ่งอ่านค่า /Count จากรูท ได้รายงานค่าเป็น 1 แทนที่จะเป็น 2 ตามที่คาดหมาย ไม่มีข้อมูลใดเสียหาย: ค่า 3 เริ่มแรกไม่เคยเป็น "รายการระดับบนสุด 3 รายการ" มาก่อน และการคำนวณที่ถูกต้องจะต้องสำรวจสายสัมพันธ์ระดับบนสุดเพื่อสะสมค่าแต่ละโหนดบวกกับค่า /Count ที่เป็นบวก และข้ามโหนดลูกของโหนดที่ย่อเก็บไว้ บทเรียนสำหรับโค้ดของคุณคือ: อย่าเพิ่งยืนยัน (assert) ค่าการเปลี่ยนแปลง /Count หลังการแก้ไขโครงสร้าง เนื่องจากค่าจะเคลื่อนไหวในลักษณะที่ไม่เป็นเชิงเส้นตามโหนดลูกหลานที่มองเห็นได้ ให้ตรวจสอบโครงสร้างโดยดูที่เนื้อหาแทน
// After edits, verify by title lookup, not by /Count deltas.
if Pdf.GetLoadedBookmarkPageIndex('Chapter 1: Installation') = 0 then
ShowMessage('Outline verified');
สิ่งนี้เหมาะสมกับเครื่องมือจัดชุดเอกสารที่โหลดมาตรงไหน
การสร้างบุ๊กมาร์กและปลายทางระบุชื่อจะช่วยเติมเต็มระบบการทำงานที่โหลดเอกสาร-แก้ไข-เซฟ ซึ่ง API ได้พยายามพัฒนามาโดยตลอด: ซึ่งวงจรรูปแบบนี้รองรับการเขียนข้อมูลเมตาของเอกสารใหม่, การเพิ่มคำอธิบายประกอบลิงก์ และการส่งผ่านคำอธิบายประกอบแบบไปกลับด้วยการนำเข้าและส่งออก XFDF รูปแบบของระบบจะมีความเป็นแบบเดียวกัน ได้แก่ LoadFromFile, ลำดับการเรียกใช้คำสั่ง *Loaded* เทียบกับกราฟวัตถุในหน่วยความจำ จากนั้นเรียก SaveLoadedDocument ซึ่งช่วยให้สามารถรวมขั้นตอนการสร้างเค้าร่างเข้ากับกระบวนการควบรวมหรือกระบวนการปั๊มตราเดิมที่มีอยู่แล้วได้อย่างสะดวกสบายเป็นอีกหนึ่งขั้นตอนก่อนการเซฟ
APIs ที่เรานำมาคุยกันที่นี่ ได้แก่ AddLoadedOutline, SetLoadedOutlineDestination, SetLoadedOutlineURI, ResolveLoadedNamedDestination, AddLoadedNamedDestination และ GetLoadedBookmarkPageIndex มีจัดส่งมาพร้อมกับ HotPDF Component มาตรฐานสำหรับ Delphi และ C++Builder โดยไม่มีความเกี่ยวข้องกับบริการภายนอกใดๆ และไม่ต้องรันโปรแกรมอ่านแยกต่างหาก