مقال تقني

مشاركة قواميس رموز JBIG2 عبر الصفحات في Delphi

يكرر عقد ممسوح ضوئيًا من خمسين صفحة الأبجدية نفسها في كل صفحة، لكن مُرمّز JBIG2 يبني قاموس رموز واحدًا لكل صورة يعيد تدريب تلك الأبجدية خمسين مرة منفصلة. يمكن لـHotPDF، مكوّن PDF الأصلي لـDelphi وC++Builder، بدلًا من ذلك تجميع قاموس رموز مشترك واحد عبر المستند بأكمله وترقيته إلى تدفق /JBIG2Globals واحد على مستوى المستند، بحيث يشير تدفق JBIG2 الخاص بكل صفحة فقط إلى معرّفات الرموز بدلًا من تخزين نسخته الخاصة من الأبجدية

تنحصر هذه المقالة عمدًا في كيفية بناء HotPDF لتلك المشاركة عبر الصفحات داخليًا فقط — أساسيات JBIG2، والمقارنة مع CCITT، والمقايضات بين Lossless وLossyLevel تعيش بالفعل في المقالة المرافقة حول ضغط JBIG2 الثنائي المستوى الأصلي في Delphi، التي تفترض هذه المقالة أنك قرأتها

لماذا لا يزال ضغط JBIG2 لكل صفحة يكرر التكلفة نفسها؟

الإجابة أن لا شيء يحمل حالة بين الاستدعاءات. في كل مرة يبني فيها مُرمّز HotPDF قاموس رموز لصورة واحدة، يكون ذلك القاموس محصورًا في نطاق استدعاء AddImage ذلك الواحد: يبدأ تمرير مطابقة الأشكال من الصفر، ويُصنَّف كل حرف على الصفحة كجديد، وتُرمَّز الصور النقطية الناتجة حسابيًا وتُخزَّن من جديد. أعطِ المُرمّز نفسه خمسين صفحة بالخط نفسه وسيكرر بسعادة عملية التدريب بأكملها خمسين مرة، لأنه من وجهة نظره كل صفحة صورة غير مرتبطة صادف أن تبدو متشابهة. تتفوق UseSymbolDictionary لكل صفحة بالفعل على ترميز منطقة عامة مسطّح بهامش واسع على صفحة واحدة، لكنها تصل إلى سقفها قبل الحد الأقصى الذي يتركه مسح ضوئي حقيقي متعدد الصفحات على الطاولة بكثير

كيف يشارك HotPDF قاموس رموز واحدًا عبر الصفحات؟

فعّل AccumulateGlobalsAcrossPages على THPDFJBIG2Options وسيبقي HotPDF قاموس رموز واحدًا حيًا في الذاكرة طوال عمر المستند بدلًا من التخلص منه بعد كل صورة. تُفحص حروف كل صفحة لاحقة مقابل ذلك القاموس الجاري قبل إعادة ترميز أي شيء: يُعاد استخدام شكل موجود بالفعل عبر معرّف رمزه، ولا يُلحَق ويُرمَّز في القاموس إلا شكل لم يره أحد من قبل. تعيد المقارنة استخدام منطق التسامح نفسه الذي تطبّقه LossyLevel على صفحة واحدة — مسح ضوئي به تشويش طفيف للحرف نفسه لا يزال يُحتسب تطابقًا — بحيث لا ينتفخ المُجمِّع بصمت إلى إدخال قاموس واحد لكل اختلاف على مستوى البكسل لنفس الحرف. يحدث الاستخراج أولًا ويغذّي تلك المقارنة: يجتاز HotPDF الصورة النقطية لكل صفحة ويستخرج أشكالًا متصلة عبر ملء الفيضان (flood fill) مقابل البكسلات السوداء، الفكرة نفسها لتتبع بقع الحبر يدويًا، وتلك الأشكال المستخرَجة، لا كتل بكسل خام، هي ما يُقارَن مقابل القاموس الجاري

كيف يقع القاموس المشترك داخل تدفق /JBIG2Globals

يُكتب القاموس المتراكم كمقطع قاموس رموز واحد داخل تدفق /JBIG2Globals، محفوظًا برقم مقطع ثابت بحيث يمكن لكل صفحة الإشارة إلى الهدف نفسه. داخل تنظيم JBIG2 المضمَّن الذي يُعرّفه ISO 32000-1 §7.4.7، يمكن لمقطع منطقة نصية تسمية مقطع آخر كمصدر رموزه عبر حقل المقطع المُشار إليه في ترويسة المقطع، وهذه هي الآلية بالضبط التي يعتمد عليها HotPDF: يحمل تدفق العموميات (globals) قاموس الرموز الكبير الواحد، ويتقلص تدفق JBIG2 الخاص بكل صفحة إلى مقطع معلومات صفحة بالإضافة إلى مقطع منطقة نصية تشير قائمة الإشارة الخاصة به مرة أخرى إلى مقطع العموميات. ما كان في السابق تدفق بتات مستقلًا بذاته لكل صفحة يصبح قائمة قصيرة من المواضع ومعرّفات الرموز، وكل صفحة مبنية بهذه الطريقة تشير إلى كائن /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 من 64 بت لبايتات التدفق، بحيث يمكن لصورتين صادف أن أنتجتا بيانات عموميات متطابقة بايتًا بايت أن تتشاركا كائن PDF واحدًا. قِيست مقابل مخرجات حقيقية، وبالكاد ساعدت تلك الذاكرة المؤقتة، لأن اكتشاف التكرار الحالي للصور الكاملة في HotPDF كان بالفعل يدمج الصور المتطابقة بايتًا بايت قبل أن تحظى الذاكرة المؤقتة بفرصة للعمل أصلًا. كان الدرس أن إزالة التكرار على مستوى التدفق لا تؤتي ثمارها إلا بمجرد أن تتمكن صورتا صفحة مختلفتان حقًا من مشاركة قاموس واحد متنامٍ، وهذا بالضبط ما يوفره التراكم الحقيقي عبر الصفحات

بالنسبة لتلك الحالة الأصعب، يضع تقدير هندسة HotPDF نفسه التوفير الإضافي عند نحو 30 إلى 60 بالمئة أصغر مما تحققه إزالة التكرار على مستوى التدفق وحدها، لمسح ضوئي نموذجي متعدد الصفحات مبني من خط واحد متكرر — يتحرك النطاق بحسب مقدار ما يتكرر فعليًا من المفردات البصرية للمستند، بما أن صفحة مليئة برسوم تخطيطية فريدة لا تمنح القاموس شيئًا يعيد استخدامه. عامل ذلك كهدف تصميمي لا ضمانًا لأي مدخل محدد، وقِس مستنداتك الخاصة بدلًا من الثقة برقم واحد. عرض 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.

أين يصطدم التراكم عبر الصفحات بحدوده

القاموس المتراكم محدود بسقف 4096 رمزًا، وهو السقف نفسه الذي يفرضه بالفعل المُرمّز الأصلي لكل صورة على صفحة واحدة. تجاوز ذلك الحد في منتصف المستند لا يجعل HotPDF يُطلق استثناءً أو يُجهض التشغيل: يرفض المُجمِّع الحرف الجديد، وتعود الصفحة التي قدّمته إلى الترميز المستقل لكل صورة تلقائيًا، بحيث يخرج المستند لا يزال صحيحًا — أنت فقط تتوقف عن الحصول على التوفير عبر الصفحات لأيًّا كانت الصفحات التي تجاوزت السقف. حارس ثانٍ يراقب الحجم الكلي بدلًا من عدد الرموز: بمجرد أن يتجاوز عرض الرموز المجمّع للقاموس المتراكم 131071 بكسلًا، يسكب HotPDF الدفعة الحالية إلى القرص ويبدأ مجموعة عموميات جديدة تلقائيًا، بدلًا من ترك بنية واحدة في الذاكرة تنمو بلا حدود. لا يحتاج أي من الحدين إلى أي شيفرة من جانبك، بما أن كليهما احتياطي تلقائي لا استثناء يجب عليك التقاطه

توافق PDF/A هو الإعداد الوحيد الذي يطفئ الآلية بأكملها بدلًا من مجرد تحديدها. يستبدل HotPDF بصمت CCITT من المجموعة 4 بدلًا من JBIG2 بمجرد أن تصبح PDFACompliance غير فارغة، في كل صفحة، بصرف النظر عن AccumulateGlobalsAcrossPages أو أي شيء آخر في JBIG2Options — اختيار توافق متعمد، لا خلل برمجي، لكنه يعني أن ملفًا أرشيفيًا ومشاركة رموز عبر الصفحات متنافيان اليوم. أيًّا كانت التهيئة التي تستقر عليها، فكّ ترميز ما كتبته قبل الوثوق به: أعد تحميل الملف عبر LoadFromFile واسحب كل صفحة عبر ExtractLoadedImage، التي تحل العموميات المشتركة عنك بالطريقة نفسها التي يفعلها أي قارئ متوافق، وقارن النتيجة مقابل صورك النقطية المصدرية

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 — فإن تدفقات الكائنات وتدفقات جدول المراجع المتقاطع تهاجم النصف الآخر من ميزانية حجم الملف بضغط بنية المستند التي تضيفها تلك الصفحات. تُشحن مشاركة عموميات JBIG2 عبر الصفحات كجزء من مكوّن HotPDF لـDelphi وC++Builder، إلى جانب خيارات JBIG2 لكل صورة وبقية خط أنابيب الضغط