يفكك PDFlibPas في إصداره 3.539.22 جداول Huffman المخصصة في JBIG2 محلياً: فالفاكّ بلغة Pascal الخالصة في PDFlibJBIG2.pas يحلل قطعة Tables (من النوع 53)، ويعيّن أكواد البادئة القياسية بترتيب أسطر الجدول كما يشترط ITU-T T.88 ملحق B.3، ويستهلك مراجع الجداول المخصصة بترتيب المحددات لقواميس الرموز ومناطق النص، ويقيّد كل قراءة بطول القطعة المعلن لا بالبايتات التي تصادف أن تلحقه
الملف الذي فرض هذا العمل لم يكن لافتاً على السطح. عقد ممسوح ضوئياً، مضغوط بـ JBIG2 بترميز Huffman رمزي لا بالترميز الحسابي الأكثر شيوعاً بكثير، ومعه مشفّر يشحن جداول أكواده الخاصة بدل الجداول القياسية B.1 حتى B.15. اختلف فاكّان مستقلان على بكسلات التنقيح فيه، وفاكّ PDFlibPas آنذاك أنتج نصاً يبدو وكأنه مر بمفرّم أوراق: شظايا محارف منزاحة ببضعة بكسلات، وعمود واحد مفقود من كل محرف. ولم يرفع شيء أي خطأ. تلك هي صورة العيب الذي ينجو لسنوات، لأن فاكّاً يرفض ملفاً يحصل على تذكرة دعم، بينما فاكّ يعرضه خطأ قليلاً يحصل على عمود يفترض أن المسح كان رديئاً
ماذا تحتوي قطعة JBIG2 Tables فعلاً؟
قطعة Tables هي وصف مضغوط لجدول Huffman واحد: بايت أعلام واحد، وحدّان من نوع 32 بتاً بإشارة، ثم تتابع من أزواج (طول البادئة، طول المدى) تقسّم الفترة بين الحدّين، كما هو مبسوط في T.88 §7.4.13 وملحق B.2. البت 0 من بايت الأعلام هو HTOOB ويقول إن للجدول رمزاً خارج النطاق. البتات 1 إلى 3 زائد واحد تعطي HTPS، عدد البتات المستخدم لكتابة كل طول بادئة؛ والبتات 4 إلى 6 زائد واحد تعطي HTRS، عرض حقل كل طول مدى. البت 7 محجوز، ويرفض PDFlibPas القطعة إن ضُبط بدل أن يخمّن ما قصدته مراجعة قادمة منه. يلحق HTLOW و HTHIGH كعددين من 32 بتاً بإشارة، وهما أول موضع يمكن أن يخطئ فيه فاكّ: قراءتهما بلا إشارة تجعل جدولاً حدّه الأدنى سالب، وهو أمر عادي تماماً في عروض الرموز المشفرة بالفروق، يبدو وكأنه يبدأ عند أربعة مليارات. وكل حقل يمر عبر مساعد ReadField محلي يفحص الطلب مقابل موضع البت الذي تنتهي عنده بيانات القطعة قبل لمس القارئ، لأن جدولاً يقرأ بعد قطعته سيكون يستهلك ترويسة القطعة التالية كأطوال بادئات
// TCodeTableSegment.readSegment، PDFlibJBIG2.pas
EndBit := (Int64(decoder.reader.bytePointer) +
segmentHeader.getSegmentDataLength) * 8;
Flags := ReadField(8);
if (Flags and $80) <> 0 then
raise EJBIG2DecodeError.Create('reserved custom Huffman table flag');
PrefixBits := ((Flags shr 1) and 7) + 1; // HTPS
RangeBits := ((Flags shr 4) and 7) + 1; // HTRS
LowValue := Integer(ReadField(32)); // HTLOW بعدد بإشارة
HighValue := Integer(ReadField(32)); // HTHIGH بعدد بإشارة
if LowValue >= HighValue then
raise EJBIG2DecodeError.Create('invalid custom Huffman range bounds');
CurrentValue := LowValue;
while CurrentValue < HighValue do
begin
PrefixLength := ReadField(PrefixBits);
RangeLength := ReadField(RangeBits);
if RangeLength > 32 then
raise EJBIG2DecodeError.Create('invalid custom Huffman range length');
AddLine(CurrentValue, PrefixLength, RangeLength);
Inc(CurrentValue, Int64(1) shl RangeLength);
end;
AddLine(LowValue - 1, ReadField(PrefixBits), jbig2HuffmanLOW);
AddLine(HighValue, ReadField(PrefixBits), 32);
if (Flags and 1) <> 0 then
AddLine(0, ReadField(PrefixBits), jbig2HuffmanOOB);
السطران الملحقان بعد الحلقة هما أسطر الهروب من ملحق B.2: سطر المدى الأدنى يبدأ عند HTLOW ناقص واحد ويعدّ تنازلياً، وسطر المدى الأعلى يبدأ عند HTHIGH بمدى ثابت من 32 بتاً، وسطر OOB الاختياري لا يحمل قيمة أصلاً. تميّزهم PDFlibPas بطولَي المدى الحارسين jbig2HuffmanLOW ($FFFFFFFD) و jbig2HuffmanOOB ($FFFFFFFE)، الاتفاقية نفسها التي تستخدمها جداوله القياسية المدمجة الخمسة عشر، فلا يهم حلقة فك الترميز أتت الجدول من المواصفة أم من الملف
لماذا يجب تعيين أكواد البادئة بترتيب أسطر الجدول؟
لأن المشفّر لا يكتب الأكواد أبداً. قطعة JBIG2 Tables تحمل أطوال البادئات فقط، ويبني الطرفان أنماط البت الفعلية بالإجراء القياسي في ملحق B.3: عُدّ كم سطراً لكل طول، وعيّن أكواد الطول واحد أولاً، ثم انقل يساراً وتابع، وداخل طول واحد وزّع الأكواد بترتيب ظهور الأسطر. أي انحراف عن ذلك الترتيب ينتج جدولاً مختلفاً بصمت. لن ينتبه الفاكّ، لأن كل نمط بت يولده ما زال كود بادئة صالحاً وإن لم يكن الذي استخدمه المشفّر، والمخرج صورة نقطية تبدو مقنعة مركبة من الرموز الخطأ
// THuffmanDecoder.buildTable، PDFlibJBIG2.pas
FillChar(Counts, SizeOf(Counts), 0);
for I := 0 to length - 1 do
begin
if table[I].prefixLen > 32 then
raise EJBIG2DecodeError.Create(
'Huffman prefixes longer than 32 bits are not supported');
Inc(Counts[table[I].prefixLen]);
end;
Active := 0;
Code := 0;
for Bits := 1 to 32 do
begin
Starts[Bits] := Active;
Positions[Bits] := Active;
Inc(Active, Counts[Bits]);
if Code + UInt64(Counts[Bits]) > (UInt64(1) shl Bits) then
raise EJBIG2DecodeError.Create('oversubscribed Huffman prefix codes');
Code := (Code + UInt64(Counts[Bits])) shl 1;
end;
SetLength(Result, Active + 1);
for I := 0 to length - 1 do // مستقر: يُحفظ الترتيب الأصلي
if table[I].prefixLen > 0 then // داخل كل طول بادئة
begin
Result[Positions[table[I].prefixLen]] := table[I];
Inc(Positions[table[I].prefixLen]);
end;
Code := 0;
for Bits := 1 to 32 do
begin
for I := Starts[Bits] to Positions[Bits] - 1 do
begin
Result[I].prefix := Cardinal(Code);
Inc(Code);
end;
Code := Code shl 1;
end;
Result[Active].rangeLen := jbig2HuffmanEOT;
THuffmanDecoder.buildTable فرز عدّ لا فرز مقارنة لسبب واحد: جولة العدّ على Counts و Starts و Positions مستقرة بالبناء، فتسقط أسطر طول البادئة المتساوي في النتيجة بترتيب تعريفها، وهو بالضبط الترتيب الذي يعيّن به ملحق B.3 الأكواد. تسقط الأسطر بطول بادئة صفر قبل تعيين الأكواد، لأن B.3 يعرفها كغير مستخدمة لا كأكواد من بت واحد. حارسان يجلسان في نفس الحلقة. فحص الإفراط في الاشتراك يلتقط جدولاً تطالب أطواله بعدد أكواد أكثر مما يستطيع كود بادئة بذلك العمق حمله، وهو متباينة Kraft معبراً عنها بمقارنة أعداد صحيحة؛ ومن دونه ينتج جدول عدائي كوداً يطابق سطرين ويختار الفاكّ أيهما يمسحه أولاً. وسقف الـ 32 بت موجود لأن prefix هو Cardinal والمطابِق في decodeInt يراكم البتات في واحد منها. يسمح T.88 على الورق ببادئات أطول، يرفضها PDFlibPas بالاسم، ولم يُشاهد مشفّر حقيقي واحد يصدر واحدة. وحساب القيم يحتاج العناية نفسها التي يحتاجها حساب الأكواد: THuffmanTable.val من نوع Int64، وسطر المدى الأدنى يُفكك كـ val - readBits(32)، إزاحة من 32 بتاً بلا إشارة تُطرح من HTLOW ناقص واحد. بوسطات Integer يلتف ذلك الطرح، ثم يُقبل القيمة الملتفة كعرض رمز. مسار الـ 64 بت يحسب القيمة الحقيقية ويفحصها مقابل مدى الـ 32 بتاً بإشارة ويرفع إن لم تتسع، فيتحول التلف الصامت إلى رفض صريح
لماذا لم تنشط الجداول المخصصة قبل 3.539.22 إطلاقاً؟
عيبان كانا يخفيان أحدهما الآخر. الأول عيب مُعيِّن من سطر واحد: كان TTextRegionHuffmanFlags.setFlags يستقبل وسيطه بالاسم نفسه الذي خزّن فيه، فكان Self.flagsAsInt := flagsAsInt يعيّن الحقل غير المهيأ لنفسه فتقرأ كل المحددات صفراً، وهو ما أرسل مناطق النص الطالبة للجداول المخصصة عبر الجداول القياسية F و H و K. والعيب الثاني كان يعني أن إصلاح الأول وحده ما زال سيُنتج رموزاً تالفة. عندما يخزن قاموس رموز Huffman رموزه كصورة نقطية جماعية غير مضغوطة يكون آخر بايت في كل صف جزئياً، وكانت حلقة النسخ القديمة تعامل padding، الذي يحمل عدد البتات الصالحة، كأنه موضع أدنى بت صالح؛ فصف عرضه 63 بكسلاً كان ينسخ بتاً واحداً من بايته الأخير بدل سبعة. الحلقة المصححة تشغّل for bitPointer := 7 downto ((8 - padding) and 7)، وتثبت عينات اصطناعية بعرض 7 بتات و9 بتات جانبَي حدّ البايت معاً. وبقراءة المحددات صحيحة توزع الجداول بالترتيب الذي تسرده المواصفة، الذي يثبته T.88 §7.4.3.1.2 لمناطق النص بوصفه FS و DS و DT و RDW و RDH و RDX و RDY و RSIZE، ويثبته §7.4.2.1.1 لقواميس الرموز بوصفه DH و DW و BMSIZE و AGGINST. كل محدد من بتين يعني الجدول القياسي 0 أو 1، ومحجوزاً للقيمة 2 في الحقول ذات الجدولين القياسيين فقط، ومخصصاً للقيمة 3، وكل اختيار مخصص يستهلك قطعة Tables التالية بين القطع المشار إليها بترتيب الإحالة. NextCustomHuffmanTable تنفذ ذلك التجول بالضبط وترفع missing custom Huffman table reference عندما تشير منطقة إلى جداول أقل مما تطالبه محدداتها. وسطر إضافي واحد يعود إلى نفس الإصلاح: قاموس رموز Huffman مجموع رموزه المدخلة والجديدة يساوي واحداً يحسب طول كود الرمز صفراً من صيغة log2، بينما تكتب النسخة Huffman من الصيغة كل معرف رمز على الأقل ببت واحد، فـ if sdHuffman and (symbolCodeLength = 0) then symbolCodeLength := 1 في TSymbolDictionarySegment يمنع مسار التنقيح والتجميع من قراءة صفر بت لكل معرف رمز
ماذا يضمن حدّ القطعة؟
يعامل PDFlibPas طول بيانات القطعة في كل ترويسة كعقد يلتزم به الاتجاهان: لا يجوز لقطعة أن تقرأ بعد نهايتها المعلنة، ولا أن تنتهي قبلها وتترك الترويسة التالية عند إزاحة غير متوقعة. القواعد التي تنبثق من ذلك العقد صغيرة كل على حدة. طول بيانات مضبوط البت 31 فيه هو علامة الطول المجهول في T.88 §7.2.7، و handleSegmentDataLength يربطها بقيمة سالبة يرفضها readSegments رفضاً قاطعاً بدل التماس ماسحاً أمامها عن موهِم نهاية. وكل رقم قطعة مُشار إليه يجب أن يكون أصغر من رقم القطعة الحالية وأن يكون موجوداً أصلاً، فتفشل الإحالة الأمامية أو المعلقة قبل أن تحاول أي منطقة حلّها. ويجب أن تعلن END_OF_PAGE و END_OF_FILE صفر بايت من البيانات. قطعة Profiles (النوع 52) تحمل عدّاً من 32 بتاً يلحق به تلك العدد من المعرفات ذات الـ 32 بتاً وبلا بكسلات إطلاقاً، فيُفحص مجموعها كـ 4 زائد 4 مرات العدّ مقابل الطول المعلن، وتُتخطى، وتُبقى في قائمة القطع حتى تستطيع القطع اللاحقة الإحالة إليها برقمها. معرف ملف جانبي مجهول ليس ترميزاً مجهولاً، ومعاملته كذلك سترفض ملفات تفكك تمام الفهم
// TJBIG2StreamDecoder.readSegments، PDFlibJBIG2.pas
DataLength := segmentHeader.getSegmentDataLength;
if DataLength < 0 then
raise EJBIG2DecodeError.Create(Context +
'unknown or oversized segment length is not supported');
if DataLength > Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create(Context + 'truncated segment data');
DataEnd := reader.bytePointer + DataLength;
for I := 0 to noOfReferredToSegments - 1 do
if (referredToSegments[I] >= segmentHeader.getSegmentNumber) or
(findSegment(referredToSegments[I]) = nil) then
raise EJBIG2DecodeError.Create(Context + 'invalid segment reference');
// ... أنشئ كائن القطعة الخاص بهذا النوع ...
reader.SegmentEnd := DataEnd;
segment.readSegment;
if reader.bytePointer > DataEnd then
raise EJBIG2DecodeError.Create(Context +
'decoded data exceeds declared segment length');
if reader.bytePointer < DataEnd then
begin
reader.bytePointer := DataEnd; // قد يترك MMR رمز EOFB دون قراءة
reader.bitPointer := 7;
end;
ذيل تلك الحلقة هو حيث أخطأ إصدار سابق من الفاكّ مع المناطق المشفرة بـ MMR. فاكّ MMR يعرف أنه انتهى عندما يُنتج آخر بكسل من آخر صف، وهو ما يمكن أن يحدث قبل أن يستهلك موهِم EOFB الذي يضع T.88 §6.2.5.7 في نهاية البيانات. كان الكود القديم يفترض أن القارئ موضوع عند الترويسة التالية، فحُللت بايتات الموهِم المتبقية كرقم قطعة وفشل التدفق بعد بايتات قليلة بخطأ مضلل. الآن النهاية المعلنة هي الغالبة: القراءة بعدها خطأ، والتوقف قبلها أمر عادي، ويُنقل القارئ إلى DataEnd مع تصفير مؤشر البت حتى تُقرأ الترويسة التالية من حيث قال الملف إنها ستكون. وينسجم ذلك الانضباط مع كل موضع يفكك فيه PDFlibPas بنيات PDF غير الموثوقة: الطول المعلن هو الحدّ، والفاكّ لا يبحث عن حدّ ألطف منه
من أين تقرأ تنقيح Huffman حجم صورته النقطية؟
قبل أن يبدأ الفاكّ الحسابي، ومن حقل لا يوجد إلا في وضع Huffman. عندما تحمل نسخة منطقة نص تنقيحاً (RI غير صفري) ومؤشر SBHUFF مضبوطاً، يقرأ T.88 §6.4.11 لدى الفاكّ RDW و RDH و RDX و RDY بجداولهما المختارة، ثم BMSIZE بجدول RSIZE، ثم يصطف على حدّ بايت، وفقط حينها يشغّل فك التنقيح العام على BMSIZE بايتات بالضبط. مناطق النص بوضع الحسابي بلا هذا الحقل، والفاكّ الذي يتشارك مسار كود واحد للوضعين سيتخطاه، ويشغّل الفاكّ الحسابي مبكراً بايتين أو أكثر، وينقّح كل رمز مقابل خردة. مسار قاموس الرموز مع REFAGG ونسخة تنقيح واحدة، الموصوف في §6.5.8.2.2، يحمل حقل BMSIZE نفسه وبالعواقب نفسها. وفي PDFlibPas يكون الحد الأعلى لذلك الحجم TStreamReader.SegmentEnd، نهاية القطعة الحالية كما ضبطها readSegments، لا نهاية التدفق كله، لأن BMSIZE لا يُشبع إلا باستعارة بايتات من القطعة التالية فهو مشوّه، والتحقق منه مقابل طول التدفق سيسمح للفاكّ الحسابي بالقراءة داخل الترويسة التالية. والحد الأدنى من بايتين يعكس زوج البايتات الابتدائي الذي يستهلكه الفاكّ الحسابي دائماً، وبعد التنقيح يقفز القارئ إلى RefinementEnd أياً كان بعد ما قرأه الفاكّ الحسابي مسبقاً، لأن موضعه النهائي ليس موضع حقل Huffman التالي
// فك ترميز منطقة نص TJBIG2Bitmap، مسار تنقيح Huffman
RefinementSize := huffmanDecoder.decodeInt(huffmanRSizeTable).intResult;
huffmanDecoder.consumeRemainingBits;
if (RefinementSize < 2) or
(RefinementSize > huffmanDecoder.reader.SegmentEnd -
huffmanDecoder.reader.bytePointer) then
raise EJBIG2DecodeError.Create('invalid refinement bitmap size');
RefinementEnd := huffmanDecoder.reader.bytePointer + RefinementSize;
arithmeticDecoder.start;
// ... readGenericRefinementRegion ...
if huffmanDecoder.reader.bytePointer > RefinementEnd then
raise EJBIG2DecodeError.Create('refinement data exceeds declared size');
huffmanDecoder.reader.bytePointer := RefinementEnd;
huffmanDecoder.reader.bitPointer := 7;
ماذا تم التحقق منه، وماذا ما زال يُرفض؟
العينة التي بدأت هذا كله، صورة JBIG2 مقاس 500 في 473 بكسلاً بجداول مخصصة وتنقيح Huffman، تفكك الآن إلى صورة نقطية بفروق بكسل صفرة مقابل فاكّ مستقل، وتنتج عينتا الصورة النقطية الجماعية الاصطناعيتان بعرض 7 بتات و9 بتات الصفوف المتوقعة عليهما معاً. والفاكّان المستقلان اللذان اختلفا على العينة الأصلية ما زالا يختلفان مع بعضهما؛ يطابق PDFlibPas أحدهما، والبيان الصادق أن المخرج المحلي يوافق تنفيذاً مستقلاً واحداً والمواصفة كما قُرئت، لا أن كل فاكّ في العالم يوافق. والوجه المشوّه من الحزمة يغطي:
- بت علم محجوز أو قيمة محدد محجوزة
- جدولاً مقطوعاً في منتصف سطر
- أطوال بادئات مفرطة الاشتراك وبادئات أطول من 32 بتاً
- منطقة تطلب محدداتها جداول مخصصة أكثر مما تشير إليه
- تأكيداً على أن المخرج القديم يُمسح بعد فشل فك الترميز لا يُترك مكانه ليخطئ به المستدعي ظانّاً أنه نتيجة
ثلاثة حدود ما زالت مقصودة. تنظيم التدفق بالوصول العشوائي، حيث تسبق كل ترويسات القطع كل بياناتها، يرفع JBIG2 random-access organisation is not supported بمجرد قراءة أعلام ترويسة الملف، لأنه لا توجد عينة نموذجية للتحقق منه والمسار المُنفّذ نصفه أسوأ من رفض مسمى. والجداول المخصصة مسقوفة عند 65,536 سطراً وبادئات من 32 بتاً. والمدخل العام لفك الترميز TPLJBIG2Decoder.LoadFromByteArray يعيد صورة الصفحة الأولى بترتيب التدفق عبر getPageAsJBIG2Bitmap(0)، أي أول قطعة معلومات صفحة تُقابل، بدل البحث عن ارتباط صفحة صفري؛ فتدفقات PDF المضمنة تعادة ترقيم صفحتها الواحدة بـ 1 في المعتاد، وطلب الصفحة 0 بالارتباط لن يجد شيئاً. ونص الفشل يهبط في TPLJBIG2Decoder.LastError، تشخيص الفاكّ الداخلي الذي يحمل رقم القطعة ونوعها وإزاحة بايت العطل، وهو ليس الشيء نفسه الذي يفعله TPDFlib.LastErrorCode على مستوى المكتبة. لا شيء من هذا يلمس جهة الترميز، وهي مغطاة في ملاحظات خلفيات مشفّر JBIG2 وكيفية ربطها؛ فعلى مسار القراءة أن يقبل ما قرر مشفّر شخص آخر إصداره، وهو يتشارك قواعده مع بقية رصة الصور، بما فيها فاكّ TIFF المدمج ورفضه لـ BigTIFF والتخطيط المبلط: ارفض بالاسم، ولا تستعر بايتات عبر حدّ معلن أبداً، وأبقِ الحساب واسعاً حتى لا تعتذر وسيطة ملتفة عن جواب صالح. وإذا كنت تقيّم مسار قراءة JBIG2 محلياً لـ Delphi أو C++Builder، فالفاكّ وبقية معالجة الصور موثقة على صفحة PDF Library for Delphi