กายวิภาคของไฟล์ PDF
โดยสรุป
หัวข้อที่มีชื่อว่า “โดยสรุป”เปิด PDF ไฟล์ใดก็ตามในโปรแกรมแก้ไขข้อความธรรมดา สิ่งแรกที่เห็นจะชวนให้อุ่นใจ คือ header ในรูป %PDF-1.x หรือ %PDF-2.0 ส่วนสิ่งสุดท้ายที่เห็นคือ %%EOF ทุกสิ่งระหว่างสองบรรทัดนี้คือเครื่องจักรขนาดเล็กที่เป็นระเบียบสำหรับค้นหาอ็อบเจกต์ด้วยหมายเลข หน้านี้คือการผ่าโครงสร้าง เราเปิดไฟล์ ตั้งชื่อให้แต่ละอวัยวะ และแสดงให้เห็นว่าอวัยวะเหล่านั้นเชื่อมโยงกันอย่างไร
หน้านี้เป็นคู่หูเชิงโครงสร้างของอีกสองหน้าข้างเคียง PDF แท้จริงแล้วคืออะไร มองไฟล์ในฐานะกราฟของอ็อบเจกต์ ส่วน incremental update ครอบคลุมวิธีที่ไฟล์เติบโตขึ้นตามกาลเวลา หน้านี้จะเกาะติดอยู่กับไบต์ คือบริเวณทางกายภาพที่ parser เดินผ่าน ตามลำดับที่บริเวณเหล่านั้นวางอยู่บนดิสก์
เหตุใดเรื่องนี้จึงสำคัญ
หัวข้อที่มีชื่อว่า “เหตุใดเรื่องนี้จึงสำคัญ”คุณแทบไม่จำเป็นต้องรู้เรื่องนี้เพื่อ ใช้ PDF เลย คุณจำเป็นต้องรู้ในวันที่ PDF ไฟล์หนึ่งเกิดผิดพลาด เช่น ไฟล์เปิดได้ในโปรแกรมดูตัวหนึ่งแต่เปิดไม่ได้ในอีกตัวหนึ่ง validator รายงานว่า “ตาราง cross-reference เสียหาย” หรือเอกสารที่ลงนามแล้วกลับตรวจสอบความถูกต้องไม่ผ่านขึ้นมาเฉยๆ ปัญหาเหล่านี้ไม่มีข้อใดเป็นปริศนาเลยเมื่อคุณอ่านกายวิภาคได้ ทั้งหมดคือตัวเลขที่ไม่ตรงกับตำแหน่งอีกต่อไป บริเวณที่อยู่ผิดที่ หรือส่วนท้ายที่ชี้ไปยังที่ว่างเปล่า
การรู้ผังโครงสร้างเปลี่ยน “PDF เสียหาย” ให้กลายเป็นการวินิจฉัยที่คุณลงมือจัดการได้ มันคือความแตกต่างระหว่างการยักไหล่ใส่กล่องทึบกับการชี้ไปยังไบต์ที่โกหกอย่างเจาะจง
ฉบับย่อ
หัวข้อที่มีชื่อว่า “ฉบับย่อ”PDF ที่ถูกต้องตามข้อกำหนดมีสี่ส่วนทางกายภาพ เรียงตามลำดับในไฟล์ดังนี้ (Spec: ISO 32000-2, §7.5.1ISO 32000-2 §7.5.1)
- ส่วน header หนึ่งบรรทัด
%PDF-2.0ที่ระบุชื่อเวอร์ชัน - ส่วน body คือเนื้อหลักของไฟล์ ได้แก่ ลำดับของ indirect object ที่มีหมายเลขกำกับ
- ส่วน cross-reference คือดัชนีจากหมายเลขอ็อบเจกต์ไปยัง byte offset ที่อ็อบเจกต์นั้นอยู่ PDF แบบดั้งเดิมใช้ table ที่เป็นข้อความ ส่วน PDF 2.0 ใช้ xref stream แบบบีบอัด
- ส่วน trailer คือ dictionary ขนาดเล็กที่ระบุจุดเข้า ตามด้วย
startxrefoffset และ%%EOF
จุดพลิกผันคือ โปรแกรมอ่านไม่ได้เริ่มจากด้านบน แต่เริ่มจากด้านล่าง อ่าน startxref เพื่อหาดัชนี แล้วใช้ดัชนีนั้นเข้าถึงอ็อบเจกต์ใดก็ได้โดยตรง ไฟล์ถูกเขียนจากต้นไปท้าย แต่ถูกอ่านจากท้ายไปต้น
วิธีที่ NextPDF จัดการกับเรื่องนี้
หัวข้อที่มีชื่อว่า “วิธีที่ NextPDF จัดการกับเรื่องนี้”มาเดินสำรวจทั้งสี่บริเวณตามลำดับ โดยมีไบต์อยู่ตรงหน้าเรา
ส่วน header มีหนึ่งบรรทัด NextPDF เขียน %PDF-2.0 และตามธรรมเนียมจะมีบรรทัดที่สองเป็นคอมเมนต์ของไบต์ที่มี high-bit เพื่อให้เครื่องมือถ่ายโอนแบบไร้เดียงสามองว่าไฟล์เป็น binary ไม่ใช่ text บรรทัดที่สองนี้เองคือเหตุผลที่ PDF ซึ่งเปิดในฐานะข้อความธรรมดาจะแสดงอักขระเพี้ยนเล็กน้อยทันทีหลังจากเวอร์ชัน
ส่วน body คือที่ที่เอกสารอาศัยอยู่ indirect object แต่ละตัวประกอบด้วยหมายเลข generation คีย์เวิร์ด obj ค่า และ endobj (Spec: ISO 32000-2, §7.3.10ISO 32000-2 §7.3.10) ค่านั้นเป็นหนึ่งในรูปทรงไม่กี่แบบ แต่มีสองแบบที่แบกรับน้ำหนักแทบทั้งหมด
- dictionary ในรูป
<< /Key value … >>คือแผนที่จาก name ไปยังค่า ทั้ง page tree, catalog และ font descriptor ล้วนเป็น dictionary - stream คือ dictionary ตามด้วย
streamบล็อกของไบต์ตามอำเภอใจ และendstreamเนื้อหาของหน้า ฟอนต์ที่ฝังไว้ และรูปภาพ ล้วนเป็น stream และแทบทุกครั้งจะถูกบีบอัด (filter ของ stream เหล่านี้เป็นเรื่องราวในตัวของมันเอง เล่าไว้ใน streams and filters)
อ็อบเจกต์ชี้ไปหากันด้วย indirect reference คือ 2 0 R หมายความว่า “อ็อบเจกต์ 2 generation 0” นั่นคือสายไฟที่เปลี่ยนรายการอ็อบเจกต์แบนๆ ให้กลายเป็นกราฟ
ส่วน cross-reference คือส่วนที่คนส่วนใหญ่นึกภาพไม่ถูกต้อง ในรูปแบบดั้งเดิมมันเป็นข้อความล้วน คือคีย์เวิร์ด xref ตามด้วยซับเซกชันของบรรทัดความกว้างคงที่ขนาด 20 ไบต์ (Spec: ISO 32000-2, §7.5.4ISO 32000-2 §7.5.4) แต่ละบรรทัดคือ byte offset สิบหลัก generation ห้าหลัก และแฟล็กตัวเดียว คือ n สำหรับ in-use และ f สำหรับ free ที่เติมให้พอดี 20 ไบต์เป๊ะ เพื่อให้โปรแกรมอ่าน seek ไปยังรายการใดก็ได้ด้วยการคำนวณเลขล้วนๆ PDF 2.0 แทนที่สิ่งนี้ด้วย cross-reference stream คือดัชนีเดียวกัน แต่เป็น binary และถูกบีบอัดไว้ภายในอ็อบเจกต์ stream ที่ทำเครื่องหมาย /Type /XRef (Spec: ISO 32000-2, §7.5.8ISO 32000-2 §7.5.8) ทั้งเล็กกว่าและสามารถบรรยายอ็อบเจกต์ที่อัดอยู่ภายใน object stream ได้
ส่วน trailer คือสารบัญของไฟล์ (Spec: ISO 32000-2, §7.5.5ISO 32000-2 §7.5.5) มันระบุ /Root คือ document catalog อ็อบเจกต์ตัวเดียวที่ทุกสิ่งอย่างอื่นแขวนอยู่ และ /Size คือจำนวนอ็อบเจกต์ จากนั้นจึงมาถึงการประสานมือที่ทำให้การอ่านย้อนกลับเป็นไปได้ คือ startxref byte offset บนบรรทัดของตัวเอง และ %%EOF โปรแกรมอ่าน seek ไปยังท้ายไฟล์ อ่าน offset นั้น กระโดดตรงไปยังส่วน cross-reference แล้วก็เริ่มทำงานได้
- HeaderOne line, %PDF-2.0, naming the version. A binary-marker comment usually follows.
- BodyNumbered indirect objects — dictionaries and streams — referenced by N G R.
- Cross-reference sectionA text table of 20-byte entries, or a compressed /Type /XRef stream in PDF 2.0.
- TrailerNames /Root and /Size, then startxref + offset + %%EOF.
- Read orderA reader starts at %%EOF, follows startxref to the index, then reaches each object directly.
ยังมีบริเวณที่ห้าที่ไฟล์อายุยืนงอกขึ้นมา คือ incremental update การเปลี่ยนแปลงจะไม่ถูกเขียนทับลงในที่เดิม แต่อ็อบเจกต์ที่เปลี่ยน ส่วน cross-reference ใหม่ และ trailer ใหม่ จะถูก ต่อท้าย ไปหลัง %%EOF แรก และ trailer ใหม่นั้นจะแบก /Prev คือ offset ของส่วน cross-reference ก่อนหน้า (Spec: ISO 32000-2, §7.5.6ISO 32000-2 §7.5.6) ส่วนต่างๆ ก่อตัวเป็นลูกโซ่ย้อนกลับ สำหรับหมายเลขอ็อบเจกต์ใดก็ตาม รายการที่ใหม่ที่สุดเป็นฝ่ายชนะ เพราะไบต์เดิมไม่เคยขยับ การตรวจเชิงวิทยาการเข้ารหัสลับเหนือช่วงไบต์ที่ลายเซ็นครอบคลุมจริงจึงยังคงอยู่หลังการอัปเดต incremental update ในภายหลังยังสามารถเปลี่ยนสิ่งที่ validator รายงานเกี่ยวกับเอกสารโดยรวมได้ คือเรื่องว่าการเปลี่ยนแปลงหลังลงนามได้รับอนุญาตหรือไม่ และลายเซ็นถูกถือว่ารับรองอะไร แต่มันเปลี่ยนไบต์ที่ลงนามแล้วเองไม่ได้ คุณสมบัตินั้นคือเนื้อหาทั้งหมดของหน้า incremental update
ตัวอย่างใช้งานจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างใช้งานจริง”นี่คือกายวิภาคทั้งหมดในไฟล์เดียวที่เล็กที่สุด ตัวเลขใต้ xref คือ byte offset และต้องแม่นยำเป๊ะ ชี้เกินจุดที่อ็อบเจกต์เริ่มต้นเพียงอักขระเดียว โปรแกรมอ่านที่เข้มงวดก็จะยอมแพ้
%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 n0000000115 00000 ntrailer<< /Size 4 /Root 1 0 R >>startxref186%%EOFอ่านมันด้วยวิธีที่ parser อ่าน บรรทัดสุดท้าย %%EOF เหนือขึ้นไป startxref 186 ดังนั้น seek ไปยังไบต์ที่ 186 ที่ซึ่ง xref เริ่มต้น ตารางบอกว่าอ็อบเจกต์ 1 อยู่ที่ไบต์ที่ 9 ส่วน /Root 1 0 R ของ trailer ชี้ไปที่นั่น คือ catalog และจาก catalog คุณเดินตาม /Pages ไปยัง page tree แล้วพบหน้าเพียงหน้าเดียว อ็อบเจกต์ 0 เป็นหัวของ free-list เสมอ โดยมี generation 65535 เป็นซากดึกดำบรรพ์จากการออกแบบยุคแรกสุดของรูปแบบไฟล์ ที่โปรแกรมอ่านทุกตัวยังคงคาดหวังให้เห็น
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”กับดักคือการอ่าน PDF เหมือนนิทาน คือจากบนลงล่าง ตามลำดับ มันไม่ใช่นิทาน แต่เป็นดัชนีที่มีตัวชี้ย้อนกลับ หมายเลขอ็อบเจกต์ไม่จำเป็นต้องเรียงตามลำดับในไฟล์ อ็อบเจกต์ปรากฏในลำดับทางกายภาพแบบใดก็ได้ และโปรแกรมอ่านไม่เคยพึ่งพาตำแหน่งของมันเลย แผนที่ที่เชื่อถือได้ เพียงอย่างเดียว คือส่วน cross-reference และวิธีเดียวที่จะหาแผนที่นั้นได้คือ offset ของ startxref ที่อยู่ท้ายสุดของไฟล์
ผลที่ตามมาทำให้คนประหลาดใจ PDF ที่มี body ไร้ที่ติแต่มีตัวเลขผิดเพียงหลักเดียวใน startxref จะอ่านไม่ได้ เพราะโปรแกรมอ่านหาดัชนีไม่เจอ ส่วน PDF ที่มีอ็อบเจกต์เรียงสับสนแต่มีส่วน cross-reference ที่ถูกต้องกลับใช้งานได้สมบูรณ์แบบ ตำแหน่งทางกายภาพไม่มีความหมายใดเลย ตำแหน่งที่ ถูกบันทึกไว้ ต่างหากที่แบกความหมายทั้งหมด
ขีดจำกัดและขอบเขต
หัวข้อที่มีชื่อว่า “ขีดจำกัดและขอบเขต”หน้านี้อธิบายโครงสร้างเชิงกายภาพ ไม่ใช่เนื้อหาของหน้ากระดาษ วิธีที่รอยหมึกตกลงบนหน้ากระดาษ ได้แก่ content-stream operator การแสดงข้อความ และ graphics state เป็นหัวข้อแยกต่างหาก อีกทั้งหน้านี้อธิบายไฟล์ที่ ถูกต้องตามรูปแบบ PDF ในโลกจริงมักเสียหายเล็กน้อยอยู่บ่อยครั้ง และรอดมาได้เพียงเพราะโปรแกรมดูที่ใจกว้างสร้างตาราง cross-reference ขึ้นใหม่ด้วยการสแกนหาคีย์เวิร์ด obj การกอบกู้นั้นเป็นพฤติกรรมของโปรแกรมดู ไม่ใช่สิ่งที่รูปแบบไฟล์รับประกัน
| Edition | Availability |
|---|---|
| Core | NextPDF เป็นตัวเขียน มันบันทึก offset ทุกค่าจากบัฟเฟอร์เอาต์พุต ณ ขณะที่อ็อบเจกต์แต่ละตัวถูกเขียนออกมา ดังนั้นไฟล์ที่มันสร้างจึงมีส่วน cross-reference ที่ตรงกับ body โดยโครงสร้าง |
| Pro | การ parse สร้างใหม่ หรือซ่อมแซมตาราง cross-reference ที่เสียหายในไฟล์ที่ NextPDF ไม่ได้เขียนเอง อยู่นอกขอบเขตในทุกรุ่น |
| Enterprise | สำหรับการตรวจสอบโครงสร้างของไฟล์ที่มีอยู่แล้ว ให้ใช้ parser หรือ validator เฉพาะทาง NextPDF รับประกันความถูกต้องสำหรับสิ่งที่ตัวมันเขียน ไม่ใช่สำหรับสิ่งที่ตัวมันอ่าน |
คำถามที่พบบ่อยฉบับย่อ
หัวข้อที่มีชื่อว่า “คำถามที่พบบ่อยฉบับย่อ”ทำไม PDF จึงมีบรรทัด binary-marker หลังจาก header? เครื่องมือถ่ายโอนรุ่นเก่าบางตัวเคยทำให้ไฟล์ที่มันคิดว่าเป็นข้อความธรรมดาเสียหาย คอมเมนต์ที่มี high-bit ทำให้ไฟล์ดูเป็น binary อย่างชัดเจน เพื่อให้มันรอดการเดินทางมาได้โดยไม่ถูกเปลี่ยนแปลง
xref stream เป็นเพียงตารางที่เล็กกว่าใช่หรือไม่? ส่วนใหญ่ก็ใช่ แต่มีพลังพิเศษเพิ่มมาอีกหนึ่งอย่าง นอกจากถูกบีบอัดแล้ว xref stream ยังสามารถบรรยายอ็อบเจกต์ที่เก็บอยู่ ภายใน object stream ได้ คือรายการที่ตารางข้อความแบบ 20 ไบต์ดั้งเดิมไม่มีทางแสดงออกมาได้
มีทั้งตารางและ stream ในไฟล์เดียวได้หรือไม่? การแก้ไขครั้งเดียวใช้อย่างใดอย่างหนึ่ง แต่ไฟล์แบบ hybrid สามารถจับคู่ตารางดั้งเดิมสำหรับโปรแกรมอ่านรุ่นเก่ากับ cross-reference stream สำหรับโปรแกรมอ่านรุ่นใหม่ได้ เพื่อให้โปรแกรมอ่านแต่ละชนิดพบดัชนีที่ตนเข้าใจ
เอกสารที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เอกสารที่เกี่ยวข้อง”- PDF แท้จริงแล้วคืออะไร สี่ส่วนเดียวกันนี้มองในฐานะกราฟของอ็อบเจกต์ แทนที่จะเป็นผังทางกายภาพ
- การอัปเดตแบบเพิ่มหน่วยและเหตุใดจึงสำคัญ วิธีที่บริเวณที่ห้าซึ่งถูกต่อท้ายทำให้ไฟล์เติบโตและปกป้องลายเซ็น
- Streams and filters สิ่งที่อยู่ภายในอ็อบเจกต์ stream ของ body และวิธีที่มันถูกบีบอัด
- PDF 2.0: what changed เหตุผลที่ cross-reference stream เป็นโครงสร้างเริ่มต้นที่ NextPDF เขียน
อภิธานศัพท์
หัวข้อที่มีชื่อว่า “อภิธานศัพท์”- Header บรรทัดแรก
%PDF-2.0ที่ระบุชื่อเวอร์ชัน โดยปกติตามด้วยคอมเมนต์ binary-marker - Indirect object อ็อบเจกต์ที่มีหมายเลขกำกับใน body เขียนเป็น
N G obj … endobjโดยNคือหมายเลขอ็อบเจกต์ และGคือ generation - Dictionary แผนที่จาก name ไปยังค่าในรูป
<< /Key value … >>คือรูปทรงอ็อบเจกต์ที่พบบ่อยที่สุด - Stream dictionary บวกกับบล็อกของไบต์ระหว่าง
streamและendstreamใช้สำหรับเนื้อหา ฟอนต์ และรูปภาพ - Cross-reference table (xref) ดัชนีจากหมายเลขอ็อบเจกต์ไปยัง byte offset โดยดั้งเดิมเป็นตารางข้อความขนาด 20 ไบต์ต่อรายการ ส่วนใน PDF 2.0 เป็น stream แบบ
/Type /XRef - Trailer dictionary ที่ระบุ
/Rootและ/Sizeพบได้ผ่าน offset ของstartxrefที่ท้ายไฟล์ - Incremental update อ็อบเจกต์ที่เปลี่ยน ส่วน cross-reference ใหม่ และ trailer ใหม่ ที่ถูกต่อท้ายหลัง
%%EOFโดยมี/Prevเชื่อมโยงย้อนกลับไปยังส่วนก่อนหน้า