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

สตรีมและฟิลเตอร์

Evidence: Standard-backed

ไบต์ส่วนใหญ่ของไฟล์ PDF จริงอยู่ภายใน สตรีม ได้แก่ เนื้อหาของหน้า ฟอนต์ รูปภาพ และตัว cross-reference stream เอง ไบต์เหล่านี้แทบไม่เคยถูกจัดเก็บแบบดิบ แต่จะผ่าน ฟิลเตอร์ หนึ่งตัวหรือมากกว่าก่อนเสมอ หน้านี้อธิบายฟิลเตอร์ที่มักพบ แต่ละตัวมีไว้เพื่ออะไร มักเกิดปัญหาที่จุดใด และเหตุใด NextPDF จึงตรึงการบีบอัดของตนไว้เพื่อให้อินพุตเดียวกันสร้างไบต์ชุดเดิมเสมอ

สตรีมและฟิลเตอร์ของสตรีมเป็นข้อตกลงอย่างหนึ่ง: “ไบต์เหล่านี้ถูกบีบอัดแบบ deflate แล้วเข้ารหัสแบบ base-85 — ถอดรหัสตามลำดับนั้นเพื่อให้ได้ข้อมูลจริง” หากรายการ /Filter ไม่ตรงกับเนื้อหาจริงของไบต์เหล่านั้น หรือ /Length ผิด หรือมีฟิลเตอร์สองตัวที่ระบุไว้ผิดลำดับ สตรีมนั้นจะถอดรหัสไม่ได้และอ็อบเจกต์ที่สตรีมบรรจุไว้ก็จะสูญหาย โปรแกรมอ่านจะไม่ใช้ฮิวริสติกคาดเดาเอง แต่จะทำตามที่ dictionary บอกไว้

ยังมีต้นทุนอีกอย่างหนึ่งที่มองเห็นได้ยากกว่า หากตัวบีบอัดของไลบรารีไม่กำหนดผลแน่นอน — zlib คนละบิลด์ ระดับการบีบอัดคนละค่า ขอบเขตบล็อกภายในคนละแบบ — การรันสองครั้งที่ควรสร้าง PDF ที่เหมือนกันกลับสร้างไฟล์ที่ต่างกันสองไฟล์ สิ่งนั้นทำให้การทำซ้ำได้ในระดับไบต์เสียไป เมื่อการทำซ้ำได้เสียไป การทดสอบแบบ golden-file การตรวจสอบบิลด์ที่มีลายเซ็น และไปป์ไลน์ใดๆที่เปรียบเทียบเอาต์พุตก็เสียตามไปด้วย ฟิลเตอร์เป็นตัวกำหนดทั้งว่า PDF ถูกต้องหรือไม่ และว่า PDF เหมือนเดิม หรือไม่

  • อ็อบเจกต์สตรีม คือ dictionary รวมกับบล็อกไบต์ ซึ่งถูกห่อหุ้มไว้ใน streamendstream พร้อม /Length และโดยปกติจะมี /Filter
  • รายการ /Filter ระบุชื่อฟิลเตอร์ถอดรหัส — หรือ อาร์เรย์ ของฟิลเตอร์ที่ใช้เป็น ไปป์ไลน์ ตามลำดับ
  • ฟิลเตอร์แบ่งออกเป็นสองตระกูล: การบีบอัด (FlateDecode, LZWDecode, RunLengthDecode, DCTDecode, JPXDecode, JBIG2Decode) และ การขนส่งแบบ ASCII (ASCIIHexDecode, ASCII85Decode) รวมถึงฟิลเตอร์ Crypt พิเศษสำหรับการเข้ารหัสลับ
  • ฟิลเตอร์ที่พบมากที่สุดคือ FlateDecode — zlib/deflate ซึ่งเป็นค่าเริ่มต้นสำหรับเนื้อหา ฟอนต์ และ cross-reference stream
  • NextPDF ตรึงเอาต์พุต Flate ของตนไว้ที่ระดับและรูปแบบที่กำหนดตายตัว เพื่อให้อินพุตไบต์เดียวกันบีบอัดออกมาเป็นเอาต์พุตไบต์ชุดเดิมเสมอ

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 โดย 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 ) โดยอาร์เรย์ /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) สามารถเปลี่ยนไบต์ที่บีบอัดได้โดยไม่เปลี่ยนเนื้อหาที่คลายการบีบอัดแล้ว — ถูกต้อง แต่ไม่ เหมือนกัน ซึ่งทำให้การตรวจสอบระดับไบต์ใช้ไม่ได้

  • อ็อบเจกต์สตรีม — dictionary รวมกับบล็อกไบต์ระหว่าง stream และ endstream บรรจุ /Length และโดยปกติจะมี /Filter
  • ฟิลเตอร์ — การแปลงถอดรหัสที่มีชื่อกำกับซึ่งโปรแกรมอ่านนำมาใช้กับไบต์ของสตรีม (เช่น FlateDecode)
  • ไปป์ไลน์ฟิลเตอร์ — อาร์เรย์ของฟิลเตอร์ที่นำมาใช้ตามลำดับ ลำดับของอาร์เรย์คือลำดับการถอดรหัส
  • FlateDecode — ฟิลเตอร์ zlib/deflate การบีบอัดค่าเริ่มต้นสำหรับเนื้อหา ฟอนต์ และ cross-reference stream
  • DCTDecode — ฟิลเตอร์รูปภาพ JPEG สูญเสียข้อมูล ดังนั้นการเข้ารหัสซ้ำจึงทำให้ภาพด้อยลงอีก
  • ฟิลเตอร์การขนส่งแบบ ASCII — ASCIIHexDecode / ASCII85Decode ทำให้ข้อมูลปลอดภัยสำหรับ 7 บิตโดยแลกกับขนาด — ไม่ใช่การบีบอัด
  • การบีบอัดแบบกำหนดผลแน่นอน — การสร้างเอาต์พุตที่บีบอัดแล้วซึ่งเหมือนกันแบบไบต์ต่อไบต์สำหรับอินพุตที่เหมือนกัน ทำได้โดยการตรึงระดับและรูปแบบของตัวบีบอัด