PDFlibPas ให้นักพัฒนา Delphi และ C++Builder action สามประเภทสำหรับการนำทางที่ทิ้งหน้าปัจจุบันไว้เบื้องหลัง คือ GoToR (Go To Remote) เปิดหน้าเจาะจงในไฟล์ PDF อื่น, GoToE (Go To Embedded) เปิดไฟล์ PDF ที่ฝังอยู่ภายในเอกสารปัจจุบัน และ Launch รันโปรแกรมภายนอกหรือเปิดไฟล์ผ่าน shell ของระบบปฏิบัติการ ทั้งสามอยู่ใน ISO 32000-1 §12.6.4 ส่วน Action Types ที่นิยาม action แบบ GoTo ทั่วไปด้วย และแต่ละตัวพกกับดักของตัวเองสำหรับผู้ไม่ระวัง คือหมายเลขหน้าที่มีความหมายต่างกันขึ้นอยู่กับว่าการเรียกไหนสร้างมัน, เป้าหมายที่เป็นชื่อไม่ใช่ path ไฟล์ และคู่พารามิเตอร์ string ที่ดูเหมือนกันแต่ให้บริการ viewer สองแบบที่ต่างกัน
ไม่มีอะไรในนี้เป็นสมมติฐาน ชุดเอกสารอ้างอิงทางเทคนิค คู่มือหลัก, PDF สเปคที่ผู้จัดจำหน่ายอัปเดตตามตารางของตัวเอง, เครื่องมือ calibration ที่ติดตั้งควบคู่กับทั้งสอง พึ่งพาการต่อสายข้ามเอกสารประเภทนี้พอดี การอ้างอิงข้ามที่ต้องลงเอยที่หน้า 5 ของไฟล์สเปค, data sheet ที่คุ้มค่าจะส่งไปภายในคู่มือแทนที่จะอยู่ข้างๆ มัน, ลิงก์ที่ส่งตรงไปยังเครื่องมือ calibration บทความนี้เป็นภาพสะท้อนของการอ่าน action ของ bookmark และ annotation กลับออกจาก PDF ที่มีอยู่แล้ว ชิ้นนั้นครอบคลุมการอ่าน action แบบ GoToR, Launch หรือ GoToE ที่ producer อื่นเขียนเข้าไฟล์ไว้แล้ว ชิ้นนี้ครอบคลุมการสร้าง action สามประเภทเดียวกันนั้นตั้งแต่ต้น รวมถึงกฎระดับฟิลด์ที่ PDFlibPas บังคับใช้ก่อนที่มันจะ commit ไบต์เดียว
สามวิธีที่ action ของ PDF ทิ้งหน้าปัจจุบันไว้เบื้องหลัง
PDFlibPas แยกการนำทางภายในออกจากทุกอย่างอื่นที่ key /S ของ action และ GoToR, GoToE และ Launch เป็นสาม subtype ที่เป้าหมายของมันอยู่นอกหน้าปัจจุบัน คือ GoToR ภายใต้ ISO 32000-1 §12.6.4.3, GoToE ภายใต้ §12.6.4.4 และ Launch ภายใต้ §12.6.4.5 ทั้งหมดอยู่ในส่วน §12.6.4 Action Types ที่กว้างกว่าซึ่งนิยาม action GoTo ทั่วไปด้วย ปลายทางของ action GoTo ธรรมดาระบุ page object ที่มีอยู่แล้วภายในเอกสาร ดังนั้น PDFlibPas จึงตรวจสอบมันได้ทันที GoToR และ GoToE ทำแบบนั้นไม่ได้ในแบบเดียวกัน เพราะไฟล์ภายนอกอาจไม่มีอยู่แม้แต่บนเครื่องนี้ด้วยซ้ำ และจำนวนหน้าของไฟล์ที่ฝังไว้ก็ไม่ใช่สิ่งที่เอกสาร host ติดตาม ดังนั้นทั้งคู่จึงพกการอ้างอิงที่ยังไม่ resolve แทนที่จะเป็นลิงก์แข็ง คือ file specification บวกปลายทางสำหรับ GoToR, ชื่อไฟล์ที่ฝังไว้บวกหน้าเป้าหมายสำหรับ GoToE ในขณะที่ Launch ทิ้งแนวคิดปลายทางไปทั้งหมด และแค่ระบุชื่ออะไรบางอย่างให้ระบบปฏิบัติการรันหรือเปิด การแยกนั้นปรากฏเป็นสองตระกูลการเรียกในฝั่งเขียน builder ระดับสูงแบบเรียกครั้งเดียว เช่น AddLinkToFile, AddLinkToFileEx, AddLinkToEmbeddedPDF และ AddLinkToLocalFile สร้าง link annotation แบบ page-hotspot และ action ของมันด้วยกัน ครอบคลุม layout จริงส่วนใหญ่ บรรทัดข้อความหรือไอคอนที่ผู้อ่านคลิก ในขณะที่ setter ระดับต่ำกว่าเช่น SetActionRemoteDestinationEx, SetActionLaunchOptions และคู่หู AddActionNext* ของพวกมัน ผูกหรือแทนที่ action บนสิ่งที่คุณถือ handle อยู่แล้ว bookmark ที่มีอยู่, trigger ฟิลด์ฟอร์ม หรือ event วงจรชีวิตระดับเอกสารหรือหน้า ทั้งสองตระกูลลงเอยด้วยการเขียนรูปร่าง dictionary เดียวกัน ความแตกต่างคือคุณยืนอยู่ตรงไหนเมื่อเรียกมัน และตามที่ส่วนถัดไปครอบคลุม หมายเลขหน้าหมายถึงอะไรเมื่อคุณทำแบบนั้น
คุณสร้างลิงก์ GoToR ที่เปิดหน้าในไฟล์ PDF อื่นได้อย่างไร
action GoToR ต้องการสองอย่าง คือ file specification และปลายทางภายในไฟล์นั้น และ PDFlibPas เปิดการเรียกที่ต่างกันสองแบบสำหรับส่วนที่สอง แต่ละแบบมีข้อตกลงการกำหนดหมายเลขหน้าของตัวเอง AddLinkToFile และ AddLinkToFileEx ซึ่งเป็น builder page-hotspot ระดับสูง ตรวจสอบอาร์กิวเมนต์ Page หรือ DestPage ของมันว่ามากกว่าศูนย์ ซึ่งเป็นการกำหนดหมายเลขเริ่มที่ 1 เดียวกับที่ PDFlibPas ใช้ทุกที่อื่น รวมถึง SelectPage SetActionRemoteDestinationEx ซึ่งเป็น setter ระดับต่ำกว่าที่ใช้ผูกหรือแทนที่ action GoToR บนสิ่งที่คุณมี handle อยู่แล้ว กลับตรวจสอบ DestPage ว่ามากกว่าหรือเท่ากับศูนย์แทน และเขียนมันตรงเข้า array ปลายทางที่ชัดเจนของ action โดยไม่ปรับแต่งเลย มันต้องการดัชนีหน้าดิบเริ่มที่ศูนย์ของเอกสารเป้าหมาย ซึ่งเป็นการกำหนดหมายเลขที่ ISO 32000-1 ระบุสำหรับปลายทางที่ชัดเจนแบบ remote เรียก setter ระดับต่ำด้วยตัวเลขเดียวกับที่คุณจะให้ builder ระดับสูง แล้วลิงก์จะเปิดเร็วไปหนึ่งหน้า
var
Lib: TPDFlib;
ActionID: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('manual.pdf', '') = 1 then
begin
Lib.SelectPage(12);
// Page is 1-based here, same as SelectPage above: this opens
// the fifth page of specs.pdf.
Lib.AddLinkToFile(72, 700, 200, 16, 'specs.pdf', 5, 0, 0, 0);
// A later maintenance pass repoints the same link at a
// reorganized file. SetActionRemoteDestinationEx edits the
// action directly, and DestPage here is the zero-based index
// PDF itself uses for a remote explicit destination -- "the
// fifth page" is now 4, not 5.
ActionID := Lib.GetAnnotActionID(1);
Lib.SetActionRemoteDestinationEx(ActionID, 'specs-2026.pdf',
4, Ord(dkFit), 0, 0, 0, 0, 0, 0, -1);
end;
finally
Lib.Free;
end;
end;
อาร์กิวเมนต์ที่เหลือของ SetActionRemoteDestinationEx ก็ตรงตัวเช่นกัน ValueMask เป็นเซตบิต คือ 1 สำหรับซ้าย, 2 สำหรับบน, 4 สำหรับขวา, 8 สำหรับล่าง, 16 สำหรับ zoom และ PDFlibPas ตรวจสอบมันเทียบกับ DestType ก่อนที่จะเขียนอะไร ปลายทาง dkFitR ต้องให้ 15 เป๊ะ (สี่ขอบทั้งหมด ไม่มี zoom), dkFit และ dkFitB ต้องให้ 0 และ dkFitH/dkFitV รับแค่พิกัดที่เกี่ยวข้องหนึ่งตัวของมันเท่านั้น บิตที่คุณปล่อยไม่ตั้งไว้ภายใน mask ที่ถูกต้องอยู่แล้วไม่ได้ถูกละไว้จาก array มันถูกเขียนเป็น null ของ PDF ที่ชัดเจน ซึ่ง ISO 32000-1 ปฏิบัติเป็น "คงค่าใดก็ตามที่ viewer มีอยู่แล้ว" สำหรับพิกัดนั้น เป็นวิธีที่ถูกต้องในการบอกว่า "กระโดดไปหน้านี้ อย่าแตะ zoom" มากกว่าจะเป็นการมองข้าม zoom เองถูกเก็บเป็นเศษส่วนของค่าที่คุณส่งเข้ามา ดังนั้นการเรียกที่ขอ 150 เปอร์เซ็นต์จะส่งค่าที่เก็บไว้เป็น 1.5 เข้า array และช่วงอินพุตที่ถูกต้องคือ 0 ถึง 6400
คุณลิงก์ไปยัง PDF ที่ฝังอยู่ภายในเอกสารของคุณเองได้อย่างไร
AddLinkToEmbeddedPDF สร้าง action GoToE และอาร์กิวเมนต์เป้าหมายของมัน EmbeddedFileName เป็นชื่อไม่ใช่ path มันต้องตรงกับ string Title ที่ส่งให้ EmbedFile ไปแล้วตอนที่ไฟล์แนบถูกสร้าง เพราะ title นั้นเป็น key ตรงตัวที่ PDFlibPas เก็บไว้ใน name tree /EmbeddedFiles ของเอกสาร และ GoToE resolve ด้วยการค้นหาชื่อนั้น ไม่ใช่การแตะ filesystem อีกครั้ง ฟังก์ชันแค่ตรวจสอบว่า EmbeddedFileName ไม่ว่างเปล่าและ TargetPage อย่างน้อย 1 เท่านั้น ส่งชื่อที่ไม่เคยถูกฝังไว้จริงๆ เข้าไป แล้วการเรียกก็ยังคงคืนความสำเร็จ action ยังคงถูกเขียน และลิงก์ก็แค่ resolve ไม่สำเร็จสำหรับทุกผู้อ่านที่คลิกมัน
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
Lib.NewPage;
// The Title argument becomes the key PDFlibPas stores in the
// document's EmbeddedFiles name tree -- that string, not
// "datasheet.pdf", is the target GoToE resolves against.
if Lib.EmbedFile('Datasheet', 'datasheet.pdf', 'application/pdf') = 1 then
Lib.AddLinkToEmbeddedPDF(72, 700, 200, 16, 'Datasheet', 3, 0, 0);
Lib.SaveToFile('manual.pdf');
finally
Lib.Free;
end;
end;
พื้นชั้นเวอร์ชันสองอันซ้อนกันตรงนี้ ไม่ใช่แค่อันเดียว EmbedFile ต้องการ PDF 1.4 สำหรับ name tree /EmbeddedFiles และ AddLinkToEmbeddedPDF ยกพื้นแยกต่างหากเป็น PDF 1.6 สำหรับประเภท action GoToE เอง ดังนั้นค่าต่ำสุดที่มีผลจริงสำหรับเอกสารใดก็ตามที่ใช้ฟีเจอร์นี้คือ 1.6 ไม่ใช่ 1.4 สังเกตด้วยว่า TargetPage ตรงนี้เริ่มที่ 1 ตามข้อตกลงปกติของ PDFlibPas ซึ่งขัดแย้งอย่างจงใจกับ DestPage เริ่มที่ศูนย์ที่ส่วนก่อนหน้าเพิ่งครอบคลุมไป และเป็นการเตือนว่ารูปแบบหมายเลขหน้าไหนที่ใช้ได้ขึ้นอยู่กับประเภท action และการเรียกที่เจาะจง ไม่ใช่กฎเดียวที่ใช้ครอบคลุมทั้งหมด dictionary เป้าหมายของ action ยังสามารถพก entry /R เป็น C สำหรับลูก หรือ P สำหรับแม่ รองรับ chain สองก้าวเข้าไปในไฟล์ที่ฝังไว้หรือกลับออกมายัง container ของมัน แม้ว่า AddLinkToEmbeddedPDF จะสร้างแค่ทิศทางลูกเท่านั้น เพราะนั่นคือทิศทางที่สมเหตุสมผลจากเอกสารที่กำลังฝังอยู่ ไม่ใช่ถูกฝัง
Action Launch: FileName เดียว สองเป้าหมาย string ที่ใช้แทนกันไม่ได้
SetActionLaunchOptions เขียนเป้าหมายไฟล์ของ action Launch เข้าสอง key ที่ต่างกันจากอาร์กิวเมนต์ FileName เดียว และสอง key นั้นถือ string สองประเภทที่ต่างกัน key /F ระดับบนสุดได้ dictionary file-specification สร้างขึ้นผ่านการแปลง path เดียวกับที่ PDFlibPas ใช้สำหรับ GoToR ซึ่งเป็นรูปแบบพกพาได้ที่ ISO 32000-1 §7.11.3 นิยามไว้สำหรับ dictionary file specification sub-dictionary /Win เมื่อ PDFlibPas เขียนตัวหนึ่ง จะได้ key /F ของตัวเองตั้งเป็นค่า FileName ดิบตรงตามที่ส่งเข้ามาเป๊ะ ไม่มีการแปลงเลย เพราะ /Win /F มีเอกสารประกอบใน ISO 32000-1 §12.6.4.5 ว่าเป็น string path Windows ธรรมดาที่มีไว้ให้ viewer แบบ Windows อ่านเท่านั้น ส่ง path แบบพกพาที่แปลงแล้วเข้าไปโดยคาดหวังว่าทั้งสอง key จะลงเอยเหมือนกัน แล้วสำเนา /Win จะพกอะไรก็ตามที่คุณส่งให้ฟังก์ชัน โดยไม่ถูกแตะเลย
var
Lib: TPDFlib;
ActionID: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('manual.pdf', '') = 1 then
begin
Lib.SelectPage(1);
Lib.AddLinkToLocalFile(72, 660, 220, 16, 'calibrate.exe', 0);
ActionID := Lib.GetAnnotActionID(1);
// Operation 0 leaves this as a normal open -- pass 1 to ask a
// Windows viewer to print instead. Parameters and
// DefaultDirectory only ever reach /Win /P and /Win /D, never
// the top-level /F.
Lib.SetActionLaunchOptions(ActionID, 'calibrate.exe',
'/silent /profile:default', 'C:\Tools\Calibration', 0, -1);
end;
finally
Lib.Free;
end;
end;
ปฏิบัติต่อ Launch เป็น action ที่มีแรงเสียดทานสูงที่สุดในสามตัว เพราะจุดประสงค์ทั้งหมดของมันคือการรันโปรแกรมหรือเปิดไฟล์นอก sandbox ของ PDF และ viewer หลักทุกตัวปฏิบัติต่อมันตามนั้น Enhanced Security ของ Adobe Acrobat บล็อคหรือถามก่อนสำหรับ action Launch โดยค่าเริ่มต้น เว้นแต่เป้าหมายจะอยู่ในตำแหน่งที่เชื่อถือได้อย่างชัดเจน และการติดตั้ง Acrobat ระดับองค์กรส่วนใหญ่ปล่อยการป้องกันนั้นเปิดอยู่ action Launch ในเอกสารที่ส่งให้สาธารณะจึงไม่ใช่ trigger ที่เชื่อถือได้ วางแผนให้มันถูกบล็อค, ถูกถาม หรือถูกเพิกเฉยอย่างเงียบๆ โดย viewer ใดก็ตามที่เปิดไฟล์ และเก็บมันไว้สำหรับสภาพแวดล้อมปิดที่คุณควบคุมการตั้งค่าความเชื่อถือของ viewer ด้วย เช่น kiosk ภายใน, การ rollout ขององค์กรที่ควบคุมได้ หรือเอกสารที่ไม่เคยออกจากเครื่องที่คุณจัดการ
ประตู PDF/A: ทำไมการเรียก GoToR และ Launch ถึงคืนค่าศูนย์ได้
SetActionRemoteDestinationEx และ SetActionLaunchOptions ทั้งคู่ปฏิเสธโดยตรงเมื่อเอกสารเป้าหมายอยู่ในโหมดความสอดคล้อง PDF/A ใดๆ ทั้งคู่ตรวจสอบโหมด PDF/A ของเอกสารเป็นเงื่อนไขแรกสุด และออกด้วยผลลัพธ์ 0 ก่อนที่จะแตะ action เลย ไม่มี exception ถูกยก นี่เป็นความจงใจ ข้อจำกัดของ PDF/A ต่อ action แบบโต้ตอบตัด Launch ออกไปโดยเฉพาะ เพราะการให้ไฟล์เก็บถาวรมีความสามารถรันโปรแกรมใดๆ เป็นพฤติกรรมที่ขึ้นกับสภาพแวดล้อมประเภทพอดีที่รูปแบบการเก็บถาวรระยะยาวมีอยู่เพื่อป้องกัน และ PDFlibPas ใช้ประตูอนุรักษ์นิยมเดียวกันกับ setter go-to แบบ remote ใน code path เดียวกัน ผลที่ตามมาในทางปฏิบัติพลาดได้ง่ายระหว่างการพัฒนา การเรียกเดียวกันเป๊ะที่ทำงานบน PDF ธรรมดาจะ compile, รัน และไม่ทำอะไรเลยอย่างเงียบๆ บนเอกสารที่โหลดด้วยระดับความสอดคล้อง PDF/A ที่ตั้งไว้ ดังนั้นตรวจสอบค่าที่คืนกลับมาแทนที่จะสมมติว่าสำเร็จ 0 ตรงนี้ไม่ใช่ข้อผิดพลาดของอินพุตที่ผิดรูปแบบ มันคือไลบรารีปฏิเสธคำขอที่ขัดแย้งกับคำอ้างความสอดคล้องของเอกสารเอง
GoToR, GoToE และ Launch เข้ากับ workflow ของ PDFlibPas ที่ใหญ่กว่าที่ไหน
action สามประเภทในบทความนี้ไม่ได้ไปถึงที่เดียวกันทั้งหมด บทความคู่กันเรื่อง trigger action วงจรชีวิตระดับเอกสารและหน้า ครอบคลุม SetDocumentAction และ SetPageAction ซึ่งสามารถผูก action GoToR หรือ Launch เข้ากับ trigger อย่าง WillClose ผ่านค่าคงที่ PDF_ACTION_BUILDER_REMOTE_DESTINATION และ PDF_ACTION_BUILDER_LAUNCH ที่ใช้ร่วมกัน ซึ่งเป็น builder เดียวกับที่ครอบคลุม trigger แบบ URI หรือ JavaScript ธรรมดาด้วย GoToE ไม่มีค่าคงที่แบบนั้นเลยและไม่มีเส้นทางเข้า builder ทั่วไปนั้นเลยด้วยซ้ำ AddLinkToEmbeddedPDF เป็นวิธีเดียวที่ PDFlibPas สร้างมันขึ้น ซึ่งทำให้มันเป็น action แบบ page-hotspot อย่างเคร่งครัด ไม่เคยเป็น trigger ระดับเอกสารหรือหน้าเลย ในที่ที่ GoToR และ Launch เข้าถึง builder ทั่วไปได้ การแลกเปลี่ยนคือการควบคุม มันสร้าง GoToR ที่ชี้ไปยังปลายทาง remote ที่มีชื่อเท่านั้น และ action Launch ที่มีแค่ชื่อไฟล์และพารามิเตอร์ ในขณะที่การระบุที่อยู่แบบหน้า-และ-ประเภท-fit ที่ชัดเจน และตัวเลือก launch เฉพาะ Windows ที่ครอบคลุมในบทความนี้ เข้าถึงได้แค่ผ่าน SetActionRemoteDestinationEx และ SetActionLaunchOptions โดยตรงเท่านั้น
มีคุณสมบัติด้านความปลอดภัยหนึ่งที่ควรรู้ก่อนที่จะสร้างเครื่องมือบำรุงรักษารอบ setter เหล่านี้ SetActionRemoteDestinationEx และ SetActionLaunchOptions สร้าง action ทดแทนทั้งหมดใน scratch dictionary ก่อน และแค่ลบและ copy key /F, /D หรือ /Win และ /NewWindow เข้า action ที่มีชีวิตก็ต่อเมื่อสำเนา scratch นั้นตรวจสอบผ่านแล้วเท่านั้น ดังนั้นการเรียกที่ล้มเหลวในการตรวจสอบ ไม่ว่าจะจาก ValueMask นอกช่วงหรือ FileName ว่างเปล่า จะทิ้ง action เดิม และ chain /Next ใดก็ตามที่แขวนอยู่กับมันแล้ว ไว้โดยไม่ถูกแตะเลย แทนที่จะถูกเขียนทับครึ่งหนึ่ง เรื่องนี้สำคัญเพราะ action GoToR และ Launch ทั้งคู่สามารถนั่งอยู่ภายใน chain /Next ที่สร้างด้วย AddActionNextRemoteDestinationEx, AddActionNextLaunchEx หรือ AddActionNextEx ที่ทั่วไปกว่า ทำให้ trigger เดียวยิง entry log JavaScript แล้วตามด้วยการกระโดด remote ตามลำดับได้ การสร้าง GoToR, GoToE และ Launch ตามที่อธิบายตรงนี้เป็นส่วนหนึ่งของPDFlibPas ไลบรารี PDF เนทีฟสำหรับ Delphi และ C++Builder