تدفق كائن PDF يتضخم دون خطأ لكنه لا يزال يُقرأ كضوضاء يفتقد عادة خطوة واحدة: عكس Predictor الخاص بـISO 32000-1. عندما تحمل قاموس /DecodeParms الخاص بتدفق قيمة /Predictor 2 أو أعلى، فإن البايتات التي تعيدها FlateDecode ليست البيانات الأصلية — إنها قيم مفاضلة على مستوى الصف بأسلوب PNG أو مفاضلة أفقيًا بأسلوب TIFF تحتاج تمريرة إعادة بناء ثانية قبل أن يكون لأي بحث في قاموس معنى. أضاف PDFiumPas، مكتبة مكوّن PDF الأصلية القائمة على VCL لـDelphi وC++Builder، تمريرة إعادة البناء تلك في v2.16.0، تحديدًا لأن تدفقات كائنات PDF 1.5+ كانت تتوسع إلى بايتات مفاضلة لم يستطع أي مُحلِّل قاموس قراءتها
لماذا لا يكفي FlateDecode وحده
FlateDecode نفسه ليس سوى فك ضغط DEFLATE (ISO 32000-1 §7.4.4.1): فهو يعيد إنتاج أيًّا كانت البايتات التي سلّمها المُرمّز إلى الضاغط، لا أكثر. يعيش Predictor طبقة أعلى، في قاموس /DecodeParms الخاص بالتدفق، ويصف تحويلًا طبّقه المُرمّز قبل الضغط — تحول المفاضلة سلاسل طويلة من القيم المُهيكلة المتشابهة، مثل الأعداد الصحيحة المُكدَّسة بإحكام داخل تدفق مرجع متقاطع أو تدفق كائن، إلى سلاسل طويلة من أرقام صغيرة يضغطها DEFLATE بشكل أفضل بكثير. ISO 32000-1 §7.4.4.3 (الجدول 8) صريح في أن التراجع عن هذا التحويل جزء من فك ترميز تدفق مُصفّى، لا تمريرة تنظيف اختيارية، ومع ذلك من السهل كتابة مساعد FlateDecode يستدعي فقط inflate ويتوقف عند ذلك
العرَض مميز بمجرد أن تعرف ما تبحث عنه. البايتات المفاضلة بواسطة Predictor ليست ضوضاء عشوائية — لا تزال تحمل شكل تدفق مضغوط، بحيث كثيرًا ما يجتاز مُحلِّل ساذج بضع رموز تبدو صالحة قبل الاصطدام بتسلسل بايتات لا يمكن أن يكون اسم أو رقم أو محدّد PDF، وتفشل صفوف مختلفة عند إزاحات مختلفة تبعًا لمقدار اختلاف القيم الكامنة صادفةً عن جيرانها. عدم الاتساق ذاك هو ما يجعل الخلل صعب التحديد من ملف فاشل واحد: يمكن لملفي PDF من المنتج نفسه أن يختلفا فقط في أي القيم يصادف أن تتكرر، بحيث يُحلَّل أحدهما تقريبًا بالمصادفة بينما يفشل الآخر كليًا
ما الذي يفعله معامل Predictor في PDF فعليًا؟
يخبر إدخال /Predictor في /DecodeParms قارئًا متوافقًا أي عملية عكس يشغّل، ويعرّف الجدول 8 من ISO 32000-1 القيم التي تهم عمليًا: 1 تعني عدم تطبيق تنبؤ، و2 تختار TIFF Predictor 2 (مفاضلة أفقية)، وأي قيمة من 10 إلى 15 تختار تنبؤًا بأسلوب PNG. تسافر ثلاثة مفاتيح إضافية معها — /Colors، و/BitsPerComponent، و/Columns — وتصف معًا هندسة الصف التي حُسبت المفاضلة مقابلها، حتى عندما لا يحمل التدفق أي بيانات صورة على الإطلاق: تدفق الكائن ليس صورة، لكن كاتبي PDF يعيدون استخدام آلية المُتنبئ نفسها المبنية على الصفوف له لأن الفرق-ثم-الضغط يضغط الأعداد الصحيحة المُكدَّسة بإحكام وإزاحات الكائنات بشكل أفضل من ضغطها خامة
TIFF Predictor 2 هو الأبسط من المخططين: يُخزَّن كل مكوّن كفرق عن المكوّن نفسه في البكسل السابق على الصف نفسه، ويعيد كل صف ضبط حافته اليسرى بدلًا من حمل فرق من الصف أعلاه. تنبؤ PNG أكثر دقة، لأن المُرشّح الفعلي يمكن أن يتغير من صف إلى صف: يبدأ كل صف ببايت وسم واحد — 0 لـNone، و1 لـSub، و2 لـUp، و3 لـAverage، و4 لـPaeth — وذلك الوسم، لا قيمة /Predictor المُعلَنة، هو ما يقرر كيفية إعادة بناء ذلك الصف تحديدًا. /Predictor بقيمة 12 هو في الحقيقة فقط تلميح المُرمّز بأنه فضّل مُرشِّح Up، حيث يُستعاد كل بايت بإضافة البايت مباشرة أعلاه في الصف السابق، لكن مُفكِّك ترميز صحيح لا يزال يتعين عليه قراءة الوسم في كل صف بدلًا من افتراض Up طوال الوقت
لماذا تجعل تدفقات الكائنات Predictor مفقودًا غير مرئي؟
تُضاعف تدفقات الكائنات المشكلة بدلًا من مجرد تكرارها. يسمح ISO 32000-1 §7.5.7 لكاتب PDF 1.5+ بحزم عدة كائنات غير مباشرة في حاوية مضغوطة واحدة، /ObjStm، ومن الشائع أن تسافر بالضبط الكائنات التي يحتاجها مُتحقق أكثر — الدليل، و/OutputIntents، أو تدفق /Metadata بـXMP — عبر تلك الحاوية بـ/Predictor 12 مرفقة، لأن تلك الكائنات قصيرة ومتكررة بما يكفي للاستفادة من مفاضلة الصفوف. عندما تكون خطوة المُتنبئ مفقودة، لا يُطلق توسيع تدفق الكائن خطأً: بل يُنتج تسلسل بايتات يبدو معقولًا سطحيًا لكنه لا يتجزّأ إلى الرموز المتوقعة، بحيث لا يظهر أيًّا كان ما حُزم بداخله ببساطة. نادرًا ما يلاحظ الرسم ذلك، لأن محرك رسم متوافق يعيد بناء البيانات المفاضلة بواسطة المُتنبئ بالفعل قبل أن تصل إلى التخطيط أصلًا؛ الشيفرة التي تلاحظ هي بالضبط النوع الذي أخفى هذا الخلل نفسه بداخله — مُتحقق، أو موقّع، أو فاحص إصدار يجتاز بايتات PDF الخام نفسها للإجابة عن سؤال بنيوي، دون احتياطي بمجرد أن تعود رؤيته الخاصة لتدفق الكائن خاطئة
اصطدم PDFiumPas بهذا الفشل بالضبط قبل v2.16.0. تدفقات كائنات مبنية بـ/Predictor 12، الحالة الشائعة لكاتبي PDF 1.5+، توسعت عبر PdfExpandObjectStreams إلى بايتات مفاضلة لم يستطع الفاحص البنيوي تحليلها، بحيث كانت كائنات الدليل و/OutputIntents و/Metadata المحزومة بداخله غير مرئية فعليًا لفحوصات التوافق — لا استثناء، لا تحذير، مجرد فحص تصرف بصمت كما لو كانت تلك الكائنات غائبة. الميكانيكا الأعمق لكيفية حل PDFiumPas تدفق كائن مقابل جدول المرجع المتقاطع النشط، بما في ذلك الحالات الهجينة وتدفقات xref الخالصة، مشروحة بشكل منفصل في مقالة التحقق من تدفقات الكائنات وxref عبر PDFiumPas؛ خطوة المُتنبئ الموصوفة هنا تعمل بعد ذلك الحل، على البايتات التي يحتويها كل كائن مضغوط فعليًا
عكس صفوف PNG وTIFF Predictor بلغة Pascal
يعكس PDFiumPas المفاضلة في روتين واحد، PdfApplyPredictor، وحسابات هندسته تستحق المعرفة سواء استدعيته أو أعدت تنفيذ الفكرة في شيفرة Delphi الخاصة بك. عرض الصف بالبايتات هو ceil(Columns × Colors × BitsPerComponent ÷ 8) وعرض البايت لكل بكسل الذي تستخدمه كلتا الخوارزميتين هو ceil(Colors × BitsPerComponent ÷ 8) — أخطئ في أي من التقريبين وستقرأ إعادة البناء عبر حد صف بدلًا من داخل واحد. /Predictor أقل من 2 يُترك دون مساس، بما أن 1 تعني أن المُرمّز لم يطبّق أي تحويل على الإطلاق؛ و2 تختار فرع TIFF المُبيَّن أدناه، وأي شيء من 10 فصاعدًا يسقط إلى إعادة بناء مُرشِّح صف PNG، حيث يقرر بايت الوسم في بداية كل صف — لا قيمة /Predictor المُعلَنة — كيفية التراجع عن ذلك الصف تحديدًا
function PdfApplyPredictor(const Src: TBytes;
Predictor, Colors, Bpc, Columns: Integer): TBytes;
var
RowLen, Bpp, R, I: Integer;
begin
Result:= Src;
if Predictor< 2 then
Exit; // 1 = no prediction, nothing to undo
if Colors<= 0 then Colors:= 1;
if Bpc<= 0 then Bpc:= 8;
if Columns<= 0 then Columns:= 1;
if (Colors> 64)or (Bpc> 32)or (Columns> 1 shl 24) then
Exit; // reject hostile row geometries
RowLen:= (Columns* Colors* Bpc+ 7) div 8; // ceil(), per ISO 32000-1 Table 8
Bpp:= (Colors* Bpc+ 7) div 8;
if Predictor= 2 then
begin
if Bpc<> 8 then
Exit; // only the 8-bit layout is reconstructed
Result:= Copy(Src, 0, Length(Src));
R:= 0;
while R+ RowLen<= Length(Result) do
begin
for I:= R+ Bpp to R+ RowLen- 1 do
Result[I]:= Byte(Result[I]+ Result[I- Bpp]);
Inc(R, RowLen);
end;
Exit;
end;
// Predictor >= 10 falls through to PNG row-filter reconstruction below
end;
// Continues inside PdfApplyPredictor once Predictor>= 10 (PNG row filters).
// Rows:= Length(Src) div (RowLen+ 1); each row is a 1-byte filter tag
// followed by RowLen data bytes, decoded left to right.
for R:= 0 to Rows- 1 do
begin
SrcOfs:= R* (RowLen+ 1);
DstOfs:= R* RowLen;
Tag:= Src[SrcOfs];
Inc(SrcOfs);
for I:= 0 to RowLen- 1 do
begin
if I>= Bpp then A:= Result[DstOfs+ I- Bpp] else A:= 0; // byte to the left
if R> 0 then B:= Result[DstOfs+ I- RowLen] else B:= 0; // byte above
case Tag of
1: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ A); // Sub
2: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ B); // Up
3: Result[DstOfs+ I]:= Byte(Src[SrcOfs+ I]+ (A+ B) div 2); // Average
// Paeth (tag 4) adds whichever of A, B or the byte above-left sits
// closest to the linear predictor A+ B- C; tag 0 (None) copies the
// filtered byte through unchanged
else Result[DstOfs+ I]:= Src[SrcOfs+ I];
end;
end;
end;
ما الذي غيّره PDFiumPas في v2.16.0
يجلس الإصلاح الذي شُحن في PDFiumPas v2.16.0 داخل PdfReadAndDecodeStream، الروتين الذي يقرأ بايتات تدفق الخام ويفكّها لكل مستدعٍ يحتاج فحص بنية PDF على مستوى البايت، بما في ذلك توسيع تدفق الكائنات؛ فهو لا يحاول إعادة البناء إلا بعد تأكيد أن /Filter هي FlateDecode خالصة، أبدًا سلسلة متتالية، لأنه لا يمكن تصحيح سلسلة مرشحات بأمان بواسطة مُتنبئ في هذه الطبقة. قراءة /Predictor و/Colors و/BitsPerComponent و/Columns مرة أخرى من قاموس التدفق لا تحتاج أيضًا مُحلِّل قاموس عامًا: تجد PdfDictRefNum كل مفتاح ببحث رمز اسم مباشر داخل نطاق بايتات ذلك القاموس الواحد، وهو أمر آمن هنا تحديدًا لأن تلك المفاتيح الأربعة لا يمكنها التكرار أو التداخل داخل قاموس تدفق واحد. بحث رمز الاسم نفسه أخطر بكثير بمجرد أن يُوجَّه إلى منطقة أكبر أو أقل حدودًا من ملف PDF، وهو موضوع المقالة المرافقة حول تحليل قواميس PDF بأمان
// Inside PdfReadAndDecodeStream, right after PdfInflate() has already run:
if PdfFilterIsPureFlate(DictTxt) then
begin
Inflated:= PdfInflate(Raw);
Predictor:= PdfDictRefNum(Data, DS, DE, 'Predictor');
if Predictor>= 2 then
begin
PColors:= PdfDictRefNum(Data, DS, DE, 'Colors');
PBpc:= PdfDictRefNum(Data, DS, DE, 'BitsPerComponent');
PColumns:= PdfDictRefNum(Data, DS, DE, 'Columns');
Result:= PdfApplyPredictor(Inflated, Predictor, PColors, PBpc, PColumns);
end
else
Result:= Inflated;
end;
قبل v2.16.0، كان تدفق كائن مبني بـ/Predictor 12 يتوسع إلى بايتات مفاضلة دون إطلاق خطأ، بحيث كان أي كائن دليل، أو /OutputIntents، أو /Metadata محزوم بداخله يختفي من فحوصات PDFiumPas البنيوية دون أي تحذير. بعد الإصلاح، يتضخم تدفق الكائن نفسه ثم يُعاد بناؤه بشكل صحيح، وتصبح الكائنات المحزومة بداخله مرئية مرة أخرى لتلك الفحوصات. سافرت حدود احترازية مع الإصلاح: ترفض PdfApplyPredictor الآن /Colors فوق 64، و/BitsPerComponent فوق 32، و/Columns فوق 2^24 كليًا، لأن تلك التوليفات تصف هندسات صفوف لا يحتاجها أي منتج PDF حقيقي وتوجد بشكل أساسي لجعل مُفكِّك الترميز يخصص ذاكرة أكثر بكثير مما تبرره بايتات المُدخل
var
Pdf: TPdf;
Report: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'incoming.pdf';
Pdf.Active := True;
Report := Pdf.ValidatePdfA;
if not Report.IsCompliant then
LogNonCompliance(Report); // caller-supplied handler
finally
Pdf.Free;
end;
end;
حدود تستحق المعرفة
إعادة بناء المُتنبئ في PDFiumPas لها حدّان يستحقان المعرفة قبل الاعتماد عليها. تغطي إعادة بناء TIFF Predictor 2 حالة 8 بت لكل مكوّن فقط؛ يسمح PDF بتعبئات أضيق، لكن بيانات TIFF مفاضلة أقل من بايت تمر دون إعادة بناء بدلًا من التخمين بها، بحيث لن يُفكّ تدفق يُعلن /Predictor 2 مع /BitsPerComponent 1 أو 2 أو 4 بشكل صحيح عبر هذا المسار اليوم. لا يملك تنبؤ PNG قيدًا كهذا — يوفر كل صف وسم مُرشِّحه الخاص، وتُعاد بناء الأنواع الخمسة المُعرَّفة كلها بصرف النظر عمّا تكون عليه قيمة /Predictor المُعلَنة بين 10 و15، وهو ما يطابق كيفية عمل الترشيح بأسلوب PNG فعليًا: القيمة المُعلَنة أقرب إلى تلميح حول ما استخدمه المُرمّز في الغالب من وعد بشأن كل صف
يعيد محرك PDFium الأصلي للرسم بناء بيانات الصور وتدفق المحتوى المفاضلة بواسطة المُتنبئ بشكل صحيح بالفعل، وهذا بالضبط سبب إمكانية أن يُرسَم ملف بشكل مثالي في أي عارض عادي بينما يقرأ مُتحقق، أو موقّع، أو فاحص إصدار على مستوى البايت مبني فوقه البايتات نفسها بشكل خاطئ. فك الترميز الواعي بالمُتنبئ الموصوف هنا يدعم ميزات التحقق من PDF/A، والفحص البنيوي، والتوقيع في PDFiumPas، مكوّن PDFium الأصلي القائم على VCL لـDelphi وC++Builder