قيمة /Predictor من 10 إلى 14 في قاموس /DecodeParms داخل PDF لا تخبر الفاكّ بأي مرشح PNG يطبقه. تعامل losLab PDF Library، منذ v3.539.21، كل قيمة من 10 إلى 15 بالمثل في ApplyInflatePredictor: كل صف من البيانات المنفوخة يبدأ ببايت نوع مرشح واحد، وذلك البايت، لا القاموس، هو من يختار None أو Sub أو Up أو Average أو Paeth لذلك الصف. التمييز بين 10 و 11 و 12 و 13 و 14 و 15 نصيحة للمشفّر عن المرشح الذي يكتبه؛ وفي جهة فك الترميز هي بلا معنى، والفاكّ الذي يستجيب لها ينتج مخرجاً منزاحاً بايتاً واحداً لكل صف. يبدو ذلك حاشية هامشية في ISO 32000-1 الجدول 8 حتى يصل ملف حقيقي كُتب تدفقه المرجعي المتقاطع بـ /Predictor 12. عندها يفكك جدول الكائنات كله خردة تبدو مقنعة، ولا يمكن العثور على جذر المستند، وبلّغ المحمّل العام خطأً يشير إلى نظام الملفات لا إلى وسيط مرشح. ثلاثة أمور تحتاج فك تشابكها: ماذا تعِد المواصفة فعلاً، ولماذا اتفق التنفيذ القديم واختبارات انحداره مع بعضهما ومع لا شيء غيرهما، وماذا تفعل حلقة الصف المصححة في PDFlibZLib.pas الآن لتدفقات Flate و LZW معاً
لماذا لا تعني /Predictor 12 مرشح Up على كل صف؟
لأن ISO 32000-1 §7.4.4.4 الجدول 8 يعرّف القيم 10 حتى 15 بوصفها عائلة منبئات PNG، وصيغة PNG نفسها، ISO/IEC 15948 §9، تعرّف الترشيح كقرار لكل سطر مسح يُسجَّل في بايت نوع مرشح في بداية كل سطر. يسرد الجدول 8 القيم الفردية بأوصاف مثل "تنبؤ PNG Sub على كل الصفوف" و"تنبؤ PNG الأمثل"، لكن تلك الأوصاف تخبر مشفّراً بما يكتبه. والجدول صريح في أن منبئات PNG تجيز مرشحاً مختلفاً لكل صف تحديداً لأن كل صف يحمل وسمه الخاص. ملف كُتب بـ /Predictor 12 من مشفّر حسن السلوك يحتوي إذن بايت 2 أمام كل صف، والفاكّ الذي يقرأ ذلك البايت يحصل على الجواب نفسه أقيل له 12 أو 15
والفَخّ أن الأوصاف تُقرأ كأنها مجموعة تعليمات. المنبئ 2 في الجدول نفسه هو فعلاً خوارزمية ثابتة، منبئ الفروق الأفقية TIFF، ولا يحمل أي وسم لكل صف إطلاقاً، فالمطور الذي ينفذ المنبئ 2 أولاً ثم يقرأ الجدول نزولاً يفترض طبيعياً أن 10 حتى 14 تعمل بالطريقة نفسها بصيغة مختلفة لكل واحدة. تلك الافتراضة هي ما شحنته إصدارات سابقة من losLab PDF Library: كانت حلقة الصف تعامل المنبئ 15 كالنسخة الموسومة الوحيدة، وتربط 10 حتى 14 بـ Parms.Predictor - 10 كمرشح ثابت. قراءة وسم الصف هي السلوك الصحيح الوحيد، وتلميحة جهة المشفّر من الأفضل تجاهلها كلياً، لأن منتجاً استخدم ترشيح PNG الأمثل وكتب /Predictor 11 في القاموس ليس مشوهاً حتى بحرف المعيار
كيف حوّلت فاتورة XFA وسيط Flate إلى الخطأ 401
ظهر الفشل في فاتورة XFA بمأذون من العميل، مسجلة في المدونة المحلية باسم xfa-invoice-1 وببصمة SHA-256 1b65f07d…46de1e، مستند من صفحة واحدة تدفقه المرجعي المتقاطع مضغوط بـ Flate مع /Predictor 12. بقراءة الخوارزمية الثابتة القديمة ساوت SourceRowLength مع TargetRowLength بدل TargetRowLength + 1، فاستهلكت الحلقة بايت نوع المرشح كأول بايت بيانات في كل صف وبدأت كل صف لاحق متأخرة بايتاً واحداً. فكان كل إدخال مرجعي بعد الأول منزاحاً، وقرأ حقل النوع من ما هو فعلاً بايت إزاحة، وأشارت الإزاحات المحلولة إلى منتصف كائنات لا صلة بها
ما رآه المستدعي هو LoadFromFile تعيد 0 و LastErrorCode يبلّغ 401، "تعذر فتح ملف الإدخال"، عن ملف كان مقروءاً وغير مشفر ويفتح حسناً في كل عارض. داخلياً السلسلة قصيرة: تدفق المرجع المتقاطع فُك، وحُللت الإدخالات إلى هراء، وحل مرجع /Root إلى كائن فارغ بدل كتالوج، وفشل بناء المستند، وهبط ربط نتيجة التحميل إلى رمزه الافتراضي. ولأن العينة نموذج XFA كان المشتبه الأول آلية XFA التي لا علاقة لها بالأمر؛ فالمستند لم يبتعد بما يكفي لينظر في قالب. الدرس الجدير بالحمل تشخيصي: عندما يفشل ملف حديث بتدفق مرجعي مضغوط عند التحميل برمز عام، فكك تدفق المرجع المتقاطع يدوياً قبل أن تنظر في أي مكان آخر، وافحص هل يسمي /DecodeParms فيه منبئاً
كيف تحدد Columns و Colors و BitsPerComponent حجم الصف
هندسة الصف تأتي كلياً من ثلاثة وسائط في الجدول 8، والخطأ في أي منها ينتج العَرَض نفسه الذي ينتجه الخطأ في الوسم. تحسب losLab PDF Library طول الصف المفكوك كـ ceil(Columns × Colors × BitsPerComponent / 8)، وهو TargetRowLength في ApplyInflatePredictor، وتضيف لعائلة PNG بايتاً واحداً لنيل SourceRowLength. القيم الافتراضية، التي يطبقها TPDFStream.GetDecodedStream عند غياب مفتاح، هي افتراضيات الجدول 8: Colors 1 و BitsPerComponent 8 و Columns 1 و Predictor 1. ولتدفق مرجعي متقاطع يضبط المنتج /Columns على مجموع عروض /W، فتزدوج /W [1 3 1] مع /Columns 5 ويكون كل صف مفكوك إدخالاً واحداً تماماً؛ وحساب /W نفسه مغطى في مقالة تدفقات الكائنات والتدفقات المرجعية المتقاطعة التي تتوقف عند النقطة التي يُسلَّم فيها حمول التدفق إلى المرشح. ومرشحات Sub و Average و Paeth تحتاج إضافةً إلى معرفة بُعد "البايت إلى اليسار"، وتعرّفه PNG بعدد البايتات لكل بكسل كامل مقرّباً إلى واحد على الأقل؛ تُشتقه losLab PDF Library كـ PixelSize := ceil(BitsPerComponent × Colors / 8)، فتتنبأ صورة RGB ذات 8 بتات بكل بايت مقابل البايت قبل ثلاثة مواضع، بينما تتنبأ صورة ذات بت واحد، وأي تدفق مرجعي متقاطع، مقابل البايت السابق مباشرة. أما المنبئ 2 فحيوان آخر يتولاه فرع منفصل: يعيد بناء كل عينة بإضافة العينة السابقة من مكوّن اللون نفسه، ويعمل على مستوى المكوّن عند 1 و 2 و 4 و 8 و 16 بتاً لكل مكوّن، ولا وسم صف له، فـ SourceRowLength عنه يساوي TargetRowLength تماماً. وتتشارك عائلتا المنبئين حساب طول الصف وحده
ماذا تفعل حلقة الصف المصححة في ApplyInflatePredictor
الفاكّ دالة واحدة، ApplyInflatePredictor(Const Input: AnsiString; Var Parms: TPDFFlateParms; MaxOutput: Integer = 0): AnsiString، و InflateStrParms هي ببساطة ApplyInflatePredictor(InflateStr(Input), Parms). السجل TPDFFlateParms يحمل Predictor و Colors و BitsPerComponent و Columns، زائد ExpandedTo8Bit و ColorSpace اللذين يستخدمهما فاكّ الصور بعد التنبؤ. أزال الإصلاح في v3.539.21 فرع الخوارزمية الثابتة كلياً؛ والمقتطف أدناه هو حلقة الصف كما هي الآن، مع حذف فرع TIFF
TargetRowLength:= Integer((BitsPerRow+ 7)div 8);
If Parms.Predictor>= 10 Then
SourceRowLength:= TargetRowLength+ 1 // عائلة PNG: بايت وسم لكل صف
Else If Parms.Predictor= 2 Then
SourceRowLength:= TargetRowLength // TIFF: بلا وسم
Else
Exit;
RowCount:= (SourceLength+ SourceRowLength- 1)div SourceRowLength;
SetLength(Result, RowCount* TargetRowLength);
SourcePos:= 1;
For Row:= 0 To RowCount- 1 Do
Begin
TargetPos:= Row* TargetRowLength+ 1;
If Parms.Predictor>= 10 Then
Begin
If SourcePos<= SourceLength Then
Predictor:= Byte(Source[SourcePos]) // 0..4 يختار مرشح الصف
Else
Predictor:= 0;
Inc(SourcePos);
If Predictor> 4 Then
Begin
Result:= ''; // وسم مجهول: ارفض التدفق
Exit;
End;
End
Else
Predictor:= 202; // علامة داخلية لـ TIFF
FillChar(Result[TargetPos], TargetRowLength, 0);
// ... انسخ حتى TargetRowLength بايت خام في Result[TargetPos] ...
If Row> 0 Then
PreviousRowPos:= TargetPos- TargetRowLength
Else
PreviousRowPos:= 0;
Case Predictor Of
1: // Sub: أضف البايت بكسلاً واحداً إلى اليسار
For Col:= PixelSize To TargetRowLength- 1 Do
Result[TargetPos+ Col]:= AnsiChar((Byte(Result[TargetPos+ Col])+
Byte(Result[TargetPos+ Col- PixelSize]))and $FF);
2: // Up: أضف البايت فوقه مباشرة، وصف أول بلا شيء فوقه
If Row> 0 Then
For Col:= 0 To TargetRowLength- 1 Do
Result[TargetPos+ Col]:= AnsiChar((Byte(Result[TargetPos+ Col])+
Byte(Result[PreviousRowPos+ Col]))and $FF);
3: // Average: (يسار + أعلى) div 2، والجيران المفقودون يقرؤون صفراً
...
4: // Paeth: PaethPredictor(يسار، أعلى، أعلى-يسار)
...
End;
End;
تتبع أذرع المرشحات الأربعة ISO/IEC 15948 §9 حرفياً، بما في ذلك قاعدة أن الجار الأيسر المفقود عند أول بكسل والصف الأعلى المفقود عند أول سطر مسح يقرآن صفراً معاً، وينفذ PaethPredictor(A, b, C: Byte): Byte ترتيب كسر التعادل الذي تشترطه مواصفة PNG: اليسار يغلب الأعلى، والأعلى يغلب أعلى-اليسار. وكل الحساب بالتناضح 256 على البايتات. لا شيء في هذا يعتمد على Parms.Predictor بعد أن تبدأ حلقة الصف، وهذه هي الجوهر كله: قيمة القاموس تفتح الباب لعائلة PNG وتحدد حجم الصف، والبيانات تتولى الأمر من هناك. أما تدفقات LZW فتمر بالكود نفسه: تنسخ TPDFLZWDecompressor.ApplyPredictor حقول الهندسة الأربعة من TPDFLZWParms إلى سجل TPDFFlateParms وتستدعي ApplyInflatePredictor بسقف مخرج فاكّ الضغط، فصحح إصلاح واحد المرشحين معاً وتغيّرت عينة انحدار LZW في نفس الإيداع. أما مخاطر LZW الخاصة، عرض الكود ووسيط /EarlyChange، فموضوع منفصل مغطى في مقالة LZW EarlyChange؛ ومعالجة المنبئ هابطة عند كل ذلك ومتطابقة للمرشحين
لماذا نجحت اختبارات الانحدار القديمة؟
نجحت لأن العينات بُنيت يدوياً لتطابق التنفيذ، لا بمشفّر قرأ المعيار. اختبار مسمّى باسم المنبئ 12 أطعم الفاكّ ثمانية بايتات خام، #1#2#3#4#10#10#10#10، مع Columns 4، وتوقع تطبيق مرشح Up على الصف الثاني. لا منتج حقيقي واحد يصدر ذلك التتابع تحت /Predictor 12؛ المنتج الحقيقي يصدر عشرة بايتات، #2#1#2#3#4#2#10#10#10#10، بوسم 2 يقدّم كل صف. الاختبار حبس سوء القراءة وأعطى كل إعادة هيكلة لاحقة ضوءاً أخضر. تحمل العينات المصححة في Tests.ZLib.pas و Tests.LZW.pas الآن بايت الوسم، وحالة مرشح None هي الأعبر تعليماً في المجموعة: المدخل #0#1#2#3#0#4#5#6 مع Columns 3 يفك إلى #1#2#3#4#5#6، ستة بايتات من ثمانية، لأن اثنين من بايتات المدخل لم يكونا بيانات قط. والحارس الأقوى اختبار مشترك في Tests/PNGPredictorSemantics.inc يجري على Delphi و FPC معاً: يبني تدفقاً حمولته المنفوخة 15 بايتاً، ثلاثة صفوف من Columns 4 زائد وسم لكل صف، بأوسمة 0 و 2 و 1 بهذا الترتيب، ثم يفك البايتات المطابقة ست مرات مع /Predictor مضبوطاً على كل قيمة من 10 حتى 15، ويجب أن تنتج كل جولة البايتات الاثني عشر نفسها. إن أعاد أحد يوماً إحياء فكرة أن قيمة القاموس تختار الخوارزمية، فجولة المنبئ 11 أو 13 ستنتج جواباً مختلفاً وسيرسب الاختبار برسالة تسمي الخطأ باسمه
procedure RunPNGPredictorSemantics;
const
// حمولة zlib تنفخ إلى: 0 1 2 3 4 | 2 10 10 10 10 | 1 5 5 5 5
Encoded: AnsiString = #$78#$9C#$63#$60#$64#$62#$66#$61#$E2#$02#$02#$46#$56#$20#$00#$00#$01#$FE#$00#$4A;
Expected: AnsiString = #1#2#3#4#11#12#13#14#5#10#15#20;
var
Structure: TPDFStructure;
Stream: TPDFStream;
Parms: TPDFDictionary;
Predictor: Integer;
begin
for Predictor:= 10 to 15 do
begin
Structure:= TPDFStructure.Create;
try
Parms:= Structure.NewDictionary;
Parms.Add('Predictor', Structure.NewNumeric(Predictor));
Parms.Add('Columns', Structure.NewNumeric(4));
Stream:= Structure.NewStream(Encoded, False);
Stream.Dictionary.Add('Filter', Structure.NewName('FlateDecode'));
Stream.Dictionary.Add('DecodeParms', Parms);
if Stream.GetDecodedStream<> Expected then
raise Exception.Create('PNG predictor ignored per-row algorithm tags');
finally
Structure.Free;
end;
end;
end;
الصف الأول وسمه 0 فيعبر كما هو: 1 2 3 4. والصف الثاني وسمه 2، فيصير 10 10 10 10 إلى 11 12 13 14 بإضافة الصف الأعلى. والصف الثالث وسمه 1، فيتراكم 5 5 5 5 من اليسار إلى اليمين في 5 10 15 20. عينة بثلاثة مرشحات مختلفة في ثلاثة صفوف هي الحد الأدنى الذي يميز "يقرأ الوسم" من "صادف أن خمّن صواباً"، والقاعدة العامة التي تفرضها جديرة بالتبني: أي عينة منبئ يجب أن ينتجها مشفّر حقيقي أو، على الأقل، أن تُركّب يدوياً وأوسمة صفوفها في موضعها. البيانات الاصطناعية التي تتخطى الوسم ليست تبسيطاً، إنها صيغة مختلفة
استدعاء الفاكّ مباشرة والحدود التي يفرضها
معظم المستدعين لا يلمسون ApplyInflatePredictor أبداً؛ فـ LoadFromFile وقارئ المرجع المتقاطع وفاكّ الصور و TPDFStream.GetDecodedStream كلها تصل إليه داخلياً. ومع ذلك يفيد معرفة كيفية قيادته حين يُسحب تدفق من ملف لفحص جنائي، والمقتطف أدناه يفك حمول تدفق مرجعي متقاطع كما يفعل المحمّل لقاموس بـ /W [1 3 1] /Columns 5 /Predictor 12
uses
PDFlibZLib;
function DecodeXRefPayload(const RawStreamBytes: AnsiString): AnsiString;
var
Parms: TPDFFlateParms;
begin
FillChar(Parms, SizeOf(Parms), 0);
Parms.Predictor:= 12; // أي قيمة 10..15 تتصرف بالمثل
Parms.Colors:= 1;
Parms.BitsPerComponent:= 8;
Parms.Columns:= 5; // مجموع /W [1 3 1]
Result:= InflateStrParms(RawStreamBytes, Parms);
// يحمل Result الآن N * 5 بايت: type(1) offset(3) gen(1) لكل إدخال.
// Result الفارغة تعني أن الحمولة رُفضت، لا أنها فارغة فحسب
end;
الحدود مقصودة وتستحق قولاً صريحاً. وسم صف أكبر من 4 يجهض فك الترميز كله ويعيد سلسلة فارغة بدل نتيجة جزئية، لأن تدفقاً يخالف مدى وسوم PNG أرجح بكثير أن يكون وسُطئ ضبط معاملاته من أن يحوي بايتاً واحداً رديئاً، والمتابعة بصمت كانت ستعيد إنتاج العيب الأصلي بزي مختلف. والصف الأخير المقطوع متسامَح معه: RowCount تقرّب صعوداً والبايتات المفقودة تقرأ صفراً والوسم المفقود يقرأ مرشح 0، وهو ما يجعل تدفقاً قُطع بخطأ /Length يمنح صفوفه السليمة. و Colors أقل من 1، و Columns أقل من 1، و BitsPerComponent خارج 1 و 2 و 4 و 8 و 16، أو Predictor خارج 2 و 10 حتى 15، كلها تُرفض قبل لمس أي صف، وعند تزويد سقف مخرج يعيد الفاكّ الفراغ عليها بدل تمرير المدخل كما هو. لا شيء من هذا يصلح تدفقاً مرجعياً متقاطعاً تالفاً؛ ملف يخالف فيه /Columns عروض /W ما زال يفكك إلى هراء، والتعافي من ذلك عمل الماسح الإزاحي الموصوف في مقالة تعافي xref التالفة لا عمل المنبئ. شُحنات معالجة وسوم الصفوف المصححة في losLab PDF Library v3.539.21 لـ Delphi و C++Builder و Free Pascal، وجرى التحقق منها مقابل ثماني مجموعات مدونة بمجموع 1472 صفحة على بناء FPC Win64 إلى جانب حزمة Delphi اللامرئية كاملة؛ وصفحة المنتج تسرد قدرات الفك وتركيبات المرشحات والمنبئات المدعومة