สตรีมและฟิลเตอร์
ISO 32000-2 §7.4 Evidence: Standard-backed
ภาพรวมโดยสังเขป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสังเขป”ไบต์ส่วนใหญ่ของไฟล์ PDF จริงอยู่ภายใน สตรีม ได้แก่ เนื้อหาของหน้า ฟอนต์ รูปภาพ และตัว cross-reference stream เอง ไบต์เหล่านี้แทบไม่เคยถูกจัดเก็บแบบดิบ แต่จะผ่าน ฟิลเตอร์ หนึ่งตัวหรือมากกว่าก่อนเสมอ หน้านี้อธิบายฟิลเตอร์ที่มักพบ แต่ละตัวมีไว้เพื่ออะไร มักเกิดปัญหาที่จุดใด และเหตุใด NextPDF จึงตรึงการบีบอัดของตนไว้เพื่อให้อินพุตเดียวกันสร้างไบต์ชุดเดิมเสมอ
เหตุใดเรื่องนี้จึงสำคัญ
หัวข้อที่มีชื่อว่า “เหตุใดเรื่องนี้จึงสำคัญ”สตรีมและฟิลเตอร์ของสตรีมเป็นข้อตกลงอย่างหนึ่ง: “ไบต์เหล่านี้ถูกบีบอัดแบบ deflate แล้วเข้ารหัสแบบ base-85 — ถอดรหัสตามลำดับนั้นเพื่อให้ได้ข้อมูลจริง” หากรายการ /Filter ไม่ตรงกับเนื้อหาจริงของไบต์เหล่านั้น หรือ /Length ผิด หรือมีฟิลเตอร์สองตัวที่ระบุไว้ผิดลำดับ สตรีมนั้นจะถอดรหัสไม่ได้และอ็อบเจกต์ที่สตรีมบรรจุไว้ก็จะสูญหาย โปรแกรมอ่านจะไม่ใช้ฮิวริสติกคาดเดาเอง แต่จะทำตามที่ dictionary บอกไว้
ยังมีต้นทุนอีกอย่างหนึ่งที่มองเห็นได้ยากกว่า หากตัวบีบอัดของไลบรารีไม่กำหนดผลแน่นอน — zlib คนละบิลด์ ระดับการบีบอัดคนละค่า ขอบเขตบล็อกภายในคนละแบบ — การรันสองครั้งที่ควรสร้าง PDF ที่เหมือนกันกลับสร้างไฟล์ที่ต่างกันสองไฟล์ สิ่งนั้นทำให้การทำซ้ำได้ในระดับไบต์เสียไป เมื่อการทำซ้ำได้เสียไป การทดสอบแบบ golden-file การตรวจสอบบิลด์ที่มีลายเซ็น และไปป์ไลน์ใดๆที่เปรียบเทียบเอาต์พุตก็เสียตามไปด้วย ฟิลเตอร์เป็นตัวกำหนดทั้งว่า PDF ถูกต้องหรือไม่ และว่า PDF เหมือนเดิม หรือไม่
ฉบับย่อ
หัวข้อที่มีชื่อว่า “ฉบับย่อ”- อ็อบเจกต์สตรีม คือ dictionary รวมกับบล็อกไบต์ ซึ่งถูกห่อหุ้มไว้ใน
stream…endstreamพร้อม/Lengthและโดยปกติจะมี/Filter - รายการ
/Filterระบุชื่อฟิลเตอร์ถอดรหัส — หรือ อาร์เรย์ ของฟิลเตอร์ที่ใช้เป็น ไปป์ไลน์ ตามลำดับ - ฟิลเตอร์แบ่งออกเป็นสองตระกูล: การบีบอัด (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) และ การขนส่งแบบ ASCII (ASCIIHexDecode, ASCII85Decode) รวมถึงฟิลเตอร์ Crypt พิเศษสำหรับการเข้ารหัสลับ
- ฟิลเตอร์ที่พบมากที่สุดคือ FlateDecode — zlib/deflate ซึ่งเป็นค่าเริ่มต้นสำหรับเนื้อหา ฟอนต์ และ cross-reference stream
- NextPDF ตรึงเอาต์พุต Flate ของตนไว้ที่ระดับและรูปแบบที่กำหนดตายตัว เพื่อให้อินพุตไบต์เดียวกันบีบอัดออกมาเป็นเอาต์พุตไบต์ชุดเดิมเสมอ
NextPDF จัดการเรื่องนี้อย่างไร
หัวข้อที่มีชื่อว่า “NextPDF จัดการเรื่องนี้อย่างไร”NextPDF ปล่อยอ็อบเจกต์สตรีมผ่านตัวช่วยบัฟเฟอร์เพียงตัวเดียว และบีบอัดผ่านตัวบีบอัดที่ตรึงไว้เพียงตัวเดียว — โดยตั้งใจ
BinaryBuffer::writeStream() (src/Support/BinaryBuffer.php) ห่อหุ้มเนื้อหาสตรีมไว้ใน dictionary ของสตรีม โดยเขียน /Length ให้เท่ากับความยาวไบต์จริงเสมอ และผสานรายการเพิ่มเติมใดๆที่ผู้เรียกระบุเข้ามา เช่น /Filter จึงไม่มีเส้นทางใดที่ทำให้ความยาวที่ประกาศไว้ขัดแย้งกับไบต์ที่เขียนได้ เพราะความยาวถูกนำมาจากสตริงเนื้อหาเอง
การบีบอัดดำเนินผ่าน PinnedZlibCompressor (src/Writer/PinnedZlibCompressor.php) คลาสนี้มีอยู่ด้วยเหตุผลเดียว: gzcompress ที่ไม่ได้ระบุระดับอย่างชัดเจนจะอิงตามค่าเริ่มต้นของรันไทม์ zlib ซึ่งในอดีตแตกต่างกันตามบิลด์ ส่วนหัว zlib ขนาด 2 ไบต์ยังเข้ารหัสระดับไว้โดยอ้อมด้วย ดังนั้น “ค่าเริ่มต้น” จึงไม่ใช่เอาต์พุตที่เสถียร ตัวบีบอัดตรึงระดับไว้ที่ค่าสูงสุดของ RFC 1951 และปล่อย deflate ที่ห่อด้วย zlib เสมอ (ส่วนหัว RFC 1950 + ส่วนท้าย Adler-32) ซึ่งเป็นสิ่งที่ /Filter /FlateDecode คาดหวังพอดี ความล้มเหลวขั้นรุนแรงจาก zlib จะกลายเป็นข้อยกเว้นที่ระบุชนิด แทนที่จะถอยกลับไปใช้เอาต์พุตที่ไม่บีบอัดอย่างเงียบๆ — สตรีมจะไม่มีวันถูกปล่อยออกมาแบบดิบอย่างเงียบๆ
ตัว cross-reference stream เองเป็นตัวอย่างที่รวมทั้งหมดนี้ไว้: CrossReferenceStream (src/Core/CrossReferenceStream.php) สร้างตารางไบนารี บีบอัดตาราง แล้วปล่อยออกมาเป็นอ็อบเจกต์สตรีมที่มี /Type /XRef อาร์เรย์ความกว้างฟิลด์ /W และ /Filter /FlateDecode ดัชนีที่ทำให้โปรแกรมอ่านค้นหาทุกอ็อบเจกต์ได้เองก็เป็นสตรีมที่ผ่านฟิลเตอร์
| ฟิลเตอร์ | ตระกูล | มีไว้เพื่ออะไร | จุดที่เกิดปัญหา |
|---|---|---|---|
| FlateDecode | การบีบอัด | zlib/deflate; ค่าเริ่มต้นสำหรับเนื้อหา ฟอนต์ และ xref stream | บิลด์ zlib ที่ไม่กำหนดผลแน่นอนทำให้ PDF ที่ “เหมือนกัน” ต่างกันแบบไบต์ต่อไบต์ |
| LZWDecode | การบีบอัด | การบีบอัดแบบ Lempel–Ziv–Welch ที่เก่ากว่า | เป็นฟิลเตอร์รุ่นเก่า ถูกแทนที่ด้วย Flate แต่ยังพบได้เป็นครั้งคราวในไฟล์เก่า |
| DCTDecode | การบีบอัด | รูปภาพ colour/grayscale ที่เข้ารหัสแบบ JPEG | สูญเสียข้อมูล — การเข้ารหัสรูปภาพที่เป็น DCT อยู่แล้วซ้ำจะทำให้คุณภาพด้อยลงอีก |
| JPXDecode | การบีบอัด | ข้อมูลรูปภาพแบบเวฟเล็ต JPEG 2000 | โปรไฟล์สำหรับการเก็บถาวรบางตัวไม่อนุญาต การรองรับในวงกว้างยังไม่สม่ำเสมอ |
| JBIG2Decode | การบีบอัด | การบีบอัดรูปภาพแบบสองระดับ (1 บิต) | ต้องไม่ใช้กับ inline image โหมดที่สูญเสียข้อมูลอาจเปลี่ยนแปลงภาพสแกนได้ |
| RunLengthDecode | การบีบอัด | run-length ระดับไบต์ | ช่วยได้เฉพาะข้อมูลที่มีไบต์เดี่ยวซ้ำกันยาวๆเท่านั้น และอาจทำให้ข้อมูลอื่น ใหญ่ขึ้น ได้ |
| ASCIIHexDecode | การขนส่ง | ไบนารีในรูปเลขฐานสิบหก | ทำให้ขนาดเพิ่มเป็นสองเท่า ใช้ได้เฉพาะกับช่องสัญญาณที่ปลอดภัยสำหรับ 7 บิตเท่านั้น ไม่ใช่เพื่อขนาด |
| ASCII85Decode | การขนส่ง | ไบนารีในรูป base-85 ASCII | โอเวอร์เฮด ~25% มีไว้เพื่อความสะดวกในการขนส่ง ไม่ใช่การบีบอัด |
| Crypt | ความปลอดภัย | นำตัวจัดการความปลอดภัยของเอกสารมาใช้ | cross-reference stream ต้อง ไม่ ใช้ฟิลเตอร์ Crypt |
ชุดฟิลเตอร์มาตรฐานของ PDF จำแนกตามตระกูล พร้อมรูปแบบความล้มเหลวที่เกี่ยวข้องกับแต่ละตัว NextPDF เขียน FlateDecode สำหรับเนื้อหา ฟอนต์ และ cross-reference stream ส่วนฟิลเตอร์การขนส่งแบบ ASCII นั้นมีไว้สำหรับช่องสัญญาณ 7 บิต ไม่ใช่เพื่อลดขนาด
หลักฐานบอกอะไร
หัวข้อที่มีชื่อว่า “หลักฐานบอกอะไร”กลไกฟิลเตอร์นิยามไว้ใน Spec: ISO 32000-2, §7.4 ISO 32000-2 §7.4 โดย dictionary ของสตรีมระบุชื่อฟิลเตอร์ของตนผ่าน /Filter เมื่อรายการระบุฟิลเตอร์มากกว่าหนึ่งตัว ฟิลเตอร์เหล่านั้นจะประกอบกันเป็นไปป์ไลน์การถอดรหัสและถูกใช้ตามลำดับ โปรแกรมเขียนเข้ารหัสสตรีมเพื่อบีบอัดหรือเพื่อให้ปลอดภัยสำหรับ 7 บิต โปรแกรมอ่านเรียกใช้ฟิลเตอร์ถอดรหัสที่สอดคล้องกันเพื่อกู้คืนข้อมูลต้นฉบับ Evidence: Standard-backed
ตารางฟิลเตอร์ในมาตรฐานแจกแจงฟิลเตอร์แต่ละตัวไว้ FlateDecode คลายการบีบอัดข้อมูลที่เข้ารหัสแบบ zlib/deflate โดยสร้างข้อความหรือข้อมูลไบนารีต้นฉบับขึ้นมาใหม่ DCTDecode สร้างตัวอย่างรูปภาพที่ ใกล้เคียง ต้นฉบับขึ้นมาใหม่ผ่าน JPEG — คำว่า “approximate” ในมาตรฐานบอกชัดว่าฟิลเตอร์นี้สูญเสียข้อมูล LZWDecode, RunLengthDecode, JBIG2Decode, JPXDecode และฟิลเตอร์ Crypt ต่างก็ถูกนิยามไว้ที่นั่นเช่นกัน โดย JBIG2 ถูกห้ามไม่ให้ใช้กับ inline image อย่างชัดเจน
cross-reference stream ใช้กลไกของรูปแบบกับตัวเอง: เป็นอ็อบเจกต์สตรีม (/Type /XRef,
Spec: ISO 32000-2, §7.5.8 ISO 32000-2 §7.5.8 ) โดยอาร์เรย์ /W ของสตรีมนี้
ระบุความกว้างไบต์ของแต่ละฟิลด์รายการ ในสตรีมที่ถอดรหัสแล้ว มาตรฐาน
กำหนดว่าสตรีมนั้นต้องไม่ถูกเข้ารหัสลับและต้องไม่ใช้ฟิลเตอร์ Crypt
NextPDF มี CrossReferenceStream ที่ทำตามข้อกำหนดนี้อย่างเคร่งครัด — FlateDecode,
/W ที่ระบุชัดเจน ไม่มีการเข้ารหัสลับ
ตัวอย่างเชิงปฏิบัติ
หัวข้อที่มีชื่อว่า “ตัวอย่างเชิงปฏิบัติ”สตรีมเนื้อหาของหน้าที่บีบอัดด้วย Flate คือรูปแบบที่พบได้บ่อยที่สุด: dictionary ที่มี /Length และ /Filter ตามด้วยไบต์ที่บีบอัดแล้วระหว่าง stream และ endstream
<?php
declare(strict_types=1);
use NextPDF\Writer\PinnedZlibCompressor;
// The marking operators a page content stream carries, uncompressed.$content = "BT /F1 12 Tf 72 712 Td (Hello) Tj ET\n";
// NextPDF compresses through the pinned compressor: fixed level,// fixed zlib-wrapped format. The same $content always yields the// same $compressed bytes, on any supported PHP/zlib build.$compressed = PinnedZlibCompressor::compress($content);
// Emitted as a stream object. /Length is the real byte length of// $compressed; /Filter names the decode the reader must apply.// N 0 obj// << /Length <strlen($compressed)> /Filter /FlateDecode >>// stream// <$compressed bytes>// endstream// endobjโปรแกรมอ่านทำสิ่งที่ตรงกันข้าม: อ่าน /Length ไบต์ ส่งไบต์เหล่านั้นผ่าน FlateDecode ตามที่ /Filter ระบุ แล้วได้โอเปอเรเตอร์ต้นฉบับกลับคืนมา เมื่อมีการตรึงตัวบีบอัดไว้ การไป-กลับนั้นไม่ได้แค่ถูกต้องเท่านั้น แต่ เหมือนกัน ทุกครั้ง ซึ่งเป็นสิ่งที่การตรวจสอบแบบ golden-file และบิลด์ที่มีลายเซ็นต้องพึ่งพา
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”กับดักคือการมองฟิลเตอร์ ASCII ว่าเป็นการบีบอัด ASCIIHexDecode และ ASCII85Decode ทำให้สตรีม ใหญ่ขึ้น — ราวสองเท่าและราว 25% ตามลำดับ ฟิลเตอร์เหล่านี้มีอยู่เพื่อเคลื่อนย้ายข้อมูลไบนารีผ่านช่องสัญญาณที่ปลอดภัยสำหรับข้อความ 7 บิตเท่านั้น ไม่ใช่เพื่อประหยัดพื้นที่ การเลือก ASCII85 เพื่อ “ย่อ” PDF จึงให้ผลตรงกันข้าม อีกครึ่งหนึ่งของความเข้าใจผิดเดียวกันคือการเชื่อว่า FlateDecode ทำให้รูปภาพไม่สูญเสียข้อมูลได้ “แบบฟรีๆ” Flate นั้น ไม่สูญเสียข้อมูล แต่หากรูปภาพถูกเข้ารหัสแบบ DCT (JPEG) มาแล้ว การห่อหุ้มซ้ำหรือการแปลงรหัสผ่านฟิลเตอร์ที่สูญเสียข้อมูลจะทำให้คุณภาพด้อยลง ไม่ว่า Flate จะทำอะไรอยู่รอบๆก็ตาม ไปป์ไลน์ฟิลเตอร์เก็บรักษาสิ่งที่ป้อนเข้าไปไว้อย่างเที่ยงตรง — รวมถึงร่องรอยจากการบีบอัดซ้ำที่ป้อนเข้าไปโดยไม่ตั้งใจด้วย
ขีดจำกัดและขอบเขต
หัวข้อที่มีชื่อว่า “ขีดจำกัดและขอบเขต”หน้านี้ครอบคลุมว่าฟิลเตอร์ถูกประกาศและใช้อย่างไร ไม่ใช่อัลกอริทึมระดับบิตภายในแต่ละตัว การรับประกันการกำหนดผลแน่นอนนั้นเจาะจงที่เอาต์พุต Flate ของ NextPDF สำหรับสตรีมที่ NextPDF เขียน การรับประกันนี้คงอยู่ระหว่างเวอร์ชันย่อยของ PHP และบิลด์ zlib ที่สอดคล้องกับมาตรฐาน แต่มาตรฐานอนุญาตอย่างชัดเจนให้ตัวเข้ารหัส deflate เลือกขอบเขตบล็อกภายในที่ต่างกันได้ ดังนั้นเอาต์พุตที่เหมือนกันแบบไบต์ต่อไบต์ระหว่างสายพัฒนา zlib ที่ ต่างกันอย่างแท้จริง (เช่น zlib มาตรฐานเทียบกับ zlib-ng) จึงไม่ได้รับประกันไว้ สภาพแวดล้อมการบิลด์ถูกตรึงไว้ด้วยเหตุผลนั้น
NextPDF เลือก FlateDecode และฟิลเตอร์การขนส่งแบบ ASCII สำหรับข้อมูลที่ตนเขียนออกมา NextPDF ไม่ใช่ตัวแปลงรหัสรูปภาพ NextPDF ไม่รับประกันว่าจะแพ็กสตรีม JPEG2000 หรือ JBIG2 ขาเข้าใดๆขึ้นใหม่ และผลแลกเปลี่ยนด้านคุณภาพของรูปภาพที่สูญเสียข้อมูลเป็นคุณสมบัติของข้อมูลต้นทาง ไม่ใช่สิ่งที่โปรแกรมเขียนจะย้อนกลับได้
คำถามที่พบบ่อยฉบับย่อ
หัวข้อที่มีชื่อว่า “คำถามที่พบบ่อยฉบับย่อ”เหตุใด FlateDecode จึงมีอยู่ทุกที่ ฟิลเตอร์นี้ไม่สูญเสียข้อมูล ใช้งานได้ทั่วไป รองรับอย่างกว้างขวาง และเหมาะกับเนื้อหาที่เป็นข้อความและโอเปอเรเตอร์ของ PDF ส่วนใหญ่ เป็นค่าเริ่มต้นที่ปลอดภัยสำหรับสตรีมเนื้อหา ฟอนต์ฝังตัว และ cross-reference stream
ปิดการบีบอัดได้หรือไม่ คุณสามารถละเว้น /Filter และจัดเก็บไบต์แบบดิบได้ และโปรแกรมอ่านจะยอมรับ ไฟล์จะใหญ่ขึ้นโดยไม่ได้ประโยชน์อื่นเพิ่มเติม แทบไม่มีเหตุผลที่จะทำเช่นนั้นนอกจากการดีบัก
เหตุใดจึงต้องตรึงระดับการบีบอัดด้วย เพื่อให้เอาต์พุตทำซ้ำได้ ระดับที่ไม่ได้ตรึงไว้ (หรือบิลด์ zlib) สามารถเปลี่ยนไบต์ที่บีบอัดได้โดยไม่เปลี่ยนเนื้อหาที่คลายการบีบอัดแล้ว — ถูกต้อง แต่ไม่ เหมือนกัน ซึ่งทำให้การตรวจสอบระดับไบต์ใช้ไม่ได้
เอกสารที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “เอกสารที่เกี่ยวข้อง”- PDF จริงๆแล้วคืออะไร — โมเดลอ็อบเจกต์ที่เป็นบริบทของสตรีมในหน้านี้
- ฟอนต์: ส่วนที่ยาก — โปรแกรมฟอนต์ที่ฝังตัวอยู่คือสตรีมที่ผ่านฟิลเตอร์ ซึ่งมีรูปแบบความล้มเหลวของตัวเอง
- PDF 2.0: อะไรที่เปลี่ยนไป — วิธีที่ baseline 2.0 จัดการกับสตรีมและ cross-reference stream ที่ NextPDF ใช้เป็นค่าเริ่มต้น
อภิธานศัพท์
หัวข้อที่มีชื่อว่า “อภิธานศัพท์”- อ็อบเจกต์สตรีม — dictionary รวมกับบล็อกไบต์ระหว่าง
streamและendstreamบรรจุ/Lengthและโดยปกติจะมี/Filter - ฟิลเตอร์ — การแปลงถอดรหัสที่มีชื่อกำกับซึ่งโปรแกรมอ่านนำมาใช้กับไบต์ของสตรีม (เช่น
FlateDecode) - ไปป์ไลน์ฟิลเตอร์ — อาร์เรย์ของฟิลเตอร์ที่นำมาใช้ตามลำดับ ลำดับของอาร์เรย์คือลำดับการถอดรหัส
- FlateDecode — ฟิลเตอร์ zlib/deflate การบีบอัดค่าเริ่มต้นสำหรับเนื้อหา ฟอนต์ และ cross-reference stream
- DCTDecode — ฟิลเตอร์รูปภาพ JPEG สูญเสียข้อมูล ดังนั้นการเข้ารหัสซ้ำจึงทำให้ภาพด้อยลงอีก
- ฟิลเตอร์การขนส่งแบบ ASCII — ASCIIHexDecode / ASCII85Decode ทำให้ข้อมูลปลอดภัยสำหรับ 7 บิตโดยแลกกับขนาด — ไม่ใช่การบีบอัด
- การบีบอัดแบบกำหนดผลแน่นอน — การสร้างเอาต์พุตที่บีบอัดแล้วซึ่งเหมือนกันแบบไบต์ต่อไบต์สำหรับอินพุตที่เหมือนกัน ทำได้โดยการตรึงระดับและรูปแบบของตัวบีบอัด