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

เศรษฐศาสตร์ของขนาดไฟล์ PDF

Spec: ISO 32000-2, §7.5.7

PDF สองไฟล์สามารถดูเหมือนกันพิกเซลต่อพิกเซลบนหน้าจอแต่ต่างกันสิบเท่าบนดิสก์ ความแตกต่างแทบไม่เคยเป็นเนื้อหาที่คุณเห็น แต่เป็นวิธีที่ไฟล์ถูกประกอบขึ้นเบื้องล่าง หน้านี้คือทัวร์เศรษฐศาสตร์ขนาด: ไบต์ของ PDF ไปอยู่ที่ใดจริงๆ และคันโยกสี่ตัวที่ผู้เขียนใช้จ่ายงบประมาณไบต์

มันคือเพื่อนคู่กันที่ว่าด้วย ทำไมไฟล์ถึงใหญ่ ของ สตรีมและฟิลเตอร์ ซึ่งครอบคลุม วิธีที่ฟิลเตอร์ถอดรหัส หน้านี้คงอยู่ที่เรื่องงบประมาณ

ขนาดไฟล์แทบไม่เคยเป็นตัวชี้วัดเพื่อความโก้ มันคือแบนด์วิดท์ในทุกการดาวน์โหลด พื้นที่จัดเก็บในทุกการเก็บถาวร และ latency ในทุกการแสดงตัวอย่าง ใบแจ้งหนี้ขนาด 12 MB ที่ควรเป็น 400 KB ไม่ใช่ปัญหาเชิงเครื่องสำอางเมื่อคุณสร้างมันหนึ่งล้านใบต่อเดือน — มันคือบิลที่แพงขึ้นสามสิบเท่า

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

ลองนึกว่า PDF คืองบประมาณที่คุณใช้จ่ายกับรายการสี่รายการ

  • โอเวอร์เฮดต่ออ็อบเจกต์ ทุก indirect object พกซองจดหมาย N G obj / endobj และถูกติดตามด้วย cross-reference entry หนึ่งรายการ เอกสารที่มีหน้าจำนวนมากมีอ็อบเจกต์เล็กๆนับพัน และซองจดหมายก็สะสมรวมกัน Object stream ทิ้งซองจดหมายนั้นไปสำหรับอ็อบเจกต์ทั้งกลุ่ม แต่ละอ็อบเจกต์ที่แพ็กไว้ยังคงเก็บ cross-reference entry ของตัวเอง แต่เป็น type-2 entry ที่กะทัดรัด
  • ไบต์รูปภาพ สำหรับเอกสารใดก็ตามที่มีภาพถ่ายหรือภาพสแกน รูปภาพครอบงำ และคันโยกที่ใหญ่ที่สุดเพียงตัวเดียวคือ การเลือกฟิลเตอร์ — codec แบบไม่สูญเสียข้อมูลเทียบกับ codec รูปภาพแบบสูญเสียข้อมูลคือความแตกต่างระหว่างเมกะไบต์กับกิโลไบต์
  • ไบต์ฟอนต์ ฟอนต์ฝังตัวเต็มรูปแบบคือ glyph หลายร้อยกิโลไบต์ที่คุณไม่เคยใช้ Subsetting เก็บไว้เฉพาะ glyph ที่เอกสารวาดจริง
  • ดัชนี cross-reference table ที่ให้โปรแกรมอ่านหาทุกอ็อบเจกต์ก็สามารถเป็น สตรีมที่บีบอัดแล้ว แทนที่จะเป็นข้อความธรรมดาได้

ทำทั้งสี่ให้ถูกแล้วไฟล์ก็เล็ก พลาดข้อหนึ่งแล้วมันก็ครอบงำทุกอย่างที่คุณทำได้ดี

ตัวเขียนของ NextPDF เป็น serializer แบบสตรีมรอบเดียว: มันต่อท้ายไบต์ของแต่ละอ็อบเจกต์ขณะที่มันถูกผลิตและบันทึก in-use cross-reference entry แบบดั้งเดิมสำหรับแต่ละตัว ค่าเริ่มต้นนั้นเร็ว คาดการณ์ได้ และผลิตไฟล์ที่เสถียรในระดับไบต์ — แต่มัน ไม่ใช่ เลย์เอาต์ที่เล็กที่สุดเท่าที่เป็นไปได้ และ NextPDF ก็ซื่อสัตย์ในเรื่องนั้น

PDF คือกราฟของ indirect object ส่วนใหญ่เป็น dictionary เล็กๆ: page node, annotation dictionary, structure-tree element, outline entry แต่ละตัวจ่ายภาษีตายตัว — คีย์เวิร์ด obj / endobj หมายเลขอ็อบเจกต์และหมายเลข generation และ cross-reference entry ที่ระบุตำแหน่งมัน บนเอกสารที่มีอ็อบเจกต์เล็กๆนับพัน ภาษีนั้นเป็นส่วนแบ่งที่มีนัยสำคัญของไฟล์

Object stream รวบรวมอ็อบเจกต์ที่ไม่ใช่สตรีมขนาดเล็กเหล่านั้นจำนวนมากเข้าไว้ในสตรีมเดียวและบีบอัดมันด้วยกัน (Spec: ISO 32000-2, §7.5.7) ซองจดหมาย obj / endobj ถูกทิ้งไปสำหรับทั้งกลุ่ม ค่าที่อยู่ภายในถูกจัดเก็บติดกันโดยไม่มีคีย์เวิร์ดต่ออ็อบเจกต์ จากนั้นถูก deflate เป็นบล็อกเดียว — ซึ่งยังบีบอัด ได้ดีกว่า เพราะตัวบีบอัดที่ขจัดข้อมูลซ้ำตอนนี้เห็น dictionary ที่คล้ายกันทั้งหมดในคราวเดียว cross-reference entry ไม่ได้หายไป — แต่ละอ็อบเจกต์ที่แพ็กไว้ยังต้องการหนึ่งรายการ — แต่มันหดเหลือเป็น type-2 entry แบบไบนารีที่กะทัดรัดใน cross-reference stream (รายละเอียดเพิ่มในคันโยกที่ 2)

ใน NextPDF สิ่งนี้ถูกส่งมอบโดย ObjectStreamPacker ตัวประมวลผลภายหลังที่สมบูรณ์ในตัวซึ่งรับ PDF แบบ cross-reference-stream ที่เสร็จแล้ว และเขียนอ็อบเจกต์ที่มีสิทธิ์ใหม่ให้เป็น /Type /ObjStm ตัวเดียว กฎที่มันปฏิบัติตามมาจากมาตรฐานโดยตรง: อ็อบเจกต์ที่มีสิทธิ์คืออ็อบเจกต์ที่เป็น generation-zero และไม่ใช่สตรีม หลังจากกันอ็อบเจกต์พิเศษที่ต้องคงเข้าถึงได้โดยตรงออก §7.5.7 ห้ามจัดเก็บอ็อบเจกต์สตรีมไว้ภายใน object stream ดังนั้นอ็อบเจกต์สตรีม — เนื้อหา ฟอนต์ รูปภาพ — จึงเก็บรายการของตัวเองไว้ และ ObjectStreamPacker ยังปฏิเสธ cross-reference stream object ของเอกสารเอง (มันถูกเขียนใหม่) และ /Encrypt dictionary ซึ่งทั้งคู่ต้องคงเข้าถึงได้โดยตรง ทุกสิ่งอื่นที่เป็น generation-zero และไม่ใช่สตรีมจะถูกแพ็ก

Object stream packing (ObjStm) — edition availability
EditionAvailability
CoreFull support in the open-source core via ObjectStreamPacker. It is opt-in: the single-pass writer’s default emits classic in-use entries, so output stays byte-identical unless you enable packing. The packer is deterministic, with its own reproducible golden baseline.
ProNot in this edition
EnterpriseNot in this edition

การ opt-in เป็นจุดยืนที่ตั้งใจ ไม่ใช่ข้อจำกัดที่ซ่อนอยู่ เอาต์พุตเริ่มต้นเสถียรในระดับไบต์และตรงกับ golden baseline ที่มีอยู่ การเปิดการแพ็กคือการเลือกเข้าสู่เลย์เอาต์ที่ต่างออกไป เล็กกว่า และกำหนดผลแน่นอนเท่ากัน คุณเลือกการแลกเปลี่ยน และเอนจินไม่เคยตัดสินใจมันลับหลังคุณ

เมื่ออ็อบเจกต์อยู่ภายใน object stream ดัชนีที่ชี้ไปยังมันก็เปลี่ยนรูปร่าง โปรแกรมอ่านหาอ็อบเจกต์ที่แพ็กไว้ผ่าน cross-reference entry แบบ บีบอัด — type-2 entry ที่ระบุชื่อ object stream และดัชนีภายในมัน (Spec: ISO 32000-2, §7.5.8.3) เนื่องจาก cross-reference ทั้งหมดเองเป็นสตรีม /Type /XRef ดัชนีสำหรับอ็อบเจกต์นับพันจึงถูกแพ็กแบบไบนารีและ deflate แทนที่จะถูกเขียนเป็นแถวข้อความธรรมดา แผนที่หดลงพร้อมกับดินแดน

ObjectStreamPacker สร้างสิ่งนี้ขึ้นใหม่พอดี: มันปล่อย object stream ตัวเดียว จากนั้น cross-reference stream ที่เขียนใหม่ซึ่งพก type-1 entry ที่กะทัดรัดสำหรับแต่ละอ็อบเจกต์ที่คงไว้และ type-2 entry สำหรับแต่ละอ็อบเจกต์ที่แพ็ก โดยรักษาทุกหมายเลขอ็อบเจกต์เพื่อให้ reference ที่มีอยู่ยังคงใช้ได้

สำหรับเอกสารใดก็ตามที่มีรูปภาพจริง คันโยกนี้ทำให้ตัวอื่นเล็กกระจ้อยร่อย ไบต์คือภาพเดียวกัน codec คืองบประมาณ ชื่อ ฟิลเตอร์อยู่ในชุดฟิลเตอร์มาตรฐาน (Spec: ISO 32000-2, §7.4) และการเลือกระหว่างมันคือการตัดสินใจเรื่องขนาด:

  • FlateDecode ไม่สูญเสียข้อมูล สมบูรณ์แบบสำหรับ line art ภาพหน้าจอ และอะไรก็ตามที่มีสีแบน — และหายนะสำหรับภาพถ่าย ที่ซึ่งไม่สูญเสียข้อมูลหมายถึง ทุกไบต์ของต้นฉบับ
  • DCTDecode คือ JPEG: สูญเสียข้อมูล และสำหรับภาพถ่ายเป็นทางเลือกที่ถูกต้องด้วยส่วนต่างที่กว้าง มักลดลงสิบเท่าสำหรับคุณภาพที่ด้อยลงโดยไม่มีใครสังเกต
  • JPXDecode คือ JPEG 2000: การบีบอัดแบบเวฟเล็ตที่มีเส้นโค้งคุณภาพต่อขนาดต่างออกไป แต่การรองรับไม่สม่ำเสมอและถูกห้ามโดยบางโปรไฟล์การเก็บถาวร

นี่คือจุดที่ วิธีที่ฟิลเตอร์ถอดรหัส สำคัญพอดี และนั่นคืองานของบทความ สตรีมและฟิลเตอร์ ประเด็นเศรษฐศาสตร์แคบกว่า: ภาพถ่ายที่จัดเก็บแบบไม่สูญเสียข้อมูลเป็นสาเหตุเดี่ยวที่พบบ่อยที่สุดของ PDF ที่ใหญ่โตอย่างไม่จำเป็น และไม่ว่าจะแพ็ก object stream มากเพียงใดก็ไม่อาจกอบกู้ไฟล์ที่น้ำหนักจริงคือภาพสแกนที่ไม่ผ่าน JPEG หนึ่งภาพได้

ฟอนต์ฝังตัวคือโปรแกรม ฟอนต์เต็มตัวอาจหลายร้อยกิโลไบต์ เพราะมันพกทุก glyph ที่นักออกแบบ typeface เคยวาด — อักขระนับพันข้ามภาษาที่คุณจะไม่มีวันใช้ในเอกสารนี้ Subsetting ฝังไว้เฉพาะ glyph ที่เอกสารวาดจริง เปลี่ยนโปรแกรมนั้นให้เป็นเศษเสี้ยวเล็กๆของตัวมันเอง จดหมายหน้าเดียวไม่ต้องการฟอนต์ที่รองรับ CJK ทั้งตัว มันต้องการ glyph ไม่กี่สิบตัวที่มันใช้ กลไกว่า glyph ถูกเลือกและทำดัชนีใหม่อย่างไรเป็นหัวข้อของตัวเอง — ดู ฟอนต์: ส่วนที่ยาก

  1. Image bytesFor media-heavy files, the largest line item. The filter choice — lossless Flate vs a lossy image codec like DCT (or JPX in a lossy mode) — is the dominant size lever.
  2. Font bytesA full embedded font is mostly glyphs you never draw. Subsetting keeps only the used glyphs, often shrinking the program by an order of magnitude.
  3. Per-object overheadThousands of small dictionaries each pay an obj/endobj envelope plus a cross-reference entry. Object streams (ObjStm) drop the envelope for the whole group; each object keeps a compact type-2 cross-reference entry.
  4. The indexA compressed cross-reference stream with type-2 entries binary-packs and deflates the map of every object, replacing plaintext index rows.
Where the bytes go: the four levers an author spends a PDF's size budget on, in the order they typically dominate a real file.

ไม่มี API ที่ปั้นขึ้นมาให้แสดงตรงนี้ เพราะการตัดสินใจเรื่องขนาดที่สำคัญที่สุดเกิดขึ้น ก่อน ที่ไบต์จะไปถึงตัวเขียน — และคันโยกเชิงโครงสร้างเพียงตัวเดียวที่ NextPDF เปิดเผยคือ opt-in ตัวเดียว ในเชิงแนวคิด งบประมาณอ่านได้เช่นนี้:

  • ป้อนภาพถ่ายเป็น JPEG เพื่อให้มันลงตัวภายใต้ DCTDecode ไม่ใช่ถูกเข้ารหัสใหม่แบบไม่สูญเสียข้อมูล ชัยชนะที่ใหญ่ที่สุดคือการเลือกเกี่ยวกับ ข้อมูลต้นทาง ไม่ใช่แฟล็กของตัวเขียน
  • ให้ตัวเขียนทำ subset ฟอนต์ฝังตัวเพื่อให้ส่งเฉพาะ glyph ที่วาด
  • สำหรับเอกสารที่มีอ็อบเจกต์เล็กๆจำนวนมาก จง opt into การแพ็ก object stream ซึ่งส่งไฟล์ที่เสร็จแล้วผ่าน ObjectStreamPacker เพื่อจัดกลุ่มอ็อบเจกต์ที่ไม่ใช่สตรีมขนาดเล็กและเขียน cross-reference stream ใหม่

เส้นทางเริ่มต้น — in-use entry แบบดั้งเดิม ไม่มี ObjStm — คือ baseline ที่ถูกต้อง: กำหนดผลแน่นอน เสถียรในระดับไบต์ และตรวจสอบง่าย การแพ็กคือการอัปเกรดที่พิจารณาแล้วเมื่อจำนวนอ็อบเจกต์ ไม่ใช่น้ำหนักรูปภาพ คือสิ่งที่ทำให้ไฟล์บวม

กับดักคือการเอื้อมไปหาปุ่ม “บีบอัด PDF” และคาดหวังว่ามันจะแก้ทุกอย่าง การบีบอัดไม่ใช่คันโยกเดียว มันคือสี่ตัว และมันไม่ทดแทนกัน การแพ็ก object stream ไม่อาจหดภาพถ่ายได้ — นั่นคืองานของฟิลเตอร์รูปภาพ JPEG ที่สมบูรณ์แบบไม่อาจชดเชยฟอนต์ที่คุณลืมทำ subset และไม่มีอะไรช่วยได้หากความบวมที่แท้จริงคือภาพสแกน 10 MB ที่จัดเก็บแบบไม่สูญเสียข้อมูลเพราะไม่มีใครเลือก DCTDecode ให้มัน

ความเข้าใจผิดข้อที่สองคือยิ่งเล็กยิ่งดีเสมอ มันไม่ object stream เข้ากันไม่ได้กับ linearization — เลย์เอาต์ fast-web-view ที่ตรึงตำแหน่งอ็อบเจกต์แบบสัมบูรณ์เพื่อให้หน้าแรกสตรีมเข้ามาแต่เนิ่นๆ และบางโปรไฟล์การเก็บถาวรจำกัดว่าฟิลเตอร์รูปภาพใดที่อนุญาตด้วยซ้ำ ขนาดเป็นแกนหนึ่ง มันแลกกับการสตรีม ความสอดคล้องการเก็บถาวร และความสามารถในการทำซ้ำ และจุดที่ถูกต้องบนเส้นโค้งขึ้นอยู่กับว่าเอกสารมีไว้เพื่ออะไร

คันโยกสี่ตัวคือเศรษฐศาสตร์ เชิงโครงสร้าง ของขนาดไฟล์ มันไม่ใช่การรับประกัน “ทำให้เล็กลง” แบบสากล และ NextPDF ไม่แกล้งทำเป็นตัวปรับให้เหมาะสมใหม่สำหรับ PDF ขาเข้าตามอำเภอใจ

ObjectStreamPacker คือการปรับให้เหมาะสมแบบ best-effort ที่ไม่เคยเสี่ยงต่อความถูกต้อง มันปฏิเสธ — โดยคืนอินพุตที่ไม่เปลี่ยนแปลง — เมื่อไฟล์ไม่ใช่ PDF แบบ cross-reference-stream เมื่อมันถูกเข้ารหัส เมื่อมันพกลายเซ็นดิจิทัล (การจัดวางอ็อบเจกต์ใหม่จะขยับ byte range ที่ลายเซ็นปกป้อง) เมื่อมันมี object stream อยู่แล้ว หรือเมื่อไม่มีอ็อบเจกต์ที่มีสิทธิ์ให้แพ็ก มันปล่อย object stream ตัวเดียวพอดีสำหรับทั้งเอกสาร มันไม่แยกออกเป็นคอลเลกชัน /Extends ซึ่งอยู่นอกขอบเขตและสอดคล้องกับมาตรฐานสำหรับขนาดเอกสารที่ NextPDF ผลิต

คันโยกรูปภาพและฟอนต์ส่วนใหญ่เป็นการตัดสินใจเกี่ยวกับ อินพุต ตัวเขียนของ NextPDF ไม่ทรานส์โค้ด bitmap แบบไม่สูญเสียข้อมูลให้เป็นสตรีมรูปภาพแบบสูญเสียข้อมูลโดยปริยาย — การเข้ารหัสภาพถ่ายใหม่เป็น DCTDecode หรือ JPXDecode เป็นการตัดสินใจเรื่องการเข้ารหัสรูปภาพต้นน้ำที่ชัดเจน ไม่ใช่สิ่งที่ serializer ทำลับหลังคุณ — และมันก็ไม่เรียกคืน glyph จากฟอนต์ที่ผู้เรียกขอให้ฝังเต็มตัว ชัยชนะเรื่องขนาดที่ใหญ่ที่สุดเกิดขึ้นต้นน้ำของตัว serialize ไบต์ งานของเอนจินคือไม่เสียงบประมาณที่คุณนำมาให้มัน

  • Object stream (ObjStm) — สตรีมเดียวที่เก็บ indirect object ที่ไม่ใช่สตรีมขนาดเล็กจำนวนมาก บีบอัดด้วยกัน เพื่อให้ซองจดหมาย obj / endobj ถูกทิ้งไปสำหรับทั้งกลุ่ม แต่ละอ็อบเจกต์ที่แพ็กไว้ยังคงเก็บ cross-reference entry ของตัวเอง ในรูปของ type-2 entry ที่กะทัดรัด เป็น opt-in ใน NextPDF
  • Indirect object — อ็อบเจกต์ที่มีหมายเลขในกราฟของ PDF ห่อหุ้มไว้ในซองจดหมาย obj / endobj และติดตามด้วยดัชนี cross-reference ซองจดหมายคือโอเวอร์เฮดต่ออ็อบเจกต์
  • Cross-reference stream — สตรีม /Type /XRef ที่ทำดัชนีให้ทุกอ็อบเจกต์ แพ็กแบบไบนารีและ deflate แทนที่จะเขียนเป็นแถวข้อความธรรมดา
  • Compressed (type-2) entry — cross-reference entry ที่ชี้ไปยังอ็อบเจกต์ที่อยู่ภายใน object stream โดยระบุชื่อสตรีมและดัชนีภายในมัน
  • Font subsetting — การฝังไว้เฉพาะ glyph ที่เอกสารวาดจริง แทนที่จะเป็น typeface เต็มตัว ทำให้โปรแกรมฟอนต์ฝังตัวหดลง
  • Lossless vs lossy filter — codec แบบไม่สูญเสียข้อมูล (FlateDecode) สร้างทุกไบต์ขึ้นใหม่ ส่วน codec รูปภาพแบบสูญเสียข้อมูล (DCTDecode หรือ JPXDecode ในโหมดสูญเสียข้อมูล) ทิ้งรายละเอียดที่ไม่อาจรับรู้ได้เพื่อผลลัพธ์ที่เล็กกว่ามาก (JPEG 2000 / JPX ยังกำหนดค่าเป็นแบบไม่สูญเสียข้อมูลได้ด้วย) การเลือกคือคันโยกหลักของไฟล์ที่มีสื่อหนัก