مقال تقني

MRC في Delphi: ضغط PDF الممسوح ضوئياً إلى JPEG + CCITT

تضغط losLab PDF Library لـ Delphi الصفحة الممسوحة ضوئياً بتقنية Mixed Raster Content عبر TPDFlib.CompressPageAsMRC. تعرض الطريقة الصفحة، وتقتطع قناعاً أمامياً 1-bit بدقة كاملة عند عتبة معينة، وتخفض دقة الباقي إلى خلفية JPEG، ثم تعيد بناء الصفحة كصورتين: خلفية /DCTDecode وقناع stencil من نوع /CCITTFaxDecode يُرسم بلون التعبئة الجاري

الصفحة التي وُجدت من أجلها هي نفسها التي تزخر بها كل أرشيفات المستندات: مسح ضوئي ملون بـ 300 DPI لفاتورة أو عقد، مخزن كملف JPEG واحد كبير. رمّزها بجودة تكفي لإبقاء النص مقروءاً فيصبح الملف ضخماً؛ واخفض الجودة فيتآكل الحرف وتظهر حوله هالات وتنسحب أطرافه، لأن أسوأ ما في JPEG على الإطلاق هو الحافة السوداء الحادة على ورق أبيض. تقنية Mixed Raster Content تكفّ عن مطالبة مرمّز واحد بفعل عملين

لماذا يجعل JPEG وحده النص الممسوح ضبابياً وكبيراً معاً؟

يتعامل JPEG وحده مع النص الممسوح بفلادة، لأن مرمّز DCT ينفق بتاته على التدرجات الناعمة ويقرّب الحواف الحادة بمعاملات عالية التردد تتخلص منها عملية التكميم (quantization) أولاً. النص في المسح الضوئي ليس سوى حواف حادة تقريباً. والإبقاء على تلك الحواف نظيفة يفرض جودة عالية على الصفحة بأكملها، بما في ذلك نسيج الورق والشعار ولطخة القهوة، لا شيء منها يحتاج ذلك أصلاً

فكرة MRC تقسم الصفحة حسب ماهية كل منطقة فعلاً. يصبح الحبر صورة ثنائية المستوى (bilevel) يسجل فيها مرمّز فاكس مثل CCITT سلاسل البكسلات السوداء والبيضاء دون فقد وبكثافة عالية جداً. وكل ما عدا ذلك، درجة الورق والصور والخلفيات الملونة، يصبح صورة متصلة التدرج (continuous-tone) يمكن تخزينها بجزء من الدقة، لأن أحداً لا يقرأ خلفية بدقة 300 DPI. تحافظ الطبقة الثنائية على حدّة النص بالدقة الكاملة؛ وتحفظ طبقة الخلفية اللون بتكلفة زهيدة

مسح JPEG واحد مقابل تقسيم طبقات Mixed Raster Content الذي ينشئه PDFlibPas CompressPageAsMRC داخل Delphi: مرمّز DCT يمسح حواف النص الحادة ويفرض جودة عالية على الصفحة كلها، بينما يصبح الحبر stencil‏ 1-bit بلا فقد بتقنية CCITT بدقة 300 DPI وتصبح درجة الورق خلفية JPEG من نوع DeviceRGB بدقة 75 DPI
تقنية Mixed Raster Content تكفّ عن مطالبة مرمّز واحد بفعل عملين: تحافظ الطبقة الثنائية على حدّة الحروف بالدقة الكاملة وتحفظ الخلفية اللون بجزء من الكلفة، لذا فالجودة من 50 إلى 70 غير ملحوظة عادة

كيف تقسم CompressPageAsMRC الصفحة إلى طبقتين؟

تشغّل TPDFlib.CompressPageAsMRC مساراً (pipeline) ثابتاً واحداً لكل صفحة. تعرض الصفحة عند الـ DPI المطلوب في صورة نقطية 24-bit، وتصنف كل بكسل بإضاءة موزونة وفق Rec. 601 (بألف الواحدة 299/587/114) مقابل Threshold، وتكتب النتيجة في قناع 1-bit بالدقة نفسها. وبالتوازي تقلص العرض نفسه بمعامل BGScale بمتوسط صندوقي، فقيمة BGScale تساوي 4 تحول كل كتلة بكسلات 4×4 إلى بكسل خلفية واحد

تدفق بيانات CompressPageAsMRC في PDFlibPas لـ Delphi: عرض واحد بدقة 300 DPI وعمق 24-bit يُقتطع بإضاءة Rec. 601 إلى stencil‏ ImageMask من نوع CCITTFaxDecode بـ K 1 وبالدقة الكاملة، ويُقلص بمتوسط صندوقي بـ BGScale 4 إلى خلفية DCTDecode من نوع DeviceRGB، وتحل الصورتان محل معاملات الصفحة الأصلية والقناع في الأعلى
كلتا الطبقتين تنبثقان من عرض واحد: يحدد DPI دقة الخط الذي ينجو من الاقتطاع، ويوازن BGScale تفاصيل الخلفية من 2 للصور الفوتوغرافية إلى 8 للنص العادي، وترسم الصفحة المعاد بناؤها الـ stencil بلون التعبئة الجاري

ثم يستبدل محتوى الصفحة. تزال المعاملات الأصلية، وتغرز الخلفية المخفضة الدقة كصورة /DCTDecode في /DeviceRGB‏ (ISO 32000-1 §7.4.8)، ويترمز القناع بمرمّز CCITT الخاص بالمكتبة كصورة /CCITTFaxDecode‏ (§7.4.6) مع وسمه /ImageMask true، وترسم الصورتان على صندوق الصفحة الكامل والقناع في الأعلى. التوقيع مختصر:

function CompressPageAsMRC(Page: Integer; DPI: Double;
  BGScale, JPEGQuality, Threshold, Options: Integer): Integer;
  • Page يبدأ من 1 ويجب أن يكون داخل PageCount
  • DPI هو دقة عرض الصورة النقطية المصدر، من 50 إلى 1200
  • BGScale هو معامل تخفيض دقة الخلفية، من 1 إلى 8
  • JPEGQuality هو جودة JPEG للخلفية، من 1 إلى 100
  • Threshold هو عتبة الإضاءة، من 1 إلى 255؛ البكسلات عند العتبة أو فوقها ورق، والأغمق منها حبر
  • Options محجوز ويجب أن يكون 0

هكذا تبدو دفعة نموذجية على ملف ممسوح ضوئياً. استدعِ SetFillColor قبل كل صفحة مباشرة، لسبب يشرحه القسم التالي

uses
  SysUtils, PDFlibrary;

procedure CompressScannedFile(const SrcFile, DstFile: string);
var
  Lib: TPDFlib;
  P, Replaced: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(SrcFile, '') <> 1 then
      raise Exception.CreateFmt('Cannot open %s', [SrcFile]);
    for P := 1 to Lib.PageCount do
    begin
      Lib.SetFillColor(0, 0, 0);  // لون حبر طبقة الـ stencil
      // قناع 300 DPI وخلفية 75 DPI وجودة JPEG تساوي 60 واقتطاع عند إضاءة 160
      Replaced := Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0);
      if Replaced = 0 then
        raise Exception.CreateFmt('MRC failed on page %d', [P]);
    end;
    if Lib.SaveToFile(DstFile) <> 1 then
      raise Exception.CreateFmt('Cannot save %s', [DstFile]);
  finally
    Lib.Free;
  end;
end;

لماذا تُرسم المقدمة بلون التعبئة الجاري؟

طبقة المقدمة قناع stencil، والقناع من هذا النوع لا يملك لوناً خاصاً به. يعرّف ISO 32000-1 §8.9.6.2 الصورة ذات /ImageMask true بأنها شكل 1-bit: قيم العينات تحدد أين يذهب الطلاء، والطلاء هو أي لون غير خطي (nonstroking color) جارٍ في حالة الرسوميات. هذا بالضبط ما يجعل الطبقة رخيصة، وهو أيضاً سبب عدم أخذ CompressPageAsMRC أي معامل لون

يرسم TPDFlib.CompressPageAsMRC القناع باللون الذي ضبطه SetFillColor، لذا فترتيب الاستدعاء مهم. المرسام يبث لون التعبئة بكسل: يسجل SetFillColor اللون ويوسمه بوصفه متغيراً، ولا يكتب رسم الـ stencil المعامل rg داخل كتلة q/Q الخاصة به إلا ما دام ذلك التغيير معلقاً. ارسم شكلاً معبأً في الأثناء فيستهلك ذلك الشكل التغيير المعلق بدلاً منه. وضبط اللون مباشرة قبل كل صفحة، كما في الدفعة أعلاه، يزيل أي تخمين. الأسود هو الاختيار المعتاد؛ والأزرق الكحلي الداكن يناسب النماذج ذات الحبر الأزرق التي تريد مخرجاتها أن تبدو أقل شبهاً بنسخة ضوئية

تسلسل البث الكسول للون التعبئة في طبقة stencil داخل MRC في PDFlibPas لـ Delphi: يسجل SetFillColor اللون بوصفه تغييراً معلقاً، ويكتب رسم الـ ImageMask stencil المعامل rg داخل كتلة q و Q الخاصة به ما دام التغيير معلقاً، بينما يستهلك شكل معبأ يتداخل التغيير المعلق بدلاً منه
قناع الـ stencil لا يملك لوناً، فكل بكسل حبر يأخذ لون تعبئة واحداً: الختم الأحمر والتوقيع الأزرق والنص الأسود تندمج معاً، ولهذا يجب استدعاء SetFillColor مباشرة قبل كل صفحة
// نموذج بحبر جاف أزرق: اعرض طبقة الحبر بالكحلي الداكن
Lib.SetFillColor(0.08, 0.12, 0.35);
Replaced := Lib.CompressPageAsMRC(PageNo, 300, 4, 55, 170, 0);

يتبع الموضع أصل إحداثيات المكتبة. تقرأ CompressPageAsMRC قيمة GetOrigin وترسم الصورتين من الحافة العليا لصندوق الصفحة: إحداثي علوي 0 عندما يكون الأصل أعلى اليسار (SetOrigin(1))، وإحداثي علوي PageHeight مع الأصل الافتراضي أسفل اليسار. لا تحتاج لتعديل شيء، لكن هذا يفسر سبب هبوط الطبقتين في مكانهما الصحيح أيّاً كان الأصل الذي يستخدمه مسارك أصلاً

من نتيجات دلالات الـ stencil ما يستحق القول بوضوح: كل بكسل مصنف كحبر يأخذ اللون نفسه. الختم الأحمر والتوقيع الأزرق والنص الأساسي الأسود ينتهي جميعها إلى لون تعبئة واحد على طبقة المقدمة. لا يحتفظ سوى خلفية JPEG بأثر باهت منخفض الدقة للألوان الأصلية تحتها

اختيار DPI و BGScale و Threshold وجودة JPEG

قيمة DPI تحدد حدّة النص، و300 هي القيمة الافتراضية المعقولة للمسوح المكتبية. يبقى القناع بالدقة الكاملة للعرض، لذا يحدد DPI مدى رفاعة الخط الذي ينجو من الاقتطاع. ولا تكسب شيئاً بالتجاوز إلى أعلى من دقة المسح الأصلية، لأن العارض سيكتفي بتكبير صورة المسح المغروسة. الذاكرة هي الحد الآخر: صفحة US Letter بدقة 300 DPI تُعرض في 2550×3300 بكسل، أي 25,245,000 بايت من بيانات البكسل 24-bit قبل اشتقاق القناع والخلفية، والصفحة نفسها عند سقف 1200 DPI تصبح 10200×13200 بكسل، نحو 404 MB

BGScale يبادل تفاصيل الخلفية بالحجم. عند 300 DPI، يعطي المعامل 4 خلفية بدقة 75 DPI، أي 638×825 بكسل لصفحة Letter، وهذا وفير لدرجة الورق ومناطق اللون المسطح. أما الصفحات ذات الصور الفوتوغرافية أو الرسائل نصف المنتنة (halftone) التي سينظر إليها القارئ فعلاً فتستحق 2؛ والنص العادي على ورق أبيض يحتمل 6 أو 8، لأن الخلفية لا تتجاوز درجة لون تقريباً

Threshold هو المعامل الذي يغير مظهر الصفحة. رفعه يصنف مزيداً من بكسلات الرمادي المتوسط كحبر، فيغلّظ الخطوط الرفعة وعلامات القلم الرصاص لكنه يجلب أيضاً ضجيج الماسح وظل الورق بوصفها بقعاً. وخفضه يعطي ورقاً أنظف وحروفاً أرفع أحياناً مكسورة. القيم حول 128 إلى 170 مدى بداية معقول للنص الأسود على أبيض؛ والمفاضلة نفسها هنا هي التي تناقشها مقالة عرض صفحات PDF أحادية اللون 1-bit داخل Delphi، حيث تقود عتبة الإضاءة نفسها RenderPageToMonochromeFileEx. بخلاف تلك الطريقة، لا يملك CompressPageAsMRC مرحلة إزالة بقع، فالمسوح المضوضاة تنفع معها عتبة أدنى قليلاً

JPEGQuality أهميته أقل مما في مسح أحادي الصورة، لأن الخلفية لم تعد تحمل أي حواف نص. الجودة من 50 إلى 70 غير ملحوظة عادة على خلفية 75 DPI خلف قناع حاد

ماذا يخبرك القيمة المعادة وماذا لا يخبرك؟

تعيد TPDFlib.CompressPageAsMRC حجم محتوى الصفحة الذي استبدله بالبايت، وتعيد 0 عند فشل شيء ما. إنها مؤشر نجاح لا مقياس توفير. في الصفحة الممسوحة النموذجية يكون تدفق المحتوى الأصلي بضع عشرات من البايتات، معاملات تكفي فقط لوضع صورة واحدة، فالرقم الموجب الصغير هو النتيجة المعتادة حتى حين يتقلص حجم الصفحة بالميجابايتات. أما الصفحة التي كان تدفق محتواها الأصلي فارغاً فتُبلّغ 0 رغم أنها أُعيد بناؤها

سلوك الفشل ليس ذرياً، وذلك هو الجزء الذي تصمم حوله. فحوصات الوسائط والعرض وترميز JPEG تجري قبل المساس بالصفحة، فقيمة BGScale خارج المدى أو JPEGQuality تساوي 101 أو فشل عرض تترك الصفحة كما هي تماماً. لكن بعد مسح المحتوى الأصلي، فإن فشل غرز إحدى الطبقتين أو رسمه يعيد 0 أيضاً، وعندها قد تكون الصفحة قد أعيد بناؤها جزئياً بالفعل. عامل أي 0 بوصفه سبباً لإتلاف المستند في الذاكرة بدل حفظه، ولا تكتب النتيجة أبداً فوق الملف المصدر

ولمعرفة ما وفّرته العملية فعلاً، قِس المستند. مقارنة طول SaveToString قبل وبعد أسرع فحص؛ ولجواب مفصل بحسب الفئات، يبيّن تدقيق حجم ملف PDF بواسطة AuditDocumentSpace إن كان الوزن انتقل من الصور كما هو متوقع

function MRCSavings(Lib: TPDFlib): Int64;
var
  Before: Int64;
  P: Integer;
begin
  Before := Length(Lib.SaveToString);
  for P := 1 to Lib.PageCount do
  begin
    Lib.SetFillColor(0, 0, 0);
    if Lib.CompressPageAsMRC(P, 300, 4, 60, 160, 0) = 0 then
      raise Exception.CreateFmt('Page %d failed; discard this document', [P]);
  end;
  Result := Before - Length(Lib.SaveToString);
end;

أين تتوقف طبقتا MRC هاتين دون ITU-T T.44

تنفذ CompressPageAsMRC نسخة MRC مبسطة عن قصد. النموذج الكامل في ITU-T T.44 يستخدم ثلاث طبقات لكل شريط: صورة خلفية، وصورة مقدمة تحمل ألوان الحبر، وقناع ثنائي المستوى ينتقي بينهما بكسلاً ببكسل، مع متسع لعدة أزواج من الأقنعة والمقدمات في صفحة واحدة. وبلغغة PDF ستكون الطبقة الثالثة صورة مقدمة ملونة منخفضة الدقة بقناع صريح (§8.9.6.3) بدل stencil يرسم بلون واحد. تتخطى PDF Library لـ Delphi طبقة لون المقدمة، وهذا ما يجعل النتيجة صغيرة إلى هذا الحد وهو أيضاً سبب انهيار الحبر الملون في لون التعبئة

  • تناسب الطريقة النص الأسود أو أحادي اللون على ورق ممسوح؛ أما المستندات المرمزة بالألوان والنص المظلل والأختام الملونة فتفقد لون حبرها
  • الصفحة المعاد بناؤها نقطية خالصة: أي نص حقيقي أو رسم متجهي أو محارف قابلة للتحديد في الصفحة الأصلية تتحول بكسلات، فاستخدمها على المسوح لا على الصفحات الرقمية الأصل، حيث تكون تجزئة الخطوط وتخفيض دقة الصور هي الأدوات الصحيحة
  • يستخدم القناع مرمّز CCITT الخاص بالمكتبة بوضع T.4 ثنائي الأبعاد، مكتوباً /CCITTFaxDecode مع /K 1؛ ويمتلك المرمّز أيضاً وضع Group 4 عبر AddImageCCITTFromBitmapStream، لكن CompressPageAsMRC لا يكشفه، ولا يستخدم JBIG2
  • التعليقات وحقول النماذج لا تُسطّح؛ تبقى كائنات فوق المحتوى المعاد بناؤه
  • الخلفية /DeviceRGB، فالمخرجات لا تناسب مسارات PDF/X-1a، ويرفض SetFillColor نفسه في وضع PDF/X-1a
  • المسح الذي يحتاج OCR أصلاً يجب التعرف عليه قبل MRC، ما دامت البكسلات الأصلية ما تزال موجودة بكامل عمق الألوان

FAQ

هل تجعل CompressPageAsMRC النص قابلاً للبحث؟

لا. يستبدل CompressPageAsMRC محتوى الصفحة بصورتين، فتزال معه كل معاملات النص في الصفحة الأصلية، بما فيها طبقة نص OCR الخفية. شغّل OCR على الصفحة المضغوطة لاحقاً، أو أعِد طبقة النص بعد الضغط

هل يمكنني ضغط بعض صفحات المستند فقط؟

نعم. يعمل CompressPageAsMRC على صفحة واحدة تبدأ من 1 في كل استدعاء، فتختار أنت الصفحات ويمكنك حتى تنويع BGScale و Threshold بحسب الصفحة، مثل معامل 2 لطبقة صور فوتوغرافية و6 لصفحات النص العادي

ماذا يجب أن تكون قيمة المعامل Options؟

مرر 0. المعامل Options محجوز للاستخدام المستقبلي في الإصدار الحالي ولا بتات معرفة له، فالقيمة غير الصفرية لا تحمل أي معنى اليوم

تتوفر CompressPageAsMRC منذ losLab PDF Library لـ Delphi إصدار v3.561.0 مع قطع البناء التي جُمعت منها، العرض و JPEG و CCITT وأقنعة الـ stencil، وكلها متاحة طرقاً عامة إن احتجت نسخة مختلفة من المسار. المكتبة ودعمها لـ C++Builder والمرجع الكامل للـ API على صفحة منتج PDF Library لـ Delphi