مقاله فنی

فایل‌های دسترسی تصادفی JBIG2 و decode کردنشان در Delphi

PDFlibPas نسخهٔ 3.539.23 فایل‌های مستقل JBIG2ای را decode می‌کند که سازمان دسترسی تصادفی از ITU-T T.88 Annex D.2 را به کار می‌برند، جایی که اول هر هدر سگمنت می‌آید و دادهٔ سگمنت‌ها به همان ترتیب دنبالش می‌شود. decoder بومی Pascal در PDFlibJBIG2.pas آفست‌های هدر را تا هدر اجباری end-of-file ایندکس می‌کند، بررسی می‌کند شمارهٔ سگمنت‌ها صعودی باشد و طول‌های دادهٔ اعلام‌شده دقیقاً به بایت‌های باقی‌مانده بخورد، و بعد هر بدنه را به ترتیب هدرها decode می‌کند بدون کپی یا جابه‌جایی دادهٔ فشرده. پیش از این انتشار همان فایل، همان لحظه که فلگ‌های هدر خوانده می‌شد، خطای یک‌کلمه‌ای «random-access organisation is not supported» می‌داد

فایل‌های JBIG2 دسترسی تصادفی کمیاب‌اند، و دقیقاً به همین دلیل وقتی پیدایشان می‌شود دردسرسازند. از پایپ‌لاین‌های آرشیوی و سیستم‌های document-imaging بیرون می‌آیند که می‌خواهند reader قبل از دست زدن به حتی یک بایت فشرده، همهٔ هدرهای سگمنت و در نتیجه همهٔ وابستگی‌های صفحه و dictionary را ببیند. اپلیکیشن Delphiای که آرشیوهای اسکن‌شده را batch-convert می‌کند معمولاً یکی از این‌ها را وسط یک job ملاقات می‌کند، بعد از صدها فایل sequential که تمیز عبور کرده بودند، و decoderی که روی یک فایل خوش‌فرم کله‌پا می‌شود فقط اندکی بهتر از decoderی است که آشغال رندر می‌کند. همین خط انتشار تازه به decoder جدول‌های Huffman سفارشی JBIG2 و کدهای پیشوندی canonical را یاد داده بود، پس دسترسی تصادفی آخرین شکاف سازمانی باقی‌مانده در محدودهٔ قابلیت مستند decoder بود

سازمان دسترسی تصادفی JBIG2 چیست؟

سازمان دسترسی تصادفی یکی از سه راهی است که T.88 Annex D برای چیدن همان سگمنت‌ها اجازه می‌دهد: sequential (D.1) هر هدر را با داده‌اش درهم می‌بافد، random-access (D.2) همهٔ هدرها را اول و همهٔ داده‌ها را بعد می‌گذارد، و embedded (D.3) شکل بدون هدر است که داخل containerهای دیگر مثل PDF استفاده می‌شود. یک فایل مستقل .jb2 با شناسهٔ هشت‌بایتی 97 4A 42 32 0D 0A 1A 0A شروع می‌شود، بعدش یک بایت فلگ می‌آید و وقتی تعداد صفحات معلوم باشد یک شمارندهٔ صفحهٔ چهاربایتی. بیت 0 بایت فلگ سازمان را انتخاب می‌کند، با 1 یعنی sequential و 0 یعنی random-access؛ ست بودن بیت 1 یعنی تعداد صفحات نامعلوم است و شمارندهٔ چهاربایتی غایب. PDFlibPas این‌ها را در checkHeader و setFileHeaderFlags می‌خواند و بیت‌های رزرو 2 تا 7 را تحمل می‌کند نه اینکه رد کند

سازمان‌های فایل JBIG2 در PDFlibPas: بایت فلگی که setFileHeaderFlags می‌خواند sequential D.1 با هدرهای درهم‌بافته یا random-access D.2 با همهٔ هدرها پیش از بلوک داده یا embedded D.3 را انتخاب می‌کند، شکل بدون هدری که stream مربوط به JBIG2Decode با dictionaryها در JBIG2Globals استفاده می‌کند
سه چیدمان همان سگمنت‌ها را حمل می‌کنند، اما فقط دسترسی تصادفی باعث می‌شود reader قبل از دست زدن به یک بایت فشرده همهٔ وابستگی‌های صفحه و dictionary را ببیند، و همین است که پایپ‌لاین‌های آرشیوی خواستارش بودند
// TJBIG2StreamDecoder.setFileHeaderFlags, PDFlibJBIG2.pas
headerFlags := reader.readByte;
fileOrganisation := headerFlags and 1;          // 0 = random-access (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 stream: بدون هدر فایل، سازمان embedded، یک صفحه
  noOfPagesKnown := True;
  randomAccessOrganisation := False;
  noOfPages := 1;
end
else
begin
  setFileHeaderFlags;
  if noOfPagesKnown then
    noOfPages := getNoOfPages;
end;

خود PDF هرگز این چیدمان را حمل نمی‌کند. یک image stream از نوع JBIG2Decode، که در ISO 32000-1 §7.4.7 توصیف شده، فقط سگمنت‌های صفحه را در سازمان embedded نگه می‌دارد، با dictionaryهای نماد مشترک که به یک stream جداگانهٔ JBIG2Globals منتقل شده‌اند و بدون هدر فایل و سگمنت‌های end-of-page و end-of-file. وقتی decodeJBIG2 شناسهٔ هشت‌بایتی را پیدا نکند دقیقاً همین را فرض می‌کند و decode ترتیبی تک‌صفحه‌ای را تحمیل می‌کند. خروجی تصویر JBIG2 بومی برعکس عمل می‌کند و سگمنت‌های PDF را در یک فایل مستقل با بایت فلگ $03 می‌پیچد، یعنی sequential با تعداد صفحات نامعلوم، و یک هدر end-of-file هم به دنبالش اضافه می‌شود. پس کار دسترسی تصادفی فقط یک مسیر را لمس می‌کند: فایل‌های مستقلی که مستقیم به TPLJBIG2Decoder داده می‌شوند، معمولاً پیش از آنکه برای PDF تبدیل یا دوباره فشرده شوند، کاری که بک‌اندهای انکودر JBIG2 در PDFlibPas در سمت خروجی بر عهده دارند

چرا یک فایل دسترسی تصادفی را نمی‌شود به ترتیب فایل خواند؟

یک فایل دسترسی تصادفی را نمی‌شود به ترتیب فایل خواند چون هیچ چیز در بایت‌استریم نشان نمی‌دهد بلوک هدر کجا تمام و بلوک داده کجا شروع می‌شود، جز خود هدر سگمنت end-of-file. هدرهای سگمنت JBIG2 طول متغیر دارند: تعداد سگمنت‌های ارجاع‌شده می‌تواند شکل کوتاه سه‌بیتی یا شکل بلند با یک retention bitmap باشد، شمارهٔ سگمنت‌های ارجاع‌شده بسته به شمارهٔ خود سگمنت یک یا دو یا چهار بایت می‌گیرد، و فیلد page association یک یا چهار بایت است. یک reader ترتیبی ساده‌لوح هدر اول را parse می‌کند، طول داده‌اش را می‌خواند و بعد بایت‌های اول هدر دوم را دادهٔ همان سگمنت می‌گیرد. decoder خیلی دیرتر می‌فهمد که اشتباه رفته، و همین است که کد قدیمی به‌جای تلاش، سازمان را یکسره رد می‌کرد

PDFlibPas چطور هدرهای سگمنت دسترسی تصادفی را ایندکس می‌کند؟

PDFlibPas هدرهای دسترسی تصادفی را در یک پیش‌اسکن واحد ایندکس می‌کند، یعنی IndexRandomHeaders، که هر هدر را parse می‌کند، فقط آفست بایتی‌اش را ثبت می‌کند و روی اولین هدر end-of-file (نوع سگمنت 51) می‌ایستد. هر هدر کامل parse و دور انداخته می‌شود، پس ایندکس آرایه‌ای از اعداد صحیح است نه فهرستی از objectها، و پیش‌اسکن طول‌های دادهٔ اعلام‌شده را هم در حین کار جمع می‌زند. وقتی اسکن تمام می‌شود، reader روی اولین بایت دادهٔ اولین سگمنت نشسته و همان موقعیت می‌شود NextBodyOffset

پیش‌اسکن IndexRandomHeaders در PDFlibPas: هر هدر سگمنت parse و دور انداخته می‌شود و فقط آفست بایتی‌اش نگه داشته می‌شود، شمارهٔ سگمنت‌ها باید اکیداً صعودی باشد، طول نامعلوم 0xFFFFFFFF رد می‌شود، اسکن روی هدر end-of-file نوع 51 می‌ایستد، و طول‌های اعلام‌شده باید دقیقاً با بایت‌های باقی‌مانده برابر باشند
این سخت‌گیری عمدی است: در چیدمانی که هدرها تنها نقشهٔ داده‌اند، یک بایت سرگردان یعنی هر بدنهٔ بعدی ممکن است جابه‌جا باشد، پس decoderی که تحملش کند نمی‌تواند padding را از عدم‌هم‌ترازی تشخیص بدهد
// 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;

هر بررسی در آن حلقه به این دلیل وجود دارد که فایل دسترسی تصادفی افزونگی کمتری از نسخهٔ sequential دارد. شمارهٔ سگمنت‌ها باید اکیداً صعودی باشد و به‌صورت مقدار بی‌علامت مقایسه شود، چون دو هدر با شمارهٔ یکسان مبهم می‌کند فهرست ارجاعِ یک ناحیهٔ بعدی کدام بدنه را می‌گوید. فیلد طول داده توسط handleSegmentDataLength خوانده می‌شود که هر مقداری با بیت بالای ست، از جمله نشانگر طول نامعلوم 0xFFFFFFFF، را به 1- می‌برد؛ در چیدمان دسترسی تصادفی راه دیگری برای پیدا کردن شروع بدنهٔ بعدی نیست، پس PDFlibPas آن طول را همان‌جا رد می‌کند به‌جای آنکه دنبال نشانگر پایان بگردد. مجموع باید در هر دو جهت دقیقاً با بایت‌های باقی‌مانده بخورد، و یک بایت اضافی بعد از آخرین بدنه با خطای «trailing random-access data» شکست می‌خورد. این سخت‌گیری عمدی است: در این چیدمان عدم تطابق طول یعنی هر بدنه بعد از نقطهٔ خطا جابه‌جاست، و decoderای که با یک بایت سرگردان شانه بالا می‌اندازد هیچ راهی ندارد بفهمد padding بی‌آزار است یا اولین نشانهٔ دادهٔ ناهم‌تراز

چرا آخرین سگمنت end-of-page گم می‌شد؟

آخرین سگمنت end-of-page گم می‌شد چون نسخهٔ اول حلقهٔ decode آزمون پایان ترتیبی را نگه داشته بود، یعنی while not reader.isFinished، و در چیدمان دسترسی تصادفی جریان داده قبل از ایندکس هدر تمام می‌شود. سگمنت‌های end-of-page (نوع 49) و end-of-file صفر بایت داده دارند و معمولاً آخرین هدرهای فایل‌اند. بعد از مصرف شدن آخرین بدنهٔ ناحیه، reader دقیقاً روی انتهای بافر می‌نشیند، پس حلقه خارج می‌شود و آن سگمنت‌های صفرطول هرگز dispatch نمی‌شوند و صفحه ناتمام می‌ماند. fix کاری می‌کند حلقهٔ دسترسی تصادفی به‌جای بایت، هدر بشمارد. هر تکرار reader را به هدر ایندکس‌شدهٔ بعدی می‌پراند، bitPointer را به 7 ریست می‌کند چون بدنهٔ قبلی ممکن است وسط بایت تمام شده باشد، آن هدر را دوباره parse می‌کند، بعد bytePointer را به NextBodyOffset می‌برد و از روی بدنه جلو می‌رود. هندلرهای موجود سگمنت، بررسی‌های سگمنت ارجاع‌شده و دیاگنوستیک‌های Context بدون تغییر کار می‌کنند و پیام خطا همچنان آفست بایتی اصلی هدر را گزارش می‌کند، نه موقعیت بدنه را

حلقهٔ decode دسترسی تصادفی در PDFlibPas: هر تکرار به HeaderOffsets هدر جاری می‌پرد، bitPointer را به 7 ریست می‌کند تا دنباله‌های وسط‌بایتی خنثی شوند، برای بدنه به NextBodyOffset می‌پرد و به‌جای بایت هدر می‌شمارد تا سگمنت‌های صفرطول end-of-page قبل از پایان حلقه نوبتشان برسد
چون سگمنت‌های end-of-page و end-of-file صفر بایت داده دارند، جریان داده قبل از ایندکس هدر تمام می‌شود و فقط حلقه‌ای که هدر می‌شمارد می‌تواند به آن سگمنت‌های پایانی نوبت بدهد
// 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;         // استفاده‌شده در context خطا
  readSegmentHeader(segmentHeader);
  if randomAccessOrganisation then
    reader.bytePointer := NextBodyOffset;      // پرش به دادهٔ همین سگمنت
  DataLength := segmentHeader.getSegmentDataLength;
  DataEnd := reader.bytePointer + DataLength;
  NextBodyOffset := DataEnd;
  // ... dispatch به هندلر موجود سگمنت، بعد رفتن به DataEnd
end;

اعتبارسنجی دسترسی تصادفی واقعاً چه چیزی را ثابت می‌کند؟

اعتبارسنجی ثابت می‌کند بایت‌های بازآرایی‌شده همان پیکسل‌های نسخه‌های ترتیبی‌شان را decode می‌کنند و ورودی دسترسی تصادفی بدفرم تمیز شکست می‌خورد؛ ثابت نمی‌کند که فایل‌های دسترسی تصادفی از انکودرهای دلخواه پوشش داده شده‌اند. رگرسیون مشترک Pascal از یک فایل مصنوعی 235 بایتی روی یک fixture جدول-سفارشی استفاده می‌کند که باید به یک ردیف 7 در 1 از پیکسل‌های سیاه decode شود، هم با تعداد صفحات معلوم و هم با حذف فیلد شمارنده، و بعد هر پیشوند بریدهٔ آن فایل را به decoder می‌دهد، یک شمارهٔ سگمنت تکراری، یک بایت اضافی در انتها و یک طول دادهٔ نامعلوم، و هر بار ادعا می‌کند LoadFromByteArray مقدار False برگرداند و Width و Height را صفر بگذارد. مورد تصویر واقعی یک تصویر refinement جدول-سفارشی 500 در 473 است که سگمنت‌هایش با حفظ هر هدر و بایت فشردهٔ اصلی به چیدمان دسترسی تصادفی بازآرایی شده؛ SHA-256 آن دقیقاً با baseline ترتیبی بازبینی‌شده می‌خواند. آن فایل مشتقی‌ای است که با یک transform سازمانی تولید شده، نه یک سند دسترسی تصادفی طبیعی پیدا شده در دنیای واقعی، و هیچ نمونهٔ طبیعی از این دست در دسترس نبود. سویت‌ها روی 1,598 تست برای Delphi Win32 و 42 برای سویت تصویر Delphi Win64 و 48 برای FPC Win32 و 46 برای FPC Win64 قبول شدند، در کنار سه مورد پیکسلی sequential موجود

بارگذاری یک فایل .jb2 دسترسی تصادفی و محدودیت‌هایش

کد اپلیکیشن عوض نمی‌شود: TPLJBIG2Decoder.LoadFromByteArray خودش هدر فایل و سازمان را تشخیص می‌دهد، روی هر ورودی ردشده False برمی‌گرداند با دلیل در LastError، و صفحهٔ decode‌شده را از طریق 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
    // فایل‌های مستقل sequential و دسترسی تصادفی همان فراخوانی را می‌گیرند
    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;

مرزها ارزش گفتن صریح را دارند. پشتیبانی دسترسی تصادفی یک قابلیت سازمان فایل است، نه یک API صفحهٔ تصادفی: TPLJBIG2Decoder همچنان بیت‌مپ صفحهٔ اول را برمی‌گرداند و فراخوانی‌ای نیست که صفحهٔ 7 را از یک فایل 40 صفحه‌ای جدا کند یا صفحات را تنبل decode کند. سگمنت‌های با طول دادهٔ نامعلوم در فایل‌های دسترسی تصادفی رد می‌شوند و محدودیت‌های موجود روی طول پیشوند Huffman سفارشی و شمار درایه‌های جدول بدون تغییرند. این محدودیت‌ها به‌قدر کافی باریک‌اند که یک اپلیکیشن Delphi بتواند موارد ردشده را با LastError به جای دیگری بفرستد، و بقیهٔ پایپ‌لاین تصویر، از استخراج تصویر PDF تا انکود JBIG2، در صفحهٔ محصول کتابخانه PDF در Delphi از PDFlibPas پوشش داده شده