مقاله فنی

اشتراک‌گذاری دیکشنری‌های نماد JBIG2 در سراسر صفحات در Delphi

یک قرارداد اسکن‌شده‌ی پنجاه‌صفحه‌ای همان الفبا را در هر صفحه تکرار می‌کند، اما یک رمزگذار JBIG2 که یک دیکشنری نماد به‌ازای هر تصویر می‌سازد، آن الفبا را پنجاه بار جداگانه دوباره آموزش می‌دهد. HotPDF، کامپوننت بومی PDF برای Delphi و C++Builder، در عوض می‌تواند یک دیکشنری نماد مشترک را در سراسر کل سند انباشته کند و آن را به یک جریان تکی /JBIG2Globals در سطح سند ارتقا دهد، پس جریان JBIG2 خودِ هر صفحه فقط به شناسه‌های نماد ارجاع می‌دهد به‌جای اینکه کپی خودش از الفبا را ذخیره کند

این مقاله عمداً محدود می‌ماند و فقط به این می‌پردازد که HotPDF چطور آن اشتراک‌گذاری بین‌صفحه‌ای را داخلاً می‌سازد — اصول JBIG2، مقایسه با CCITT، و معاوضه‌های Lossless در برابر LossyLevel از پیش در مقاله‌ی همراه درباره‌ی فشرده‌سازی بومی دوسطحی JBIG2 در Delphi زندگی می‌کنند، که این مقاله فرض می‌کند آن را خوانده‌اید

چرا فشرده‌سازی JBIG2 به‌ازای هر صفحه همچنان همان هزینه را تکرار می‌کند؟

پاسخ این است که هیچ چیزی وضعیت را بین فراخوانی‌ها حمل نمی‌کند. هر بار که رمزگذار HotPDF یک دیکشنری نماد برای یک تصویر می‌سازد، آن دیکشنری به همان یک فراخوانی AddImage محدود است: پاس تطبیق شکل از صفر شروع می‌شود، هر گلیف روی صفحه به‌عنوان جدید طبقه‌بندی می‌شود، و بیت‌مپ‌های حاصل به‌صورت حسابی کدگذاری و تازه ذخیره می‌شوند. همان رمزگذار را با پنجاه صفحه در همان قلم تنظیم‌شده تغذیه کنید و آن با کمال میل کل آن پاس آموزش را پنجاه بار تکرار می‌کند، چون از دید آن، هر صفحه یک تصویر بی‌ربط است که اتفاقاً شبیه به نظر می‌رسد. UseSymbolDictionary به‌ازای هر صفحه از پیش یک کدگذاری region عمومی تخت را با فاصله‌ی زیادی روی یک صفحه‌ی تکی شکست می‌دهد، اما به‌خوبی پیش از سقفی که یک اسکن چندصفحه‌ای واقعی روی میز می‌گذارد، متوقف می‌شود

HotPDF چطور یک دیکشنری نماد تکی را در سراسر صفحات به اشتراک می‌گذارد؟

AccumulateGlobalsAcrossPages را روی THPDFJBIG2Options فعال کنید و HotPDF یک دیکشنری نماد را برای کل عمر سند در حافظه زنده نگه می‌دارد به‌جای اینکه پس از هر تصویر آن را دور بریزد. گلیف‌های هر صفحه‌ی بعدی پیش از هر بازکدگذاری‌ای در برابر آن دیکشنری در حال اجرا بررسی می‌شوند: شکلی که از پیش وجود دارد از طریق شناسه‌ی نمادش دوباره استفاده می‌شود، و فقط شکلی که هیچ‌کس پیش از این ندیده افزوده و درون دیکشنری کدگذاری می‌شود. این مقایسه همان منطق تحمل‌پذیری‌ای را دوباره استفاده می‌کند که LossyLevel روی یک صفحه‌ی تکی اعمال می‌کند — یک اسکن کمی نویزی از همان حرف همچنان یک تطبیق شمرده می‌شود — پس انباشته‌گر بی‌سروصدا به یک ورودی دیکشنری به‌ازای هر انحراف در سطح پیکسل همان گلیف باد نمی‌کند. استخراج ابتدا اتفاق می‌افتد و آن مقایسه را تغذیه می‌کند: HotPDF بیت‌مپ هر صفحه را می‌پیماید و شکل‌های متصل را از طریق flood fill در برابر پیکسل‌های سیاه بیرون می‌کشد، همان ایده‌ی ردیابی دستی لکه‌های جوهر، و این همان شکل‌های استخراج‌شده هستند، نه بلوک‌های پیکسل خام، که در برابر دیکشنری در حال اجرا مقایسه می‌شوند

دیکشنری مشترک چطور درون یک جریان /JBIG2Globals می‌نشیند

دیکشنری انباشته‌شده به‌عنوان یک سگمنت دیکشنری نماد تکی درون جریان /JBIG2Globals نوشته می‌شود، در یک شماره‌ی سگمنت ثابت نگه‌داشته‌شده پس هر صفحه بتواند به همان هدف اشاره کند. درون سازمان‌دهی JBIG2 جاسازی‌شده‌ای که ISO 32000-1 §7.4.7 تعریف می‌کند، یک سگمنت text-region می‌تواند سگمنت دیگری را از طریق فیلد سگمنت-ارجاع‌شده در هدر سگمنت به‌عنوان منبع نمادش نام ببرد، و این دقیقاً همان سازوکاری است که HotPDF به آن تکیه می‌کند: جریان globals آن یک دیکشنری نماد بزرگ را حمل می‌کند، و جریان JBIG2 خودِ هر صفحه به یک سگمنت page-info به‌علاوه‌ی یک سگمنت text-region کوچک می‌شود که فهرست ارجاع‌شده‌اش به سگمنت globals برمی‌گردد. آنچه سابقاً یک بیت‌استریم خودکفا به‌ازای هر صفحه بود، به یک فهرست کوتاه از موقعیت‌ها و شناسه‌های نماد تبدیل می‌شود، و هر صفحه‌ای که این‌طور ساخته شده به همان شیء غیرمستقیم یکسان /JBIG2Globals ارجاع می‌دهد نه یک کپی از آن. پوشش رگرسیون خودِ HotPDF دقیقاً همین را بررسی می‌کند: یک سند کوتاه را که هر صفحه‌اش چیدمان گلیف متفاوتی دارد کدگذاری کنید، آن را دوباره بار کنید، و بشمارید چند ارجاع شیء متمایز /JBIG2Globals در فایل ظاهر می‌شود — یک سند، یک ارجاع شیء، بدون توجه به اینکه چند صفحه در آن نماد سهیم بوده‌اند

روشن‌کردن انباشت دیکشنری نماد بین‌صفحه‌ای

این کلید روی همان رکورد گزینه‌هایی می‌نشیند که در مقاله‌ی همراه پوشش داده شده، و پیش از اینکه انباشت واقعاً درگیر شود به چهار تنظیم نیاز دارد که با یکدیگر موافق باشند

var
  Pdf: THotPDF;
  Bmp: TBitmap;
  PageIdx, ImgIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.JBIG2Options.Lossless := True;
    Pdf.JBIG2Options.UseSymbolDictionary := True;
    Pdf.JBIG2Options.UseGlobalSegments := True;
    Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := True;  // opt-in, default False
    Pdf.JBIG2Options.UseExternalEncoder := False;            // accumulation needs the native path
    Pdf.JBIG2Options.UseNativeArithmeticFallback := True;
    Pdf.BeginDoc;
    for PageIdx := 0 to ScannedPages.Count - 1 do
    begin
      if PageIdx > 0 then
        Pdf.AddPage;
      Bmp := ScannedPages[PageIdx];             // 1-bit TBitmap for this page
      ImgIdx := Pdf.AddImage(Bmp, icJBIG2);
      Pdf.CurrentPage.ShowImage(ImgIdx, 0, 0, Bmp.Width, Bmp.Height, 0);
    end;
    Pdf.EndDoc;                                  // the shared /JBIG2Globals stream is finalized here
  finally
    Pdf.Free;
  end;
end;

آن جفت‌کردن یک تزئین اختیاری نیست. رابط رمزگذار خارجی که در مقاله‌ی فشرده‌سازی دوسطحی توصیف شده — همانی که از طریق RegisterJBIG2EncoderBackend برای نسبت‌های کیفیت-تولیدی ثبت می‌کنید — حول کدگذاری به‌ازای هر تصویر ساخته شده، و دموها و تست‌های رگرسیون انباشت خودِ HotPDF همیشه AccumulateGlobalsAcrossPages را با UseExternalEncoder := False جفت می‌کنند. این را به‌عنوان یک الزام سخت درنظر بگیرید نه یک پیشنهاد: اشتراک‌گذاری بین‌صفحه‌ای یک ویژگی رمزگذار بومی است، و یک رابط خارجی ثبت‌شده به‌سادگی بخشی از مسیری که دیکشنری مشترک را می‌سازد نیست

یک اسکن چندصفحه‌ای واقعاً چقدر کوچک‌تر می‌شود؟

پاسخ صادقانه با چیزی شروع می‌شود که ابتدا سوزن را حرکت نداد. یک نسخه‌ی پیشین یک کش با آدرس‌دهی محتوایی برای جریان‌های /JBIG2Globals افزود — یک جستجوی کلیددهی‌شده با یک هش ۶۴بیتی FNV-1a از بایت‌های جریان، پس دو تصویری که اتفاقاً داده‌ی globals یکسان بایت‌به‌بایت تولید کرده بودند می‌توانستند یک شیء PDF را به اشتراک بگذارند. در برابر خروجی واقعی سنجیده شد، آن کش به‌سختی کمک کرد، چون تشخیص تکرار کل-تصویر موجود HotPDF از پیش تصاویر یکسان بایت‌به‌بایت را پیش از اینکه کش اصلاً فرصت اجرا پیدا کند، فروبرده بود. درسی که گرفته شد این بود که حذف تکرار در سطح جریان فقط وقتی سود می‌دهد که دو تصویر صفحه‌ی واقعاً متفاوت همچنان بتوانند یک دیکشنری در حال رشد را به اشتراک بگذارند، که همان چیزی است که انباشت واقعی بین‌صفحه‌ای ارائه می‌دهد

برای آن حالت سخت‌تر، تخمین مهندسی خودِ HotPDF صرفه‌جویی اضافی را برای یک اسکن معمول چندصفحه‌ای که از یک فونت مکرر ساخته شده، حدود ۳۰ تا ۶۰ درصد کوچک‌تر از آنچه فقط حذف تکرار در سطح جریان به‌دست می‌آورد قرار می‌دهد — این بازه با اینکه چقدر از واژگان بصری سند واقعاً تکرار می‌شود جابه‌جا می‌شود، چون صفحه‌ای پر از نمودارهای یکتا هیچ چیزی برای استفاده‌ی دوباره به دیکشنری نمی‌دهد. این را به‌عنوان یک هدف طراحی درنظر بگیرید نه یک تضمین برای هر ورودی مشخصی، و اسناد خودتان را اندازه بگیرید نه اینکه به یک عدد تکی اعتماد کنید. دموی JBIG2Benchmark که با HotPDF عرضه می‌شود دقیقاً برای همین هدف وجود دارد: همان اسکن چندصفحه‌ای را چهار روش متفاوت کدگذاری می‌کند و اندازه‌ی فایل حاصل را برای هر پیکربندی چاپ می‌کند، پس مقایسه در برابر ترکیب اسکن خودتان اجرا می‌شود نه یک ترکیب مصنوعی

procedure RunScenario(const Title: string; AccumulateGlobals: Boolean);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.JBIG2Options.Lossless := True;
    Pdf.JBIG2Options.UseSymbolDictionary := True;
    Pdf.JBIG2Options.UseGlobalSegments := True;
    Pdf.JBIG2Options.AccumulateGlobalsAcrossPages := AccumulateGlobals;
    Pdf.JBIG2Options.UseExternalEncoder := not AccumulateGlobals;
    // ... encode the same three-page scan here, then compare file sizes.
  finally
    Pdf.Free;
  end;
end;

begin
  RunScenario('Per-image lossless baseline', False);
  RunScenario('Cross-page accumulated globals', True);
end.

انباشت بین‌صفحه‌ای کجا به محدودیت‌هایش برخورد می‌کند

دیکشنری انباشته‌شده به ۴۰۹۶ نماد محدود است، همان سقفی که رمزگذار بومی به‌ازای هر تصویر از پیش روی یک صفحه‌ی تکی اعمال می‌کند. از آن حد در میانه‌ی سند عبور کنید و HotPDF یک استثنا raise نمی‌کند یا اجرا را لغو نمی‌کند: انباشته‌گر گلیف جدید را رد می‌کند، و صفحه‌ای که آن را معرفی کرده به‌طور خودکار به کدگذاری مستقل به‌ازای هر تصویر فرومی‌گردد، پس سند همچنان درست بیرون می‌آید — فقط برای هر صفحه‌ای که از سقف عبور کرده، دیگر صرفه‌جویی بین‌صفحه‌ای دریافت نمی‌کنید. یک سد دوم به‌جای شمارش نماد، اندازه‌ی کلی را می‌پاید: به‌محض اینکه عرض نماد ترکیبی دیکشنری انباشته‌شده از ۱۳۱۰۷۱ پیکسل عبور کند، HotPDF دسته‌ی فعلی را به‌طور خودکار روی دیسک می‌ریزد و یک گروه globals تازه را آغاز می‌کند، به‌جای اینکه اجازه دهد یک ساختار درون‌حافظه‌ای بدون حد رشد کند. هیچ‌کدام از این دو محدودیت به هیچ کدی در سمت شما نیاز ندارد، چون هر دو fallbackهای خودکارند نه استثناهایی که باید بگیرید

مطابقت PDF/A تنها تنظیمی است که کل این سازوکار را به‌جای صرفاً محدودکردنش، خاموش می‌کند. HotPDF بی‌سروصدا CCITT Group 4 را همان لحظه‌ای که PDFACompliance غیرخالی باشد به‌جای JBIG2 جایگزین می‌کند، روی هر صفحه، مستقل از AccumulateGlobalsAcrossPages یا هر چیز دیگری در JBIG2Options — یک انتخاب عمدی برای مطابقت، نه یک باگ، اما به این معناست که یک پروفایل آرشیوی و اشتراک‌گذاری نماد بین‌صفحه‌ای امروز متقابلاً منحصربه‌فرد هستند. هر پیکربندی‌ای که به آن برسید، پیش از اعتمادکردن به آن، آنچه نوشته‌اید را رمزگشایی کنید: فایل را دوباره با LoadFromFile بار کنید و هر صفحه را از طریق ExtractLoadedImage بکشید، که globals مشترک را برایتان همان‌طور که هر خواننده‌ی مطابقی حل می‌کند، حل می‌کند، و نتیجه را در برابر بیت‌مپ‌های منبع خودتان مقایسه کنید

var
  Loaded: THotPDF;
  PageBmp: TBitmap;
  PageIdx: Integer;
begin
  Loaded := THotPDF.Create(nil);
  try
    Loaded.LoadFromFile('scanned-contract.pdf');
    for PageIdx := 0 to Loaded.PagesCount - 1 do
    begin
      PageBmp := Loaded.ExtractLoadedImage(PageIdx);   // resolves the shared globals for you
      try
        // Compare PageBmp against the source bitmap for this page.
      finally
        PageBmp.Free;
      end;
    end;
  finally
    Loaded.Free;
  end;
end;

اشتراک‌گذاری دیکشنری بین‌صفحه‌ای فقط سمت تصویر دوسطحی یک سند را لمس می‌کند. اگر همان خط لوله در کنار اسکن‌ها، صفحات متن تولیدشده را هم منتشر کند — صفحات جلد، صفحات فهرست، یک لایه‌ی متن OCR — جریان‌های شیء و جریان‌های xref نیمه‌ی دیگر بودجه‌ی اندازه‌ی فایل را هدف می‌گیرند با فشرده‌سازی ساختار سندی که آن صفحات می‌افزایند. globals مشترک بین‌صفحه‌ای JBIG2 به‌عنوان بخشی از کامپوننت HotPDF برای Delphi و C++Builder عرضه می‌شود، در کنار گزینه‌های JBIG2 به‌ازای هر تصویر و بقیه‌ی خط لوله‌ی فشرده‌سازی