หน้า 1 ของ PDF ไม่ใช่วัตถุ 1 ความแตกต่างนี้คือแหล่งที่มาที่พบบ่อยที่สุดของบั๊กการดึงหน้าผิดลำดับในโปรแกรมแยกวิเคราะห์ (parsers) ของ PDF และวิธีแก้ปัญหาคือการอ่านข้อกำหนดมาตรฐาน (spec) แทนที่จะอ่านไบต์ของไฟล์
วัตถุ การอ้างอิง และแค็ตตาล็อก
ไฟล์ PDF คือชุดของวัตถุที่มีหมายเลขกำกับ วัตถุแต่ละชิ้นจะมีหมายเลขวัตถุและหมายเลขรุ่น (generation number) ที่ไม่ซ้ำกัน ซึ่งเขียนในรูปแบบ N G obj โดย G เกือบจะเป็น 0 เสมอในไฟล์ที่ไม่ได้รับการอัปเดตแบบเพิ่มส่วน (incrementally updated) วัตถุจะอ้างอิงถึงกันด้วยรูปแบบ N G R ดังนั้น 3 0 R จึงหมายถึง "เวอร์ชันปัจจุบันของวัตถุ 3" ส่วนท้าย (trailer) จะชี้ไปยังวัตถุแค็ตตาล็อกราก (root catalog object) ซึ่งมีรายการ /Pages ที่นำไปสู่โครงสร้างต้นไม้หน้าเว็บ สิ่งที่สามารถนำทางได้ทั้งหมดใน PDF จะเริ่มต้นจากรากนั้น ไม่ใช่จากไบต์แรกของส่วนเนื้อหาไฟล์
ตารางการอ้างอิงโยง (หรือสตรีมการอ้างอิงโยงใน PDF 1.5+) จะจับคู่หมายเลขวัตถุกับออฟเซ็ตของไฟล์ หน้าที่ของมันคือการเข้าถึงแบบสุ่ม (random access) ไม่ใช่การจัดลำดับ โปรแกรมเขียนที่สร้างเอกสารแบบเพิ่มส่วนสามารถเพิ่มวัตถุใหม่ที่ส่วนท้ายด้วยหมายเลขที่สูงกว่า ในขณะที่ตามตรรกะแล้ววัตถุเหล่านั้นมาก่อนวัตถุที่มีอยู่แล้วในลำดับหน้า นี่ไม่ใช่ข้อบกพร่อง แต่เป็นการออกแบบที่ตั้งใจไว้
โครงสร้างต้นไม้หน้าเว็บ (ISO 32000-1 §7.7.3)
ลำดับหน้าอาศัยอยู่ในโครงสร้างต้นไม้หน้าเว็บ แค็ตตาล็อกรากจะมีการอ้างอิง /Pages ที่ชี้ไปยังโหนดประเภท /Pages อาร์เรย์ /Kids ของโหนดนั้นจะแสดงรายการโหนดลูกตามลำดับการอ่าน โหนดลูกแต่ละโหนดอาจเป็นใบหน้า (leaf node) ประเภท /Page หรือโหนด /Pages ระดับกลางที่มีอาร์เรย์ /Kids เป็นของตัวเอง หน้า 1 คือใบหน้าแรกที่เข้าถึงได้จากการท่องไปในอาร์เรย์ Kids แบบ depth-first จากซ้ายไปขวา รายการ /Count บนโหนดกลางแต่ละโหนดจะแคชจำนวนรวมของใบหน้าที่สืบทอดมา ดังนั้นโปรแกรมอ่านจึงสามารถข้ามไปยังหน้า 500 ได้โดยไม่ต้องเดินดูทั้งต้นไม้
นี่คือลักษณะของโครงสร้างต้นไม้สามหน้าแบบมินิมอลในรูปแบบไวยากรณ์ PDF ดิบ:
16 0 obj
<<
/Type /Pages
/Count 3
/Kids [20 0 R 1 0 R 4 0 R]
/MediaBox [0 0 612 792]
>>
endobj
20 0 obj
<< /Type /Page /Parent 16 0 R /Contents 21 0 R /Resources 22 0 R >>
endobj
1 0 obj
<< /Type /Page /Parent 16 0 R /Contents 2 0 R /Resources 3 0 R >>
endobj
4 0 obj
<< /Type /Page /Parent 16 0 R /Contents 5 0 R /Resources 6 0 R >>
endobj
อาร์เรย์ Kids คือ [20 0 R, 1 0 R, 4 0 R] หน้าตามตรรกะที่ 1 คือวัตถุ 20, หน้าตามตรรกะที่ 2 คือวัตถุ 1, หน้าตามตรรกะที่ 3 คือวัตถุ 4 โค้ดใดก็ตามที่วนซ้ำหมายเลขวัตถุตั้งแต่ 1 ขึ้นไปจะพบสิ่งเหล่านี้ตามลำดับ 1, 4, 20 และสร้างลำดับ หน้า 2, หน้า 3, หน้า 1 เอกสารผลลัพธ์จะถูกแสดงผลในลำดับที่สลับกันซึ่งอาจดูปกติในโปรแกรมอ่านที่ปฏิบัติตามโครงสร้างต้นไม้ และผิดพลาดอย่างร้ายแรงในโปรแกรมอ่านที่ไม่ปฏิบัติตาม
การสืบทอด (Inheritance)
โหนดระดับกลางสามารถมีคุณสมบัติที่ลูกหลานสืบทอดมา รายการสืบทอดที่พบบ่อยที่สุดคือ /MediaBox (ขนาดหน้า), /CropBox, /Resources (ฟอนต์และรูปภาพ) และ /Rotate ใบหน้าที่ไม่ได้ระบุ /MediaBox ไม่ได้แปลว่าไฟล์พัง แต่มันจะดึงค่าจากโหนดบรรพบุรุษที่ใกล้ที่สุดที่กำหนดค่านั้นไว้ ส่วนหน้าที่ระบุ /MediaBox ไว้เองจะลบล้างค่าใด ๆ ก็ตามที่กำหนดไว้โดยโหนดแม่ สำหรับหน้านั้นโดยเฉพาะ
สิ่งนี้มีความสำคัญต่อการแยกวิเคราะห์ การอ่านวัตถุ /Page โดด ๆ และทึกทักเอาว่าคุณสมบัติของมันครบถ้วนแล้ว จะทำให้รายงานขนาดหน้าผิดพลาดสำหรับหน้าใด ๆ ที่อาศัยการสืบทอด โปรแกรมอ่านที่ถูกต้องจะเดินดูตามเชน /Parent เพื่อรวบรวมคุณสมบัติที่ยังไม่พบ และจะหยุดที่โหนดราก
โครงสร้างต้นไม้ที่ซ้อนกัน (Nested trees)
ไม่มีสิ่งใดในข้อกำหนดที่จำกัดโครงสร้างต้นไม้ให้อยู่แค่ระดับเดียว เอกสารขนาดใหญ่อาจจัดกลุ่มหน้าเว็บไว้ใต้โหนดระดับกลางที่สอดคล้องกับบทความอย่างคร่าว ๆ:
2 0 obj % root Pages node, Count = 8
<< /Type /Pages /Count 8 /Kids [3 0 R 4 0 R] >>
endobj
3 0 obj % first chapter, 5 pages
<< /Type /Pages /Parent 2 0 R /Count 5
/Kids [10 0 R 11 0 R 12 0 R 13 0 R 14 0 R]
/MediaBox [0 0 612 792] >>
endobj
4 0 obj % second chapter, 3 pages
<< /Type /Pages /Parent 2 0 R /Count 3
/Kids [20 0 R 21 0 R 22 0 R]
/MediaBox [0 0 612 792] >>
endobj
อัลกอริทึมในการท่องโครงสร้างเหมือนกัน: ไปยัง Kids ตามลำดับ เข้าไปในโหนด /Pages ใด ๆ และรวบรวมโหนดใบหน้า /Page ค่า /Count ช่วยให้โปรแกรมอ่านสามารถข้ามโครงสร้างย่อยทั้งหมดได้เมื่อต้องการกระโดดไปยังหน้าที่อยู่เลยออกไป ซึ่งเป็นเหตุผลว่าทำไมจำนวนเหล่านี้จึงต้องถูกต้อง โปรแกรมแก้ไข PDF บางตัวจากปลายทศวรรษ 1990 และต้นทศวรรษ 2000 ไม่ได้คำนวณจำนวนหน้าใหม่หลังจากแก้ไขแทนที่ ดังนั้นโปรแกรมแยกวิเคราะห์ที่ปลอดภัยจึงควรตรวจสอบ /Count กับจำนวนใบหน้าที่แท้จริงแทนที่จะเชื่อเพื่อการจัดสรรอาร์เรย์
สิ่งที่เกิดขึ้นในทางปฏิบัติ
บั๊กลำดับหน้ามักจะปรากฏในสองสถานการณ์ สถานการณ์แรกคือโปรแกรมแยกวิเคราะห์แบบกำหนดเองที่สแกนหาวัตถุประเภท /Page แทนที่จะตามโครงสร้างต้นไม้ โปรแกรมจะพบทุกหน้า แต่จะอยู่ในลำดับหมายเลขวัตถุ ไม่ใช่ลำดับการอ่าน วิธีแก้ปัญหาก็เหมือนกันเสมอ: เริ่มต้นจาก trailer ค้นหาแค็ตตาล็อกราก ตาม /Pages ไป และท่องไปในอาร์เรย์ Kids
สถานการณ์ที่สองคือไฟล์ที่มีการอัปเดตแบบเพิ่มส่วน (incremental-update file) เมื่อโปรแกรมแก้ไข PDF เพิ่มการเปลี่ยนแปลงโดยไม่ได้เขียนใหม่ทั้งไฟล์ วัตถุหน้าเว็บใหม่จะได้หมายเลขวัตถุที่สูงขึ้น ในขณะที่อาร์เรย์ Kids ในโครงสร้างต้นไม้เดิมยังคงควบคุมตำแหน่งตรรกะของมัน หน้าที่เดิมเป็นวัตถุ 5 จะถูกแทนที่ด้วยวัตถุใหม่ 143 แต่อาร์เรย์ Kids ในตอนนี้อ้างอิง 143 แทน 5 เดิม ดังนั้นลำดับทางตรรกะจึงยังคงอยู่ การท่องไปตามหมายเลขวัตถุจะทำให้หน้านั้นไปอยู่ในตำแหน่งที่ผิดในลำดับ
PDF แบบ Linearized (ปรับให้เหมาะกับเว็บ) เป็นรูปแบบที่สาม: ไฟล์จะถูกจัดเรียงใหม่ทางกายภาพเพื่อให้เนื้อหาของหน้าแรกปรากฏใกล้ส่วนต้นของไฟล์สำหรับการแสดงผลอย่างรวดเร็วผ่านการเชื่อมต่อที่ช้า โครงสร้างต้นไม้ของหน้าเว็บยังคงเป็นผู้กำหนดลำดับ แต่ตารางการอ้างอิงโยงจะจับคู่กับออฟเซ็ตที่จัดเรียงใหม่ โปรแกรมแยกวิเคราะห์ที่อาศัยตำแหน่งในไฟล์แทนที่จะเป็นตาราง xref จะอ่านผิดพลาดแม้กระทั่งหน้าแรกของไฟล์ Linearized
HotPDF Delphi PDF Component จัดการเรื่องการท่องโครงสร้างต้นไม้หน้าเว็บ การแก้ปัญหาการสืบทอด และการรวม xref สำหรับอัปเดตแบบเพิ่มส่วนไว้เป็นการภายใน การทำงานกับวัตถุหน้าโดยตรงหมายความว่าลำดับอาร์เรย์ Kids ได้ถูกนำไปใช้เรียบร้อยแล้ว ดัชนีหน้าจะจับคู่กับหน้าทางตรรกะ ไม่ใช่หมายเลขวัตถุ