تخطَّ إلى المحتوى

الأدفاق والمرشِّحات

Evidence: Standard-backed

معظم بايتات ملف ⁨PDF⁩ الحقيقي تقع داخل الأدفاق: محتوى الصفحات، والخطوط، والصور، ودفق الإحالة المرجعية نفسه. ونادرًا ما تُخزَّن هذه البايتات خامًا؛ فهي تمرّ أولًا عبر مرشِّح واحد أو أكثر. تتناول هذه الصفحة المرشِّحات التي تصادفها، والغرض من كلٍّ منها، ومواضع تعثُّرها، ولماذا يثبِّت ⁨NextPDF⁩ ضغطه بحيث يُنتج المُدخل نفسه البايتات نفسها دائمًا.

الدفق ومرشِّحه عقدٌ بينهما: “هذه البايتات مضغوطة بـ ⁨deflate⁩، ثم مرمَّزة بـ ⁨base-85⁩ — فكّ الترميز بهذا الترتيب للحصول على البيانات الحقيقية.” إذا خالف مدخل /Filter حقيقة البايتات، أو كانت قيمة /Length خاطئة، أو أُدرج مرشِّحان بترتيب خاطئ، أصبح الدفق غير قابل لفكّ الترميز وفُقد الكائن الذي كان يحمله. لا يخمِّن القارئ استدلاليًّا؛ بل يفعل ما يمليه عليه القاموس.

وهناك كلفة ثانية أقل ظهورًا. إذا كان ضاغط المكتبة غير حتميّ — إصدار ⁨zlib⁩ مختلف، أو مستوى مختلف، أو حدود كتل داخلية مختلفة — فإن تشغيلين كان يُفترض أن يُنتجا ملف ⁨PDF⁩ متطابقًا يُنتجان ملفّين مختلفين. وهذا يكسر قابلية إعادة الإنتاج على مستوى البايت. وكسر قابلية إعادة الإنتاج يكسر بدوره اختبارات الملف الذهبي، والتحقق من البناء الموقَّع، وأيّ خطّ معالجة يقارن المُخرجات بالفروق. لذلك تحدِّد المرشِّحات ما إذا كان ملف ⁨PDF⁩ صحيحًا وما إذا كان مطابقًا في الوقت نفسه.

  • كائن الدفق هو قاموس مضافًا إليه كتلة من البايتات، محصورة بين streamendstream، ومعها /Length وعادةً /Filter.
  • يسمِّي مدخل /Filter مرشِّح فكّ الترميز — أو مصفوفة من المرشِّحات تعمل كـ خطّ معالجة بالترتيب.
  • تنقسم المرشِّحات إلى عائلتين: الضغط (⁨FlateDecode⁩ و⁨LZWDecode⁩ و⁨RunLengthDecode⁩ و⁨DCTDecode⁩ و⁨JPXDecode⁩ و⁨JBIG2Decode⁩) والنقل بترميز ⁨ASCII⁩ (⁨ASCIIHexDecode⁩ و⁨ASCII85Decode⁩)، إضافةً إلى مرشِّح ⁨Crypt⁩ الخاص بالتشفير.
  • والمرشِّح الذي ستراه أكثر من غيره هو ⁨FlateDecode⁩ — ⁨zlib/deflate.⁩ وهو الافتراضي للمحتوى والخطوط ودفق الإحالة المرجعية.
  • يثبِّت ⁨NextPDF⁩ ناتج ⁨Flate⁩ على مستوى وتنسيق ثابتين بحيث تُضغط بايتات المُدخل نفسها دائمًا إلى بايتات المُخرج نفسها.

يُصدر ⁨NextPDF⁩ كائنات الدفق عبر مساعد تخزين مؤقت واحد، ويضغط عبر ضاغط مثبَّت واحد — وذلك عن قصد.

تغلِّف BinaryBuffer::writeStream() (src/Support/BinaryBuffer.php) محتوى الدفق داخل قاموسه، فتكتب دائمًا قيمة /Length مساوية لطول البايتات الفعلي، وتدمج أيّ إدخالات إضافية يوفِّرها المستدعي، مثل /Filter. ولا يوجد مسار يمكن فيه أن يتعارض الطول المُعلَن مع البايتات المكتوبة، لأن الطول يُؤخذ من سلسلة المحتوى نفسها.

يجري الضغط عبر PinnedZlibCompressor (src/Writer/PinnedZlibCompressor.php). يوجد هذا الصنف لسبب واحد. إن استخدام gzcompress دون مستوى صريح يعتمد على القيمة الافتراضية في زمن تشغيل ⁨zlib⁩، وهي قيمة تباينت تاريخيًّا بين الإصدارات. بل إن ترويسة ⁨zlib⁩ المؤلَّفة من بايتين ترمِّز المستوى ترميزًا غير مباشر، فلا يكون “الافتراضي” ناتجًا مستقرًّا. يثبِّت الضاغط المستوى عند الحدّ الأقصى لـ ⁨RFC 1951⁩ ويُصدر دائمًا ⁨deflate⁩ مغلَّفًا بـ ⁨zlib⁩ (ترويسة ⁨RFC 1950⁩ + ذيل ⁨Adler-32⁩)، وهو بالضبط ما يتوقّعه /Filter /FlateDecode. ويتحوّل الفشل الصريح من ⁨zlib⁩ إلى استثناء مُحدَّد النوع بدلًا من الرجوع صامتًا إلى ناتج غير مضغوط — فلا يُصدَر دفق خامًا في صمت أبدًا.

دفق الإحالة المرجعية نفسه مثالٌ تطبيقيّ على كلّ ذلك: يبني CrossReferenceStream (src/Core/CrossReferenceStream.php) جدولًا ثنائيًّا، ويضغطه، ويُصدره ككائن دفق يحمل /Type /XRef، ومصفوفة عرض الحقول /W، و/Filter /FlateDecode. فالفهرس الذي يتيح للقارئ العثور على كلّ كائن هو، بدوره، دفق مُرشَّح.

المرشِّحالعائلةالغرض منهموضع تعثُّره
⁨FlateDecode⁩ضغط⁨zlib/deflate⁩؛ الافتراضي للمحتوى والخطوط وأدفاق ⁨xref⁩إصدار ⁨zlib⁩ غير الحتميّ يجعل ملفات ⁨PDF⁩ “المتطابقة” تختلف بايتًا ببايت
⁨LZWDecode⁩ضغطضغط ⁨Lempel⁩–⁨Ziv⁩–⁨Welch⁩ الأقدمقديم؛ حلّ ⁨Flate⁩ محلّه، ولا يزال يُرى أحيانًا في الملفات القديمة
⁨DCTDecode⁩ضغطصور ⁨colour/grayscale⁩ مرمَّزة بـ ⁨JPEG⁩فاقد — إعادة ترميز صورة مرمَّزة بـ ⁨DCT⁩ سلفًا تُتلفها مجدَّدًا
⁨JPXDecode⁩ضغطبيانات صور ⁨JPEG 2000⁩ المويجيةغير مسموح به في بعض الملامح الأرشيفية؛ والدعم الواسع له متفاوت
⁨JBIG2Decode⁩ضغطضغط الصور ثنائية المستوى (بت واحد)يجب ألّا يُستخدم مع الصور المضمَّنة؛ والأوضاع الفاقدة قد تُغيِّر المستندات الممسوحة
⁨RunLengthDecode⁩ضغطترميز طول التشغيل على مستوى البايتلا يفيد إلا البيانات ذات سلاسل البايت الواحد الطويلة؛ وقد يزيد حجم البيانات الأخرى
⁨ASCIIHexDecode⁩نقلالبيانات الثنائية على هيئة أرقام ست عشريةيضاعف الحجم؛ للقنوات الآمنة لـ 7 بتات فقط، لا للحجم أبدًا
⁨ASCII85Decode⁩نقلالبيانات الثنائية على هيئة ⁨ASCII⁩ بترميز ⁨base-85⁩عبء إضافي بنحو 25%؛ تسهيل للنقل، لا ضغط
⁨Crypt⁩أمانيطبِّق معالج أمان المستنديجب على دفق الإحالة المرجعية ألّا يستخدم مرشِّح ⁨Crypt⁩

مجموعة مرشِّحات ⁨PDF⁩ القياسية، حسب العائلة، مع الإخفاق المرتبط كلٌّ منها. يكتب ⁨NextPDF⁩ ترميز ⁨FlateDecode⁩ للمحتوى والخطوط ودفق الإحالة المرجعية؛ ومرشِّحات النقل بترميز ⁨ASCII⁩ للقنوات ذات 7 بتات، لا لتقليل الحجم أبدًا.

آلية المرشِّح معرَّفة في Spec: ISO 32000-2, §7.4 . يسمِّي قاموس الدفق مرشِّحاته عبر /Filter. عندما يُدرج المدخل أكثر من مرشِّح، تشكِّل تلك المرشِّحات خطّ معالجة لفكّ الترميز وتُطبَّق بالتتابع. يرمِّز الكاتب الدفق لضغطه أو لجعله آمنًا لـ 7 بتات. ويستدعي القارئ مرشِّحات فكّ الترميز المقابلة لاستعادة البيانات الأصلية. Evidence: Standard-backed

يصنِّف جدول المرشِّحات في المعيار كلَّ مرشِّح. يفكّ ⁨FlateDecode⁩ ضغط البيانات المرمَّزة بـ ⁨zlib/deflate-encoded⁩، فيعيد إنتاج النص الأصلي أو البيانات الثنائية الأصلية. ويعيد ⁨DCTDecode⁩ إنتاج عيِّنات صورة تقارب الأصل عبر ⁨JPEG⁩ — وكلمة “تقارب” هي تنبيه المعيار لك إلى أنه فاقد. وكلٌّ من ⁨LZWDecode⁩ و⁨RunLengthDecode⁩ و⁨JBIG2Decode⁩ و⁨JPXDecode⁩ ومرشِّح ⁨Crypt⁩ معرَّف هناك أيضًا، مع منع ⁨JBIG2⁩ صراحةً من الصور المضمَّنة.

يطبِّق دفق الإحالة المرجعية آلية التنسيق نفسها على نفسه: فهو كائن دفق (/Type /XRef، Spec: ISO 32000-2, §7.5.8 ) تذكر مصفوفة /W فيه عرض البايتات لكلّ حقل مدخل في الدفق بعد فكّ ترميزه. ويشترط المعيار ألّا يكون مشفَّرًا وألّا يستخدم مرشِّح ⁨Crypt.⁩ يتّبع CrossReferenceStream في ⁨NextPDF⁩ هذا بدقّة — ⁨FlateDecode⁩، و/W صريحة، ودون تشفير.

دفق محتوى صفحة، مضغوط بـ ⁨Flate.⁩ هذا هو الشكل الأكثر شيوعًا بفارق كبير: قاموس به /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 يقول ذلك، فيستعيد المعامِلات الأصلية. وبما أن الضاغط مثبَّت، لا تكون رحلة الذهاب والإياب صحيحةً فحسب، بل تكون مطابقة في كلّ مرة، وهو ما تعتمد عليه فحوص الملف الذهبي والبناء الموقَّع.

الفخّ هو معاملة مرشِّحات ⁨ASCII⁩ على أنها وسيلة ضغط. يجعل ⁨ASCIIHexDecode⁩ و⁨ASCII85Decode⁩ الدفق أكبر — نحو الضعف، ونحو 25% على التوالي. وهي موجودة لنقل البيانات الثنائية عبر قناة آمنة للنص ذي 7 بتات فقط، لا لتوفير المساحة. واختيار ⁨ASCII85⁩ لـ “تقليص” ملف ⁨PDF⁩ يفعل العكس. والنصف الثاني من المفهوم الخاطئ ذاته هو الاعتقاد بأن ⁨FlateDecode⁩ غير فاقد للصور “بلا مقابل”. إن ⁨Flate⁩ غير فاقد فعلًا، لكن إذا كانت الصورة مرمَّزة سلفًا بـ ⁨DCT⁩ (⁨JPEG⁩)، فإن تغليفها مجدَّدًا أو إعادة ترميزها عبر مرشِّح فاقد يُتلفها بصرف النظر عمّا يفعله ⁨Flate⁩ من حولها. يحفظ خطّ معالجة المرشِّحات ما تُدخله إليه بالضبط — بما في ذلك أيّ أثر لإعادة ضغط أدخلته إليه عرَضًا.

تتناول هذه الصفحة كيفية الإعلان عن المرشِّحات وتطبيقها، لا تفاصيل الخوارزمية على مستوى البتات داخل كلٍّ منها. ضمان الحتمية يخصّ تحديدًا ناتج ⁨Flate⁩ في ⁨NextPDF⁩ للأدفاق التي يكتبها. وهو يصمد عبر إصدارات ⁨PHP⁩ الثانوية وإصدارات ⁨zlib⁩ المطابقة للمعايير، لكن المعيار يسمح صراحةً لمرمِّز ⁨deflate⁩ باختيار حدود كتل داخلية مختلفة، لذا فإن الناتج المتطابق بايتًا ببايت عبر تطبيقات ⁨zlib⁩ المختلفة فعلًا (مثل ⁨zlib⁩ القياسي مقابل ⁨zlib-ng⁩) ليس مضمونًا. ولهذا السبب تُثبَّت بيئة البناء.

يختار ⁨NextPDF⁩ ترميز ⁨FlateDecode⁩ ومرشِّحات النقل بترميز ⁨ASCII⁩ للبيانات التي يُصدرها. وهو ليس مُحوِّل ترميز صور. وهو لا يعِد بإعادة تعبئة أيّ دفق ⁨JPEG2000⁩ أو ⁨JBIG2⁩ وارد أيًّا كان شكله، والمفاضلات في الصور الفاقدة سمةٌ من سمات بيانات المصدر، وليست شيئًا يستطيع الكاتب التراجع عنه.

لماذا يوجد ⁨FlateDecode⁩ في كلّ مكان؟ لأنه غير فاقد، وعامّ الغرض، ومدعوم جيدًا، ومناسب لمحتوى النصوص والمعامِلات في معظم ملفات ⁨PDF.⁩ وهو الافتراضي الآمن لأدفاق المحتوى والخطوط المضمَّنة ودفق الإحالة المرجعية.

هل يمكنني إيقاف الضغط؟ يمكنك حذف /Filter وتخزين البايتات خامًا، وسيقبلها القارئ. سيكبر الملف، ولن يتحسّن أي شيء آخر؛ ونادرًا ما يوجد سبب لذلك خارج تنقيح الأخطاء.

لماذا نثبِّت مستوى الضغط من الأساس؟ لكي يكون الناتج قابلًا لإعادة الإنتاج. قد يغيِّر المستوى غير المثبَّت (أو إصدار ⁨zlib⁩) البايتات المضغوطة دون تغيير المحتوى بعد فكّ الضغط — فيبقى صحيحًا، لكنه غير مطابق، وهو ما يُبطل التحقق على مستوى البايت.

  • كائن الدفق — قاموس مع كتلة من البايتات بين stream وendstream، يحمل /Length وعادةً /Filter.
  • المرشِّح — تحويل فكّ ترميز مُسمّى يطبِّقه القارئ على بايتات الدفق (مثل FlateDecode).
  • خطّ معالجة المرشِّحات — مصفوفة من المرشِّحات تُطبَّق بالتتابع؛ وترتيب المصفوفة هو ترتيب فكّ الترميز.
  • ⁨FlateDecode⁩ — مرشِّح ⁨zlib/deflate⁩؛ الضغط الافتراضي للمحتوى والخطوط وأدفاق الإحالة المرجعية.
  • ⁨DCTDecode⁩ — مرشِّح صور ⁨JPEG⁩؛ فاقد، فإعادة الترميز تُتلف الصورة مجدَّدًا.
  • مرشِّح النقل بترميز ⁨ASCII⁩ — ⁨ASCIIHexDecode⁩ / ⁨ASCII85Decode⁩؛ يجعل البيانات آمنة لـ 7 بتات على حساب الحجم — وليس ضغطًا.
  • الضغط الحتميّ — إنتاج ناتج مضغوط متطابق بايتًا ببايت للمُدخل المتطابق، يتحقّق بتثبيت مستوى الضاغط وتنسيقه.