เมื่อคุณเปิดเอกสาร PDF ที่สร้างขึ้นโดย Microsoft Word หรือ Excel และเปิดอ่านผ่าน ๆ ก็ดูไม่มีอะไรผิดปกติ เมื่อโหลดเข้าไปในโปรแกรม Delphi และอ่านค่าจำนวนหน้ากลับมา ตัวเลขที่ได้ก็ถูกต้อง แต่เมื่อคุณลองบันทึกไฟล์นั้นซ้ำโดยเปิดใช้งานการเข้ารหัส งานการบันทึกกลับล้มเหลวด้วยข้อผิดพลาด EListError หรือผลลัพธ์เปิดขึ้นมาพร้อมคำเตือนว่าตารางการอ้างอิงไขว้ (cross-reference) เสียหาย ที่จริงไฟล์นี้ไม่ได้เสียหายเลย แต่มันคือไฟล์แบบอ้างอิงไฮบริด (hybrid-reference file) และโครงสร้างการจัดรูปแบบที่ยอมให้โปรแกรมอ่านรุ่นเก่าอายุสิบห้าปีเปิดอ่านได้ ก็คือโครงสร้างตัวเดียวกับที่ทำให้ตัวโหลดที่หยุดอ่านเร็วเกินไปทำงานล้มเหลว
นี่เป็นวิธีที่พบบ่อยที่สุดวิธีหนึ่งที่ระบบประมวลผล PDF ที่เคยผ่านการทดสอบภายในมาทุกการทดสอบ ต้องมาเจอกับไฟล์ที่ไม่สามารถบันทึกและเปิดซ้ำได้ (cannot round-trip) ไฟล์อินพุตทั้งหมดที่ระบบสร้างขึ้นภายในองค์กรมักไม่เคยเป็นแบบไฮบริด ไฟล์ไฮบริดไฟล์แรกมักจะเดินทางมาถึงในวันที่ลูกค้าส่งต่อใบแจ้งหนี้ที่ส่งออกมาจากสเปรดชีตมาให้
สิ่งที่ Word และ Excel เขียนจริง ๆ
ISO 32000-1 อธิบายโครงสร้างแบบอ้างอิงไฮบริดไว้ใน §7.5.8.4 แอปพลิเคชันที่ต้องการใช้ฟีเจอร์ของ PDF 1.5 เช่น สตรีมของอ็อบเจกต์ (object streams) แต่ยังคงต้องการให้โปรแกรมอ่าน PDF 1.4 สามารถเปิดไฟล์ได้ จะเขียนข้อมูลการอ้างอิงไขว้เป็นสองชุดด้วยกัน ชุดแรกคือตารางการอ้างอิงไขว้แบบคลาสสิก ซึ่งเป็นแถวข้อความ ASCII ที่มีความกว้างคงที่ซึ่งอยู่ท้ายไฟล์ PDF ทุกตัวจนถึงเวอร์ชัน 1.4 และชุดที่สองคือสตรีมการอ้างอิงไขว้ (cross-reference stream) ที่ระบุดัชนีของข้อมูลส่วนที่เหลือ ส่วนท้าย (trailer) ของเซกชันแบบคลาสสิกจะมีค่ารายการ /XRefStm ซึ่งเก็บค่าออฟเซตไบต์ของสตรีมนั้น
การแบ่งหน้าที่เช่นนี้เป็นความตั้งใจ อ็อบเจกต์ที่โปรแกรมอ่านรุ่นเก่าจำเป็นต้องเข้าถึง รวมถึงแคตตาล็อกและโครงสร้างหน้า (page tree) จะสามารถเรียกแอดเดรสได้จากตารางแบบคลาสสิก ส่วนอ็อบเจกต์ที่ถูกรวมไว้ในสตรีมอ็อบเจกต์แบบบีบอัดจะถูกทำเครื่องหมายว่าว่าง (free) ในตารางแบบคลาสสิกด้วยรายการประเภท f เพื่อให้โปรแกรมอ่านรุ่น 1.4 ข้ามส่วนเหล่านี้ไปได้เลยและไม่ต้องประสบปัญหับโครงสร้างที่มันไม่สามารถวิเคราะห์ได้ ตำแหน่งที่แท้จริงของอ็อบเจกต์เหล่านี้จะอยู่ในสตรีมการอ้างอิงไขว้เท่านั้น ลายเซ็นของไฟล์ลักษณะนี้คือส่วนท้ายของมัน: เซกชันแบบคลาสสิกสั้น ๆ ซึ่งบ่อยครั้งไม่มีอะไรมากไปกว่าคีย์เวิร์ด xref ตามด้วยหัวข้อเซกชันย่อย 0 0 และมีส่วนท้ายชี้ไปที่ /XRefStm ซึ่งเป็นจุดเก็บข้อมูลสำหรับกู้คืนที่แท้จริง
ทำไมการนับจำนวนหน้าได้ถูกต้องจึงไม่ได้พิสูจน์อะไรเลย
เนื่องจากแคตตาล็อกและโครงสร้างหน้านั้นตั้งใจออกแบบมาให้เข้าถึงได้จากตารางแบบคลาสสิก ตัวโหลดที่อ่านเฉพาะตารางนั้นจึงยังคงค้นพบข้อมูล /Root ไล่เรียงโครงสร้างหน้า และรายงานจำนวนหน้าที่ถูกต้องกลับมา ทุกสิ่งที่โปรแกรมอ่านรุ่นเก่าต้องการยังมีอยูอย่างครบถ้วน ไฟล์จึงดูเหมือนว่าสมบูรณ์ดี แต่อ็อบเจกต์ที่หายไปคืออ็อบเจกต์ที่แพ็กรวมอยู่ในสตรีมอ็อบเจกต์: พจนานุกรมฟิลด์ของ AcroForm, องค์ประกอบโครงสร้างของ tagged-PDF และพจนานุกรมขนาดเล็กอื่น ๆ อีกมากมายที่ไม่จำเป็นต้องปรากฏให้โปรแกรมอ่านรุ่นเก่ามองเห็น
คุณจะไม่ทันสังเกตเห็นความผิดปกตินี้จนกว่าจะมีอะไรบางอย่างไปเรียกใช้อ็อบเจกต์เหล่านั้น และการบันทึกไฟล์ใหม่ทั้งหมดแบบเต็มรูปแบบ (full resave) จะเรียกใช้อ็อบเจกต์ทุกตัว การไล่อ่านเอกสารเพื่อทำการเข้ารหัสหรือเขียนทับใหม่คือตัวการหลักที่ต้องร้องขอหมายเลขอ็อบเจกต์ทีละตัวตามลำดับ ซึ่งเป็นเหตุผลที่ว่าทำไมปัญหาจึงมาแสดงอาการในขั้นตอนการบันทึกไฟล์แทนที่จะเป็นตอนโหลด และอยู่ห่างไกลจากสาเหตุที่แท้จริง
กับดักคือตัวตรวจสอบที่เห็น xref แล้วหยุดทำงานทันที
วิธีที่ง่ายและรวดเร็วที่สุดในการระบุการทำดัชนีของไฟล์คือการค้นหาตามตำแหน่ง startxref และตรวจสอบข้อมูลไบต์แรกที่ชี้ไป คีย์เวิร์ด xref จะหมายถึงตารางอ้างอิงแบบคลาสสิก ส่วนอ็อบเจกต์สตรีมจะหมายถึงสตรีมการอ้างอิงไขว้ การทดสอบดังกล่าวจะถูกต้องเสมอสำหรับไฟล์ที่ใช้รูปแบบการทำดัชนีเพียงอย่างใดอย่างหนึ่ง แต่มันจะทำงานผิดพลาดสำหรับไฟล์แบบไฮบริด ซึ่งค่า startxref จะมุ่งเป้าไปที่เซกชันแบบคลาสสิกด้วยจุดประสงค์เดียวคือต้องการให้สามารถรองรับโปรแกรมอ่านรุ่นเก่าได้ ในขณะที่ /XRefStm ในส่วนท้ายของเซกชันนั้นคือจุดที่ใช้ทำดัชนีเอกสารส่วนใหญ่อย่างแท้จริง ตัวตรวจสอบที่ส่งคืนค่ากลับมาว่า "classic" ทันทีที่เจอคำว่า xref ตัวแรกจะไม่เคยอ่านค่า /XRefStm เลย และส่งผลให้อ็อบเจกต์ทุกตัวที่อยู่ในสตรีมกลายเป็นสิ่งที่มองไม่เห็น
var
Pdf: THotPDF;
PageCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
PageCount := Pdf.LoadFromFile('Invoice_XLS.pdf'); // count is correct
// inspect or edit the loaded document here
Pdf.SaveLoadedDocument('Invoice_secured.pdf'); // walks every object
finally
Pdf.Free;
end;
end;
หากตัวตรวจสอบทำงานแบบยุติก่อนกำหนด การโหลดไฟล์จะดูปกติสมบูรณ์ดี แต่ขั้นตอนการบันทึกไฟล์ใหม่จะเป็นจุดที่อ็อบเจกต์ที่ขาดหายไปเริ่มแสดงปัญหาออกมา การแก้ไขไม่ใช่การอ่านข้อมูลไบต์ตอนเริ่มต้นให้มากขึ้น แต่คือการระบุโครงสร้างส่วนท้ายแบบไฮบริดให้ได้ และติดตามอ่านข้อมูลจาก /XRefStm ก่อนที่จะสรุปว่าวิเคราะห์ไฟล์เสร็จสิ้น
ลำดับการผสานข้อมูลที่เปลี่ยนแปลงไม่ได้
เมื่ออ่านดัชนีทั้งสองชุดมาแล้ว ขั้นตอนการนำมารวมกันจะสามารถทำได้ในทิศทางเดียวเท่านั้น โดยจะต้องผสานสตรีมการอ้างอิงไขว้เข้ามาก่อน แล้วจึงนำข้อมูลแบบคลาสสิกมาเติมทับรอบ ๆ เหตุผลคือกุศโลบายที่ซ่อนอยู่ใจกลางโครงสร้างรูปแบบนี้ ไฟล์แบบไฮบริดจะทำเครื่องหมายอ็อบเจกต์แบบบีบอัดของมันว่าว่าง (free) ในตารางแบบคลาสสิกเพื่อให้โปรแกรมอ่านรุ่นเก่าข้ามไป ตัวโหลดที่ยึดหลักการ "เจอตัวแรกถือว่าชนะ" และเลือกอ่านตารางแบบคลาสสิกก่อนจะบันทึกหมายเลขอ็อบเจกต์เหล่านั้นว่าว่าง จากนั้นจะทิ้งข้อมูลนำเข้าจากสตรีมที่ระบุตำแหน่งอ็อบเจกต์จริง ๆ เนื่องจากช่องข้อมูลถูกจองไปแล้ว แต่หากสลับลำดับใหม่ รายการข้อมูลประเภท 2 จากสตรีม (ซึ่งประกอบด้วยหมายเลขสตรีมอ็อบเจกต์รวมถึงค่าดัชนี) จะเข้าจองช่องข้อมูลที่มันควรจะเป็น และข้อมูลแบบคลาสสิกจะถูกนำมาเติมเข้าในช่องที่เหลือแทน
หลักการทำงานเดียวกันนี้ยังช่วยป้องกันไม่ให้ประวัติเวอร์ชันเก่าดึงอ็อบเจกต์ที่ถูกลบไปแล้วกลับขึ้นมาใช้งานใหม่ การอัปเดตแบบเพิ่มทีละส่วน (incremental updates) จะเชื่อมโยงย้อนกลับผ่านคีย์ /Prev และรายการว่างประเภท 0 คือตัวแจ้งเตือนว่าส่วนที่ใหม่กว่าได้ประกาศเลิกใช้งานหมายเลขอ็อบเจกต์นั้นแล้ว เซกชันที่เก่ากว่าซึ่งอยู่ถัดไปในลิงก์จะต้องไม่ได้รับอนุญาตให้เขียนทับตัวแจ้งเตือนนั้นด้วยตำแหน่งข้อมูลที่ล้าสมัย หากเรายึดข้อมูลที่เจอตัวแรกเป็นหลักสำหรับการทำเครื่องหมายข้อมูลว่าง อ็อบเจกต์ที่ถูกลบไปแล้วก็จะยังคงถูกลบต่อไป แต่หากจัดการปัญหานี้อย่างไม่ระมัดระวัง ประวัติการทำงานเก่าของตัวไฟล์เองอาจจะดึงเนื้อหาที่การอัปเดตล่าสุดสั่งลบไปแล้วกลับคืนมา
ความหมายใน HotPDF
ตัวเอ็นจิ้นจะช่วยจัดการและแก้ไขปัญหาไฟล์อ้างอิงไฮบริดให้คุณโดยอัตโนมัติในทุกเส้นทางการทำงานที่จำเป็นต้องวิเคราะห์ข้อมูลการอ้างอิงไขว้ ไม่ว่าคุณจะโหลดเอกสารด้วย LoadFromFile หรือ LoadFromStream ปรับแต่งค่า และเรียกใช้ SaveLoadedDocument หรือแม้แต่การรันคำสั่งแบบครั้งเดียวจบ เช่น EncryptFile ที่อ่านอินพุตและเขียนเอาต์พุตออกไป ในทุก ๆ กรณีระบบจะทำการอ่านข้อมูลจาก /XRefStm เสมอ และผสานเซกชันสตรีมก่อนรายการแบบคลาสสิก เพื่อวิเคราะห์อ็อบเจกต์ที่อยู่ในสตรีมก่อนที่ตัวเขียนจะระบุรายการออกมา พาธการเข้ารหัสด้วย AES-256 คือจุดที่ปัญหานี้แสดงให้เห็นชัดเจนที่สุด เนื่องจากกระบวนการเข้ารหัสเอกสารจำเป็นต้องเขียนอ็อบเจกต์ทุกตัวใหม่ ซึ่งต้องอาศัยการระบุตำแหน่งของอ็อบเจกต์ทุกตัวให้เรียบร้อยก่อน
// One-shot: read the hybrid input, write an AES-256 encrypted copy
Pdf.EncryptFile('Letter_DOC.pdf', 'Letter_secured.pdf',
'owner-secret', '', aes256, [prPrint, prFillAnnotations]);
รายละเอียดสำคัญที่คุณควรจดจำอยู่ก่อนการเรียกใช้ API ไฟล์ที่ส่งมาจาก Word, Excel, PowerPoint และระบบส่งออกไฟล์ผ่านคำสั่ง "Save as PDF" อื่น ๆ อีกจำนวนมาก มักจะเป็นไฟล์แบบไฮบริดเป็นประจำ ดังนั้น ตัวโหลดที่คุณทำการทดสอบเฉพาะกับผลลัพธ์จากตัวสร้างของคุณเองอาจจะไม่เคยเจอปัญหานี้เลยในการทดสอบ ควรเตรียมชุดข้อมูลทดสอบ (fixtures) ของคุณด้วยเอกสารที่ส่งออกจริงจากแอปพลิเคชันตระกูล Office ด้วย ไม่ใช่มีเพียงแค่ไฟล์ที่สร้างขึ้นมาจากโค้ดของคุณเองเท่านั้น
การตรวจสอบไฟล์ที่คุณสงสัย
มีวิธีการตรวจสอบสองวิธีเพื่อหาคำตอบได้อย่างรวดเร็ว วิธีแรกคือเปิดไฟล์ในรูปแบบมุมมองเลขฐานสิบหก (hex view) และอ่านค่าไบต์หลังจากตำแหน่ง startxref ตัวสุดท้าย ไฟล์แบบไฮบริดจะแสดงเซกชันแบบคลาสสิกสั้น ๆ ซึ่งในพจนานุกรมส่วนท้าย (trailer dictionary) จะมีรายการ /XRefStm หรือวิธีที่สองคือเปรียบเทียบจำนวนอ็อบเจกต์ที่ได้จากการวิเคราะห์ทั้งหมดกับหมายเลขอ็อบเจกต์สูงสุดที่ /Size ประกาศไว้ในส่วนท้าย ความแตกต่างของตัวเลขที่ห่างกันมากหมายความว่ามีอ็อบเจกต์ซ่อนอยู่ในสตรีมที่ตัวโหลดยังไม่ได้เปิดอ่าน ซึ่งเป็นความบกพร่องแบบเดียวกับที่จะกลายมาเป็นความล้มเหลวในขั้นตอนการบันทึกไฟล์ในภายหลัง
ในด้านของกระบวนการเขียนว่าสตรีมอ็อบเจกต์และการอ้างอิงไขว้แบบบีบอัดถูกสร้างขึ้นมาได้อย่างไรนั้น สามารถอ่านเพิ่มเติมได้ใน บทความของเราเกี่ยวกับสตรีมอ็อบเจกต์และการอัปเดตแบบเพิ่มทีละส่วน และเมื่อไฟล์ไฮบริดที่พบมีขนาดใหญ่มาก เทคนิคการโหลดใน บทความการใช้งาน Direct File API สำหรับการประมวลผล PDF ขนาดใหญ่ จะช่วยให้คุณตรวจสอบไฟล์ได้โดยไม่ต้องโหลดเนื้อหาทั้งหมดเข้าไปในหน่วยความจำ ทั้งสองอย่างนี้ผสานเข้ากับฟังก์ชันการกู้คืนข้อมูลที่อธิบายไว้ที่นี่ ซึ่งพร้อมใช้งานในฐานะส่วนหนึ่งของ HotPDF Component สำหรับ Delphi และ C++Builder ร่วมกับ API สำหรับการโหลด การแก้ไข การเข้ารหัส และการลงนามที่อธิบายในบล็อกนี้