يفك الإصدار 3.539.23 من PDFlibPas ملفات JBIG2 المستقلة التي تستخدم تنظيم الوصول العشوائي من الملحق D.2 من ITU-T T.88، حيث تأتي ترويسات كل المقاطع أولاً وتتبعها بيانات المقاطع بالترتيب نفسه. يفهرس فاكّ Pascal الأصيل في PDFlibJBIG2.pas إزاحات الترويسات حتى ترويسة نهاية الملف الإلزامية، ويفحص أن أرقام المقاطع تتزايد وأن أطوال البيانات المعلنة تجمع بالضبط إلى البايتات المتبقية، ثم يفك كل جسم بترتيب الترويسات دون نسخ البيانات المضغوطة أو إعادة ترتيبها. قبل هذا الإصدار كان الملف نفسه يرفع خطأ «تنظيم الوصول العشوائي غير مدعوم» جافاً لحظة قراءة أعلام الترويسة
ملفات JBIG2 عشوائية الوصول نادرة، وهو بالضبط سبب ألمها حين تظهر فعلاً. فهي تخرج من خطوط أرشفة وأنظمة تصوير المستندات التي تريد أن يرى القارئ كل ترويسة مقطع، وبالتالي كل تبعية صفحة وقاموس، قبل لمس بايت مضغوط واحد. وتطبيق Delphi الذي يحول دفعات الأرشيفات الممسوحة إلى PDF يلتقي عادة واحداً منها في منتصف مهمة، بعد أن مرت مئات الملفات التسلسلية بنظافة، وفاكّ يتوقف ميتاً أمام ملف سليم التكوين ما هو إلا أفضل بهامش ضئيل من فاكّ يعرض خردة. وكان خط الإصدار نفسه قد أنهى لتوكه تعليم الفاكّ جداول Huffman المخصصة وأكواد البادئة القياسية في JBIG2، فكان الوصول العشوائي آخر فجوة تنظيم متبقية في حدود قدرات الفاكّ الموثقة
ما هو تنظيم الوصول العشوائي في JBIG2؟
تنظيم الوصول العشوائي هو إحدى ثلاث طرق يجيز بها الملحق D من T.88 تمهيد المقاطع نفسها: في التنظيم التسلسلي (D.1) تتشابك كل ترويسة مع بياناتها، والوصول العشوائي (D.2) يضع كل الترويسات أولاً وكل البيانات بعدها، والمضمّن (D.3) هو الصيغة عديمة الترويسة المستخدمة داخل حاويات أخرى مثل PDF. يبدأ ملف .jb2 مستقل بمعرف الثماني بايتات 97 4A 42 32 0D 0A 1A 0A، ثم بايت أعلام واحد، وعندما يكون عدد الصفحات معلوماً عدد صفحات من أربعة بايتات. البت 0 من بايت الأعلام يختار التنظيم، فـ 1 يعني تسلسلياً و 0 يعني وصولاً عشوائياً؛ وضبط البت 1 يعني أن عدد الصفحات مجهول وأن العد رباعي البايتات غائب. ويقرأها PDFlibPas في checkHeader و setFileHeaderFlags، أما البتات المحجوزة من 2 إلى 7 فتُتسامح معها ولا تُرفض
// TJBIG2StreamDecoder.setFileHeaderFlags في PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1; // 0 = وصول عشوائي (D.2)
randomAccessOrganisation := fileOrganisation = 0;
pagesKnown := headerFlags and 2; // 1 = عدد الصفحات محذوف
noOfPagesKnown := pagesKnown = 0;
// TJBIG2StreamDecoder.decodeJBIG2
validFile := checkHeader; // 97 4A 42 32 0D 0A 1A 0A
if not validFile then
begin
// تدفق PDF: بلا ترويسة ملف، تنظيم مضمّن، صفحة واحدة
noOfPagesKnown := True;
randomAccessOrganisation := False;
noOfPages := 1;
end
else
begin
setFileHeaderFlags;
if noOfPagesKnown then
noOfPages := getNoOfPages;
end;
PDF نفسه لا يحمل هذا التعميد أبداً. فتدفق الصور JBIG2Decode، الموصوف في §7.4.7 من ISO 32000-1، يحمل مقاطع الصفحة وحدها بتنظيم مضمّن، مع قواميس الرموز المشتركة منقولة إلى تدفق JBIG2Globals منفصل وبلا ترويسة ملف ولا مقاطع نهاية صفحة أو نهاية ملف. وحين يعجز decodeJBIG2 عن إيجاد المعرف الثماني البايتات يفترض ذلك بالضبط ويفرض فكاً تسلسلياً أحادي الصفحة. أما تصدير صور JBIG2 الأصيل فيسير في الاتجاه المعاكس ويغلف مقاطع PDF في ملف مستقل بايت أعلامه $03، تسلسلي بعدد صفحات مجهول، يليه ترويسة نهاية ملف ملحقة. فعمل الوصول العشوائي يلمس مساراً واحداً فقط: الملفات المستقلة المسلّمة إلى TPLJBIG2Decoder مباشرة، عادة قبل تحويلها أو إعادة ضغطها لصالح PDF، وهي المهمة التي يتولاها خلفيات ترميز JBIG2 في PDFlibPas على جانب الإخراج
لماذا لا يمكن قراءة ملف عشوائي الوصول بترتيب الملف؟
لا يمكن قراءة ملف عشوائي الوصول بترتيب الملف لأن لا شيء في تدفق البايتات يعلّم أين تتوقف كتلة الترويسات وتبدأ كتلة البيانات، سوى ترويسة مقطع نهاية الملف ذاتها. ترويسات مقاطع JBIG2 متغيرة الطول: فيمكن أن يكون عدد المقاطع المشار إليها بصيغة قصيرة من ثلاث بتات أو بصيغة طويلة مع خريطة بتات احتفاظ، وتأخذ أرقام المقاطع المشار إليها بايتاً أو بايتين أو أربعة بحسب رقم المقطع نفسه، وحقل اقتران الصفحة بايت واحد أو أربعة. القارئ التسلسلي الساذج يفك أول ترويسة ويقرأ طول بياناتها ثم يعامل أول بايتات الترويسة الثانية بوصفها بيانات ذلك المقطع. ولا يستطيع الفاكّ أن يدرك أنه أخطأ إلا بعد وقت طويل، ولهذا كان الكود القديم يرفض التنظيم جملةً واحدة بدل محاولته
كيف يفهرس PDFlibPas ترويسات المقاطع عشوائية الوصول؟
يفهرس PDFlibPas الترويسات عشوائية الوصول في مسح تمهيدي واحد، IndexRandomHeaders، يفك كل ترويسة ويسجل إزاحتها بالبايتات فقط ويتوقف عند أول ترويسة نهاية ملف (مقطع من النوع 51). تُفك كل ترويسة كاملة ثم تُرمى، فالفهرس مصفوفة أعداد صحيحة لا قائمة كائنات، ويراكم المسح التمهيدي أطوال البيانات المعلنة وهو يمضي. حين ينتهي المسح يجلس القارئ على أول بايت من بيانات أول مقطع، ويصير ذلك الموضع NextBodyOffset
// IndexRandomHeaders، محلية داخل TJBIG2StreamDecoder.readSegments
while not reader.isFinished do
begin
Offset := reader.bytePointer;
Header := TSegmentHeader.Create;
try
readSegmentHeader(Header);
if reader.BufferOverrun then
raise EJBIG2DecodeError.CreateFmt(
'JBIG2 truncated random-access header at byte %d', [Offset]);
if (HeaderCount > 0) and
(Cardinal(Header.getSegmentNumber) <= Cardinal(PreviousNumber)) then
raise EJBIG2DecodeError.Create('JBIG2 random-access segment numbers must increase');
PreviousNumber := Header.getSegmentNumber;
Count := Header.getSegmentDataLength;
if Count < 0 then
raise EJBIG2DecodeError.Create('JBIG2 unknown or oversized segment length is not supported');
Inc(TotalLength, Count); // مراكم Int64
HeaderOffsets[HeaderCount] := Offset; // ينمو على دفعات
Inc(HeaderCount);
if Header.getSegmentType = JBIG2_END_OF_FILE then
begin
if Count <> 0 then
raise EJBIG2DecodeError.Create('JBIG2 invalid end segment length');
FoundEnd := True;
Break;
end;
finally
Header.Free;
end;
end;
if not FoundEnd then
raise EJBIG2DecodeError.Create('JBIG2 random-access file is missing its end-of-file header');
if TotalLength > Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 truncated random-access segment data');
if TotalLength < Length(reader.Data) - reader.bytePointer then
raise EJBIG2DecodeError.Create('JBIG2 trailing random-access data');
NextBodyOffset := reader.bytePointer;
كل فحص في تلك الحلقة له سبب: ملف الوصول العشوائي يحمل فائض حماية أقل من التسلسلي. يجب أن تتزايد أرقام المقاطع تزايداً صارماً، مقارنة كقيم غير مأشرة، لأن ترويستين تدعيان الرقم نفسه تجعلان الأمر ملتبساً: أي جسم تعنيه قائمة الإحالات في منطقة لاحقة. ويقرأ حقل طول البيانات handleSegmentDataLength، الذي يطابق أي قيمة مضبوطة البت الأعلى، بما فيها علامة «الطول المجهول» 0xFFFFFFFF، إلى 1-؛ وفي تعميد الوصول العشوائي لا وسيلة أخرى لإيجاد موضع بداية الجسم التالي، فيرفض PDFlibPas ذلك الطول فوراً بدل البحث عن علامة نهاية. ويجب أن يطابق المجموع البايتات المتبقية بالضبط في الاتجاهين، وبايت زائد واحد بعد آخر جسم يفشل بـ «بيانات عشوائية الوصول زائدة». تلك الصرامة مقصودة: في هذا التعميد يعني عدم تطابق الطول أن كل جسم بعد نقطة الخطأ مزاح، والفاكّ الذي يتجاوز بايتاً طائشاً واحداً لا سبيل له إلى معرفة هل هو حشو غير مؤذٍ أم أول أعراض بيانات غير محاذاة
لماذا ضاع آخر مقطع نهاية صفحة؟
ضاع آخر مقطع نهاية صفحة لأن النسخة الأولى من حلقة الفك أبقت اختبار الإنهاء التسلسلي while not reader.isFinished، وفي تعميد الوصول العشوائي ينفد تدفق البيانات قبل فهرس الترويسات. مقاطع نهاية الصفحة (النوع 49) ونهاية الملف تحمل صفر بايت من البيانات، وهي عادة آخر ترويسات الملف. بعد استهلاك جسم المنطقة الأخير يجلس القارئ تماماً على نهاية المخزن، فتخرج الحلقة ولا تُرسل تلك المقاطع ذات الطول الصفري أبداً، وتظل الصفحة غير منتهية. الإصلاح يجعل حلقة الوصول العشوائي تعد الترويسات بدل البايتات. كل دورة تقفز بالقارئ إلى الترويسة المفهرسة التالية، وتعيد bitPointer إلى 7 لأن الجسم السابق ربما انتهى في منتصف بايت، وتعيد فك تلك الترويسة، ثم تنقل bytePointer إلى NextBodyOffset وتقدمه بعد الجسم. ويعمل معالجو المقاطع القائمون وفحوص المقاطع المشار إليها وتشخيصات Context دون تغيير، وما زالت رسالة الخطأ تبلّغ الإزاحة الأصلية للترويسة بالبايتات، لا موضع الجسم
// TJBIG2StreamDecoder.readSegments، الحلقة الرئيسية
if randomAccessOrganisation then
IndexRandomHeaders;
while (randomAccessOrganisation and (HeaderIndex < HeaderCount)) or
((not randomAccessOrganisation) and (not reader.isFinished)) do
begin
if randomAccessOrganisation then
begin
reader.bytePointer := HeaderOffsets[HeaderIndex];
reader.bitPointer := 7; // إعادة محاذاة بعد بايت جزئي
Inc(HeaderIndex);
end;
SegmentOffset := reader.bytePointer; // يُستخدم في سياق الخطأ
readSegmentHeader(segmentHeader);
if randomAccessOrganisation then
reader.bytePointer := NextBodyOffset; // قفز إلى بيانات هذا المقطع
DataLength := segmentHeader.getSegmentDataLength;
DataEnd := reader.bytePointer + DataLength;
NextBodyOffset := DataEnd;
// ... الإرسال إلى معالج المقطع القائم، ثم الانتقال إلى DataEnd
end;
ماذا يثبت تحقق الوصول العشوائي فعلاً؟
يثبت التحقق أن البايتات المعاد تنظيمها تفك إلى البكسلات نفسها التي تفك إليها أصولها التسلسلية، ويثبت أن مدخلات الوصول العشوائي المشوهة تفشل بنظافة؛ ولا يثبت تغطية ملفات الوصول العشوائي القادمة من مرمزات اعتباطية. يستخدم انحدار Pascal المشترك ملفاً اصطناعياً من 235 بايت مبنياً على تركيبة جدول مخصص يجب أن تفك إلى صف بكسلات سوداء من 7×1، بعدد صفحات معلوم وبعد حذف حقل العد كليهما، ثم يغذي الفاكّ كل بادئة مبترة من ذلك الملف، ورقم مقطع مكرراً، وبايتاً زائداً واحداً، وطول بيانات مجهولاً، مؤكداً في كل مرة أن LoadFromByteArray يعيد False ويترك Width و Height على الصفر. وحالة الصورة الحقيقية صورة تنقيح بجدول مخصص بأبعاد 500×473 أعيد تنظيم مقاطعها إلى تعميد الوصول العشوائي مع حفظ كل ترويسة أصلية وكل بايت مضغوط؛ وبصمة SHA-256 الخاصة بها تطابق خط الأساس التسلسلي المراجع تطابقاً تاماً. ذلك الملف اشتقاق أنتجه تحويل تنظيم، لا مستند عشوائي الوصول طبيعي وجد في الواقع، ولم يكن نموذج طبيعي من ذلك متاحاً. ونجحت الحزم عند 1,598 اختباراً لـ Delphi Win32، و 42 لحزمة صور Delphi Win64، و 48 لـ FPC Win32 و 46 لـ FPC Win64، إلى جانب حالات البكسلات التسلسلية الثلاث القائمة
تحميل ملف .jb2 عشوائي الوصول وحدوده
كود التطبيق لا يتغير: يكشف TPLJBIG2Decoder.LoadFromByteArray ترويسة الملف وتنظيمه بنفسه، ويعيد False على أي مدخل مرفوض والسبب في LastError، ويعرض الصفحة المفكوكة عبر Width و Height و GetScanline، التي تعيد بايتاً واحداً لكل بكسل
uses
SysUtils, Classes, PDFlibJBIG2;
function ReadJb2(const FileName: string): TJBIG2ByteArray;
var
FS: TFileStream;
begin
FS := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
SetLength(Result, FS.Size);
if Length(Result) > 0 then
FS.ReadBuffer(Result[0], Length(Result));
finally
FS.Free;
end;
end;
function CountBlackPixels(const FileName: string): Integer;
var
Decoder: TPLJBIG2Decoder;
Row: TJBIG2ByteArray;
X, Y: Integer;
begin
Result := 0;
Decoder := TPLJBIG2Decoder.Create;
try
// الملفات المستقلة التسلسلية وعشوائية الوصول تأخذ الاستدعاء نفسه
if not Decoder.LoadFromByteArray(ReadJb2(FileName)) then
raise Exception.Create('JBIG2 rejected: ' + Decoder.LastError);
for Y := 0 to Decoder.Height - 1 do
if Decoder.GetScanline(Y, Row) then
for X := 0 to Decoder.Width - 1 do
if Row[X] = 1 then
Inc(Result);
finally
Decoder.Free;
end;
end;
الحدود تستحق أن تقال بوضوح. دعم الوصول العشوائي ميزة تنظيم ملف، لا واجهة صفحات عشوائية: فـ TPLJBIG2Decoder ما زال يعيد الصورة النقطية للصفحة الأولى، ولا استدعاء يختار الصفحة 7 من ملف من 40 صفحة أو يفك الصفحات عند الطلب. المقاطع ذات طول البيانات المجهول تُرفض في ملفات الوصول العشوائي، والحدود القائمة على أطوال بادئات Huffman المخصصة وعدد مدخلات الجداول كما هي. تلك الحدود ضيقة بما يكفي ليستطيع تطبيق Delphi توجيه الحالات المرفوضة إلى معالجة أخرى عبر LastError، وبقية خط أنابيب الصور، من استخراج صور PDF إلى ترميز JBIG2، مغطى على صفحة منتج مكتبة PDF من PDFlibPas لـ Delphi