ข้ามไปยังเนื้อหา
getnextpdf.com

กายวิภาคของไฟล์ 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.1)

  1. ส่วน header หนึ่งบรรทัด %PDF-2.0 ที่ระบุชื่อเวอร์ชัน
  2. ส่วน body คือเนื้อหลักของไฟล์ ได้แก่ ลำดับของ indirect object ที่มีหมายเลขกำกับ
  3. ส่วน cross-reference คือดัชนีจากหมายเลขอ็อบเจกต์ไปยัง byte offset ที่อ็อบเจกต์นั้นอยู่ PDF แบบดั้งเดิมใช้ table ที่เป็นข้อความ ส่วน PDF 2.0 ใช้ xref stream แบบบีบอัด
  4. ส่วน trailer คือ dictionary ขนาดเล็กที่ระบุจุดเข้า ตามด้วย startxref offset และ %%EOF

จุดพลิกผันคือ โปรแกรมอ่านไม่ได้เริ่มจากด้านบน แต่เริ่มจากด้านล่าง อ่าน startxref เพื่อหาดัชนี แล้วใช้ดัชนีนั้นเข้าถึงอ็อบเจกต์ใดก็ได้โดยตรง ไฟล์ถูกเขียนจากต้นไปท้าย แต่ถูกอ่านจากท้ายไปต้น

มาเดินสำรวจทั้งสี่บริเวณตามลำดับ โดยมีไบต์อยู่ตรงหน้าเรา

ส่วน header มีหนึ่งบรรทัด NextPDF เขียน %PDF-2.0 และตามธรรมเนียมจะมีบรรทัดที่สองเป็นคอมเมนต์ของไบต์ที่มี high-bit เพื่อให้เครื่องมือถ่ายโอนแบบไร้เดียงสามองว่าไฟล์เป็น binary ไม่ใช่ text บรรทัดที่สองนี้เองคือเหตุผลที่ PDF ซึ่งเปิดในฐานะข้อความธรรมดาจะแสดงอักขระเพี้ยนเล็กน้อยทันทีหลังจากเวอร์ชัน

ส่วน body คือที่ที่เอกสารอาศัยอยู่ indirect object แต่ละตัวประกอบด้วยหมายเลข generation คีย์เวิร์ด obj ค่า และ endobj (Spec: ISO 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.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.8) ทั้งเล็กกว่าและสามารถบรรยายอ็อบเจกต์ที่อัดอยู่ภายใน object stream ได้

ส่วน trailer คือสารบัญของไฟล์ (Spec: ISO 32000-2, §7.5.5) มันระบุ /Root คือ document catalog อ็อบเจกต์ตัวเดียวที่ทุกสิ่งอย่างอื่นแขวนอยู่ และ /Size คือจำนวนอ็อบเจกต์ จากนั้นจึงมาถึงการประสานมือที่ทำให้การอ่านย้อนกลับเป็นไปได้ คือ startxref byte offset บนบรรทัดของตัวเอง และ %%EOF โปรแกรมอ่าน seek ไปยังท้ายไฟล์ อ่าน offset นั้น กระโดดตรงไปยังส่วน cross-reference แล้วก็เริ่มทำงานได้

  1. HeaderOne line, %PDF-2.0, naming the version. A binary-marker comment usually follows.
  2. BodyNumbered indirect objects — dictionaries and streams — referenced by N G R.
  3. Cross-reference sectionA text table of 20-byte entries, or a compressed /Type /XRef stream in PDF 2.0.
  4. TrailerNames /Root and /Size, then startxref + offset + %%EOF.
  5. Read orderA reader starts at %%EOF, follows startxref to the index, then reaches each object directly.
The four physical regions of a PDF in file order, and the path a reader actually takes through them — starting at the trailer and working inward via the cross-reference section.

ยังมีบริเวณที่ห้าที่ไฟล์อายุยืนงอกขึ้นมา คือ incremental update การเปลี่ยนแปลงจะไม่ถูกเขียนทับลงในที่เดิม แต่อ็อบเจกต์ที่เปลี่ยน ส่วน cross-reference ใหม่ และ trailer ใหม่ จะถูก ต่อท้าย ไปหลัง %%EOF แรก และ trailer ใหม่นั้นจะแบก /Prev คือ offset ของส่วน cross-reference ก่อนหน้า (Spec: ISO 32000-2, §7.5.6) ส่วนต่างๆ ก่อตัวเป็นลูกโซ่ย้อนกลับ สำหรับหมายเลขอ็อบเจกต์ใดก็ตาม รายการที่ใหม่ที่สุดเป็นฝ่ายชนะ เพราะไบต์เดิมไม่เคยขยับ การตรวจเชิงวิทยาการเข้ารหัสลับเหนือช่วงไบต์ที่ลายเซ็นครอบคลุมจริงจึงยังคงอยู่หลังการอัปเดต incremental update ในภายหลังยังสามารถเปลี่ยนสิ่งที่ validator รายงานเกี่ยวกับเอกสารโดยรวมได้ คือเรื่องว่าการเปลี่ยนแปลงหลังลงนามได้รับอนุญาตหรือไม่ และลายเซ็นถูกถือว่ารับรองอะไร แต่มันเปลี่ยนไบต์ที่ลงนามแล้วเองไม่ได้ คุณสมบัตินั้นคือเนื้อหาทั้งหมดของหน้า incremental update

นี่คือกายวิภาคทั้งหมดในไฟล์เดียวที่เล็กที่สุด ตัวเลขใต้ xref คือ byte offset และต้องแม่นยำเป๊ะ ชี้เกินจุดที่อ็อบเจกต์เริ่มต้นเพียงอักขระเดียว โปรแกรมอ่านที่เข้มงวดก็จะยอมแพ้

%PDF-2.0
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
<< /Size 4 /Root 1 0 R >>
startxref
186
%%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 การกอบกู้นั้นเป็นพฤติกรรมของโปรแกรมดู ไม่ใช่สิ่งที่รูปแบบไฟล์รับประกัน

Reading and repairing arbitrary third-party PDFs — edition availability
EditionAvailability
CoreNextPDF เป็นตัวเขียน มันบันทึก 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 สำหรับโปรแกรมอ่านรุ่นใหม่ได้ เพื่อให้โปรแกรมอ่านแต่ละชนิดพบดัชนีที่ตนเข้าใจ

  • 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 เชื่อมโยงย้อนกลับไปยังส่วนก่อนหน้า