PDF แท้จริงแล้วคืออะไร
ISO 32000-2 §7 Evidence: Standard-backed
ภาพรวมโดยสังเขป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสังเขป”PDF ไม่ใช่คำอธิบายหน้ากระดาษที่บังเอิญถูกเก็บอยู่ในไฟล์ แต่เป็นฐานข้อมูลกราฟขนาดเล็กที่มีเครื่องพิมพ์ติดมาด้วย หน้านี้อธิบายสี่ส่วนที่ PDF ทุกไฟล์ต้องมี ได้แก่ header, body, ตาราง cross-reference และ trailer รวมถึงวิธีที่ NextPDF เขียนส่วนเหล่านี้เพื่อให้โปรแกรมอ่านสามารถค้นหาอ็อบเจกต์ทุกตัวได้โดยไม่ต้องเดา
เหตุใดเรื่องนี้จึงสำคัญ
หัวข้อที่มีชื่อว่า “เหตุใดเรื่องนี้จึงสำคัญ”บั๊กของ PDF ส่วนใหญ่ไม่ใช่บั๊กของการเรนเดอร์ แต่เป็นบั๊กของ โครงสร้าง เช่น byte offset ที่ชี้เกินอ็อบเจกต์ที่ควรชี้ไปหนึ่งอักขระ trailer ที่ระบุ root ผิด หรือรายการ cross-reference ที่ขัดแย้งกับตำแหน่งจริงของอ็อบเจกต์ ปัญหาเหล่านี้ไม่มีข้อใดเปลี่ยนหน้าตาของหน้ากระดาษ จนกว่าโปรแกรมอ่านจะไล่ตามโครงสร้างไฟล์ผ่านอีกเส้นทางหนึ่งแล้วอ่านเลยท้ายไฟล์ไป
หากมอง PDF เป็นกล่องทึบ ความล้มเหลวเหล่านั้นจะดูเหมือนเกิดขึ้นแบบสุ่ม หากเข้าใจโมเดลของอ็อบเจกต์ ความล้มเหลวเหล่านั้นจะเผยตัวตามจริงทุกประการ คือตัวเลขที่ไม่ตรงกับตำแหน่ง การอ่านรูปแบบไฟล์ให้เข้าใจคือสิ่งที่แยกระหว่าง “PDF เสียหาย” กับ “offset ของอ็อบเจกต์ 14 ล้าสมัยเพราะตัวเขียนวัดค่าไว้ก่อนสรุปความยาวของสตรีม”
ฉบับย่อ
หัวข้อที่มีชื่อว่า “ฉบับย่อ”PDF มีสี่ส่วน เรียงตามลำดับในไฟล์ ดังนี้
- ส่วน header คือบรรทัดเดียวที่ระบุเวอร์ชัน (
%PDF-2.0) - ส่วน body คือลำดับของ indirect object ที่มีหมายเลขกำกับ ได้แก่ dictionary, stream, array, number, string และ name
- ส่วน ตาราง cross-reference (หรือใน PDF 2.0 คือ cross-reference stream) คือดัชนีค้นจากหมายเลขอ็อบเจกต์ไปยัง byte offset เพื่อให้เข้าถึงอ็อบเจกต์ใดๆ ได้โดยไม่ต้องสแกนทั้งไฟล์
- ส่วน trailer คือ dictionary ขนาดเล็กที่ระบุอ็อบเจกต์ root ของเอกสารและชี้ไปยังตำแหน่งเริ่มต้นของส่วน cross-reference
โปรแกรมอ่านไม่ได้อ่าน PDF จากต้นไปท้าย แต่อ่านบรรทัดสุดท้ายเป็นอันดับแรก หาตำแหน่ง startxref แล้วข้ามไปยังส่วน cross-reference และใช้ส่วนนั้นเป็นดัชนีเพื่อเข้าสู่ body รูปแบบไฟล์นี้ถูกออกแบบมาให้อ่านย้อนกลับ ข้อเท็จจริงเพียงข้อนี้อธิบายการออกแบบส่วนใหญ่ของรูปแบบไฟล์ได้
วิธีที่ NextPDF จัดการกับเรื่องนี้
หัวข้อที่มีชื่อว่า “วิธีที่ NextPDF จัดการกับเรื่องนี้”NextPDF สร้าง PDF ด้วยลำดับเดียวกับวิธีที่รูปแบบไฟล์ถูกอ่าน คือเขียนอ็อบเจกต์ก่อน บันทึก offset ทีหลัง แล้วเขียนตารางเป็นลำดับสุดท้าย
indirect object ทุกตัวจะได้รับการจัดสรรหมายเลขจาก registry เพียงตัวเดียว (src/Core/ObjectRegistry.php) registry แจกจ่ายหมายเลขตามลำดับผ่าน allocate() และ หลังจาก ที่ไบต์ของอ็อบเจกต์ถูกเขียนลงบัฟเฟอร์เอาต์พุตแล้ว จึงบันทึก byte offset ผ่าน register() offset ไม่เคยถูกคาดเดาล่วงหน้า แต่ถูกอ่านค่าจาก BinaryBuffer::getOffset() ณ ขณะที่ header ของอ็อบเจกต์ถูกเขียนออกมา นี่คือเหตุผลที่รายการ cross-reference ของ NextPDF ไม่อาจคลาดเคลื่อนจากอ็อบเจกต์ที่อธิบายได้ เพราะ offset คือค่าตำแหน่งจริงของบัฟเฟอร์ ณ ขณะนั้น
เมื่อ body เสร็จสมบูรณ์ serialization strategy เฉพาะเวอร์ชัน (src/Writer/PdfSerializationStrategy.php) จะเขียนส่วน cross-reference และ trailer ดังนี้
Pdf20StreamStrategyเขียน cross-reference stream แบบบีบอัด (/Type /XRef) ซึ่งเป็นค่าเริ่มต้นของ PDF 2.0Pdf17TableStrategyและPdf14TableStrategyเขียน cross-reference table แบบดั้งเดิมขนาด 20 ไบต์ พร้อม dictionary ของ trailer แยกต่างหาก ซึ่งเป็นข้อกำหนดของโปรไฟล์ PDF/A ที่บังคับให้ใช้โครงสร้างไฟล์แบบเก่า
NextPDF เลือก strategy จากโปรไฟล์เอาต์พุต ไม่ใช่จากการอนุมาน ไม่ว่าจะเป็นแบบใด ไบต์สุดท้ายก็มีรูปแบบเดียวกัน คือส่วน cross-reference ตามด้วย startxref ตามด้วย byte offset และ %%EOF ส่วนท้ายนั้นคือสิ่งที่โปรแกรมอ่านพบเป็นอันดับแรก
- Step 1 of 4: ISO 32000-2 §7.5.5 — %%EOF and startxref at the file end
- Step 2 of 4: ISO 32000-2 §7.5.4 / §7.5.8 — the cross-reference section maps object number to offset
- Step 3 of 4: ISO 32000-2 §7.5.5 — the trailer names /Root, the document catalog
- Step 4 of 4: ISO 32000-2 §7.3.10 — each indirect object is reached at its recorded offset
หลักฐานบอกอะไร
หัวข้อที่มีชื่อว่า “หลักฐานบอกอะไร”โครงสร้างสี่ส่วนนี้ไม่ใช่ธรรมเนียมของ NextPDF แต่เป็นข้อกำหนดเรื่องโครงสร้างไฟล์ของ Spec: ISO 32000-2, §7.5 ISO 32000-2 §7.5 มาตรฐานนิยาม PDF ว่าประกอบด้วย header, body ของอ็อบเจกต์, ตาราง cross-reference และ trailer และระบุว่าโปรแกรมอ่านควรแยกวิเคราะห์ไฟล์จากท้ายไฟล์ บรรทัดสุดท้ายคือ %%EOF และสองบรรทัดก่อนหน้าคือคีย์เวิร์ด startxref และ byte offset ที่ชี้ไปยังส่วน cross-reference
คำว่า indirect object ถูกนิยามว่าเป็นหมายเลขอ็อบเจกต์และหมายเลข generation คั่นด้วยช่องว่าง ตามด้วยค่าของอ็อบเจกต์ที่อยู่ระหว่างคีย์เวิร์ด obj และ endobj หมายเลขอ็อบเจกต์รวมกับหมายเลข generation ระบุอ็อบเจกต์ได้อย่างไม่ซ้ำกัน reference แบบอ้อมไปยังอ็อบเจกต์นั้นเขียนเป็นหมายเลขอ็อบเจกต์ หมายเลข generation และคีย์เวิร์ด R ObjectRegistry ของ NextPDF สะท้อนรูปแบบนี้โดยตรง คือหมายเลขตามลำดับ generation 0 สำหรับอ็อบเจกต์ที่เพิ่งเขียน และ offset ที่บันทึกไว้
ตั้งแต่ PDF 1.5 เป็นต้นมา ยังอนุญาตให้อ็อบเจกต์อยู่ภายใน object stream ได้ โดยจัดเก็บโดยไม่มีคีย์เวิร์ด obj/endobj และต้องมี generation เป็นศูนย์ cross-reference stream (/Type /XRef,
Spec: ISO 32000-2, §7.5.8 ISO 32000-2 §7.5.8 ) คือกลไกของ PDF 2.0
สำหรับทำดัชนีให้ทั้งอ็อบเจกต์ปกติและอ็อบเจกต์ที่ถูกบีบอัดเหล่านี้
CrossReferenceStream ของ NextPDF สร้างสิ่งนี้ด้วยอาร์เรย์ความกว้างของฟิลด์ /W และ
การบีบอัดแบบ FlateDecode
ตัวอย่างเชิงปฏิบัติ
หัวข้อที่มีชื่อว่า “ตัวอย่างเชิงปฏิบัติ”นี่คือรูปแบบของ body ของ PDF ขั้นต่ำและ trailer ของ PDF นั้น ตัวเลขในส่วน cross-reference คือ byte offset ตัวเลขเหล่านี้ต้องถูกต้องอย่างแม่นยำ นี่คือเหตุผลที่ NextPDF บันทึกค่าจากบัฟเฟอร์แทนที่จะคำนวณ
%PDF-2.01 0 obj<< /Type /Catalog /Pages 2 0 R >>endobj2 0 obj<< /Type /Pages /Kids [3 0 R] /Count 1 >>endobj3 0 obj<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>endobjxref0 40000000000 65535 f0000000009 00000 n0000000058 00000 n0000000122 00000 ntrailer<< /Size 4 /Root 1 0 R >>startxref196%%EOFโปรแกรมอ่านเปิดไฟล์นี้จากท้ายไฟล์ คือ %%EOF ตามด้วย startxref 196 จากนั้นไปยังไบต์ที่ 196 ซึ่งเป็นจุดเริ่มต้นของ xref อ่านได้ว่าอ็อบเจกต์ 1 อยู่ที่ไบต์ที่ 9 ตาม /Root 1 0 R ไปยัง catalog แล้วเดินสำรวจ page tree จากตรงนั้น อ็อบเจกต์ 0 เป็นหัวของ free-list เสมอ โดยมี generation 65535 ซึ่งเป็นลักษณะเฉพาะที่สืบทอดมาจากการออกแบบยุคแรกสุดของรูปแบบไฟล์ และถูกทำซ้ำอย่างซื่อตรงเพราะโปรแกรมอ่านต่างๆ คาดหวังให้เป็นเช่นนั้น
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”กับดักคือการเชื่อว่า PDF ถูกอ่านจากบนลงล่างเหมือนซอร์สโค้ด ซึ่งไม่ใช่เช่นนั้น body จะมีลำดับอ็อบเจกต์แบบใดก็ได้ หมายเลขอ็อบเจกต์ไม่จำเป็นต้องเรียงตามลำดับในไฟล์ และโปรแกรมอ่านไม่เคยพึ่งพาว่าหมายเลขจะเรียงตามลำดับ ดัชนีที่เชื่อถือได้ เพียงอย่างเดียว คือส่วน cross-reference และวิธีเดียวที่จะหาส่วนนั้นได้คือ trailer ที่อยู่ท้ายไฟล์ PDF ที่มี body สมบูรณ์ถูกต้องทุกประการ แต่มีตัวเลขผิดเพียงตัวเดียวใน startxref ก็จะอ่านไม่ได้ PDF ที่มีอ็อบเจกต์ถูกเขียนในลำดับที่สับสน แต่มีตาราง cross-reference ที่ถูกต้องจะใช้งานได้ปกติ ตำแหน่งไม่มีความหมาย ตำแหน่งที่ ถูกบันทึกไว้ ต่างหากคือทุกสิ่ง
ขีดจำกัดและขอบเขต
หัวข้อที่มีชื่อว่า “ขีดจำกัดและขอบเขต”หน้านี้อธิบายโครงสร้างไฟล์ ไม่ใช่เนื้อหาของหน้ากระดาษ วิธีที่รอยหมึกปรากฏบนหน้ากระดาษ ไม่ว่าจะเป็น content stream, graphics operator หรือการแสดงข้อความ เป็นหัวข้อแยกต่างหาก อีกทั้งยังไม่ครอบคลุมถึงสิ่งที่เกิดขึ้นเมื่อไฟล์ถูก เปลี่ยนแปลง หลังจากเขียนเสร็จแล้ว นั่นเป็นหน้าที่ของ incremental update ซึ่งตัวเขียนจะต่อท้ายส่วน cross-reference ส่วนที่สอง และ trailer จะเชื่อมโยงย้อนกลับ
NextPDF เป็น ตัวเขียน พฤติกรรมที่อธิบายไว้ในที่นี้คือวิธีที่ NextPDF serialize เอกสารที่ตนเองสร้างขึ้น NextPDF ไม่ใช่ parser สำหรับ PDF หรือเครื่องมือซ่อมแซมแบบอเนกประสงค์ NextPDF ไม่รับประกันว่าจะอ่าน สร้างใหม่ หรือกู้คืนไฟล์จากบุคคลที่สามใดๆ ที่มีตาราง cross-reference เสียหายได้ การรับประกันนี้แคบและจงใจให้เป็นเช่นนั้น ไฟล์ที่ NextPDF เขียน มี offset ที่ตรงกัน เพราะค่าเหล่านั้นได้จากการวัด ไม่ใช่การคาดการณ์
คำถามที่พบบ่อยฉบับย่อ
หัวข้อที่มีชื่อว่า “คำถามที่พบบ่อยฉบับย่อ”ทำไมต้องมีหมายเลข generation ในเมื่อไฟล์ใหม่ใช้ 0 เสมอ? หมายเลข generation มีไว้สำหรับการนำอ็อบเจกต์กลับมาใช้ซ้ำข้ามการอัปเดต ไฟล์ที่เพิ่งเขียนใหม่จะมีทุกอ็อบเจกต์อยู่ที่ generation 0 ส่วน generation ที่ไม่เป็นศูนย์จะปรากฏก็ต่อเมื่อไฟล์ถูกอัปเดตแบบ incremental และหมายเลขอ็อบเจกต์ถูกนำกลับมาใช้ใหม่เท่านั้น
อ็อบเจกต์สองตัวมีหมายเลขเดียวกันได้หรือไม่? ภายในส่วน cross-reference เดียวกัน ไม่ได้ เมื่อมีการอัปเดตแบบ incremental ไฟล์อาจมีสำเนาของหมายเลขอ็อบเจกต์เดียวกันหลายชุดในเชิงกายภาพ รายการ cross-reference ล่าสุดเป็นฝ่ายชนะ นั่นคือหัวข้อของหน้าถัดไป
ลำดับของอ็อบเจกต์ในไฟล์มีผลต่อเอาต์พุตหรือไม่? ไม่มี NextPDF เขียนอ็อบเจกต์ในลำดับที่กำหนดได้แน่นอนเพื่อให้การ build ทำซ้ำได้ แต่โปรแกรมอ่านค้นหาทุกสิ่งผ่านส่วน cross-reference ดังนั้นลำดับเชิงกายภาพจึงไม่มีความหมายในเชิงความหมาย
เอกสารที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เอกสารที่เกี่ยวข้อง”- Incremental updates and why they matter สิ่งที่เกิดขึ้นเมื่อ PDF ที่เขียนแล้วถูกเปลี่ยนแปลง ได้แก่ ส่วนที่ต่อท้ายและ trailer ที่เชื่อมโยงเป็นลูกโซ่
- Streams and filters วิธีที่อ็อบเจกต์สตรีมใน body ถูกบีบอัดและเข้ารหัส
- PDF 2.0: what changed ความแตกต่างของโครงสร้างไฟล์ระหว่าง 1.7 กับ baseline 2.0 ที่ NextPDF มุ่งหมาย
อภิธานศัพท์
หัวข้อที่มีชื่อว่า “อภิธานศัพท์”- Indirect object อ็อบเจกต์ที่มีหมายเลขกำกับใน body เขียนเป็น
N G obj … endobjโดยNคือหมายเลขอ็อบเจกต์ และGคือหมายเลข generation - Indirect reference ตัวชี้ไปยัง indirect object เขียนเป็น
N G Rตามลำดับ - Cross-reference table (xref) ดัชนีจากหมายเลขอ็อบเจกต์ไปยัง byte offset ใน PDF 2.0 สิ่งนี้มักเป็น cross-reference stream (
/Type /XRef) แทนที่ตารางข้อความแบบดั้งเดิมที่ใช้ 20 ไบต์ต่อรายการ - Trailer dictionary ที่อยู่ท้ายส่วน cross-reference ซึ่งระบุ
/Root(document catalog) และ/Sizeและพบได้ผ่าน offset ของstartxref - Object stream อ็อบเจกต์สตรีมที่มีอ็อบเจกต์อ้อม (indirect object) อื่นๆ อยู่ภายใน (บีบอัดรวมกัน) สมาชิกไม่มี
obj/endobjและมี generation เป็นศูนย์ - Document catalog อ็อบเจกต์ที่
/Rootระบุถึง เป็นจุดเริ่มต้นเข้าสู่ page tree และส่วนอื่นๆ ทั้งหมดในเอกสาร