مقال تقني

إعادة أخذ عينات صور PDF التكيفية في Delphi عبر PDFiumPas

تصل شكويان في الأسبوع التالي لإصدار ميزة الضغط: العقد الممسوح ضوئيًا أصبحت حروفه متدرجة السلالم ومُشعرة، والشعار الشفاف على صفحة الغلاف يجلس داخل هالة شاحبة. يجيب PDFiumPas عن كلتيهما في موضع واحد. تقيس TPdf.OptimizeImages كل صورة قبل تصغيرها، ثم تختار نواة إعادة أخذ عينات وتجمّع الألوان بصيغة مراعية لقناة ألفا

لم يكن هذا صحيحًا دائمًا. قبل الإصدار v3.100.0 كانت الدالة نفسها تخفض دقة كل صورة غير ثنائية المستوى بخطوة أقرب جار ثابتة، وهي الخوارزمية نفسها التي تُنتج الشكويين: تأخذ عينة نقطة واحدة من بكسل مصدر لكل بكسل إخراج، وتتعامل مع قيم RGB الكامنة تحت بكسل شفاف تمامًا وكأن قارئًا سيراها يومًا. الاستبدال في v3.100.0 يستبدل ذلك المسار الوحيد بخمس نوى، وقاعدة اختيار مقيسة، وميزانية ذاكرة عمل صريحة

لماذا تجعل إعادة أخذ العينات النص الممسوح يبدو ممزق الحواف؟

لأن أخذ عينات النقاط يجيب عن السؤال الخطأ. عندما يُعاد توجيه مسح ضوئي بدقة 300 DPI إلى 150 DPI، يمثّل كل بكسل وجهة كتلة اثنين في اثنين من بكسلات المصدر، ويُبقي أقرب جار واحدًا من الأربعة ويتجاهل الباقي. أيها يبقى يعتمد على التقريب، لذا فإن حافة الضربة التي كانت ممهدة بمضادة التعرج في المصدر تصبح رمية عملة لكل بكسل. النتيجة هي السلم المتعرج الكلاسيكي على حواف الحروف الرسومية، بالإضافة إلى نمط التموج (moire) في مناطق الألوان النصفية حيث حملت العينات المُتجاهَلة النمط بالصدفة. هذا أهم في PDF منه على الشاشة لأن الضرر دائم. يحمل كائن XObject الصوري بيانات عيناته إلى جانب /Width و/Height و/BitsPerComponent (ISO 32000-1 §8.9.5)، وإعادة أخذ العينات تعيد كتابة الثلاثة داخل الملف. التكبير الرديء في عارض هو إطار يمكنك إعادة رسمه، ولدى PDFiumPas آليات منفصلة لذلك في ذاكرة التخزين المؤقت للعرض وأداء التكبير. أما خفض الدقة الرديء فهو مستند جديد تسلّمه للعميل

لماذا يُفسد خفض الدقة بأقرب جار النص الممسوح في PDFiumPas لـ Delphi: كل بكسل إخراج يُبقي واحدًا من أربعة بكسلات مصدر ويتجاهل الباقي، مُنتجًا حواف حروف متعرجة ونمط تموج، وهو ما تستبدله نوى إعادة أخذ العينات الخمس
أخذ عينات النقاط يُبقي بكسل مصدر واحدًا لكل بكسل إخراج ويرمي الثلاثة الأخرى، ولهذا يقدّم PDFiumPas الآن خمس نوى بدلًا من واحدة

كيف يقيس PDFiumPas التفاصيل ويختار النواة

يقرر PDFiumPas لكل صورة على حدة، لا لكل مستند. قبل اختيار النواة يحسب درجة تفاصيل إضاءة مُطبَّعة من شبكة أخذ عينات محدودة: الخطوتان الأفقية والعمودية هما (Width + 63) div 64 و(Height + 63) div 64، لذا فإن مسحًا من 12000 بكسل وصورة مصغرة من 300 بكسل يكلفان المسحة 64×64 نفسها تقريبًا. في كل موضع مُعايَن يجمع الفرق المطلق مع الجار إلى اليمين والجار إلى الأسفل، عبر حتى ثلاث قنوات، ثم يقسم على عدد العينات مضروبًا في 255. تقع الدرجة بين 0 و1، حيث تجلس الرسومات التجارية المسطحة قرب الصفر وتتصاعد الملامح الفوتوغرافية الكثيفة

يعمل سلّم الاختيار بعدها بترتيب ثابت. إذا كان ResampleFilter أي شيء غير pirfAdaptive، يُستخدم ذلك المرشّح حرفيًا. وإلا: المحتوى ذو البت الواحد يأخذ pirfBilevel؛ وContentClass بقيمة piccLineArt يأخذ pirfBox؛ ومعامل قياس مقداره 4 أو أكثر يأخذ pirfBox أيضًا، لأن متوسط المساحة عند ذلك القدر من التصغير هو الأرخص والأصوب في آن واحد؛ أما piccPhoto أو درجة تفاصيل 0.08 فأعلى أو PreferredQuality بقيمة 0.9 فأعلى فتأخذ pirfLanczos بنواته ثلاثية الفصوص؛ وقياس مقداره 2 فأكثر أو جودة 0.7 فأعلى يأخذ pirfBicubic بنصف قطر 2؛ وكل ما تبقى يأخذ pirfBilinear. بما أن TPdfImageOptimizeOptions.Default يضبط PreferredQuality على 0.85، فإن التشغيل الافتراضي لا يتراجع إلى bilinear إلا عندما يكون التصغير طفيفًا والمحتوى مسطحًا

كيف يختار PDFiumPas نواة إعادة أخذ العينات في Delphi: مسحة محدودة من أربعة وستين في أربعة وستين تُنتج درجة تفاصيل مُطبَّعة، ثم يوجّه سلّم شروط ثابت كل صورة إلى مرشّح bilevel أو box أو Lanczos أو bicubic أو bilinear
درجة التفاصيل تكلف الشيء نفسه على مسح من 12000 بكسل وعلى صورة مصغرة، والسلّم تحتها يتوقف عند أول شرط يتحقق
uses
  PDFium;

procedure ShrinkScannedPdf(const InputFile, OutputFile: string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := InputFile;
    // الإعدادات الافتراضية: TargetDpi 150، وMinDpiRatio 1.5، وPreserveBilevel True،
    // وMinDimension 8، وpirfAdaptive، وpiccAuto، وجودة 0.85، وميزانية 64 MiB.
    Options := TPdfImageOptimizeOptions.Default;
    Options.TargetDpi := 150;
    Options.MinDpiRatio := 1.5;
    Options.ContentClass := piccAuto;
    Options.PreferredQuality := 0.85;
    if Pdf.OptimizeImages(Options, Report) and (Report.OptimizedCount > 0) then
      Pdf.SaveAs(OutputFile);
  finally
    Pdf.Free;
  end;
end;

لا تُلمَس الصورة إلا عندما يصل أكبر مقامي DPI لوضعها الأفقي والعمودي، مقسومًا على TargetDpi، إلى MinDpiRatio. هذا الحارس موجود حتى لا تُعاد ترميز صورة فوتوغرافية بدقة 160 DPI تستهدف هدف 150 DPI من أجل مكسب ستة في المئة يكلّف جيلًا من الجودة. الصور الأصغر من MinDimension على أي من المحورين، وهي 8 افتراضيًا، تُتخطّى بوصفها أيقونات أو خطوطًا فاصلة

لماذا تلتقط الشعارات الشفافة هدبًا أبيض؟

لأن اللون تحت بكسل شفاف تمامًا اعتباطي، والمتوسط الوزني البسيط يتيح له التصويت. صدّر شعارًا من أداة تصميم وستجد الهامش غير المرئي أبيض في الغالب، أو أسود، أو أيًا كان لون لوحة الرسم؛ قناة ألفا تخفيه، والجمع المباشر عبر بصمة النواة يخلطه فورًا عائدًا في الحافة المرئية. يتجنب PDFiumPas هذا بتجميع عينات BGRA بصيغة مضاعَفة مسبقًا (premultiplied) وإلغاء المضاعفة المسبقة عند بكسل الوجهة فقط

بشكل ملموس، كل عينة مساهمة تضيف channel * alpha * weight إلى مجمّع الألوان، وalpha * weight إلى مجمّع ألفا، وweight إلى مجموع الأوزان. يُقسَم لون الوجهة بعدها على مجمّع ألفا بدلًا من مجموع الأوزان، وهذه هي الخطوة المهمة: القسمة على مجموع الأوزان ستجرّ اللون نحو البكسلات غير المرئية، بينما القسمة على ألفا المتراكمة تعيد بناء اللون الذي اتفقت عليه العينات المرئية فعلًا. ألفا الوجهة كمية منفصلة، 255 * AlphaSum / WeightSum. الصيغ بلا ألفا تُقسَم على مجموع الأوزان كالمعتاد، وبايت الحشو في وجهة FPDFBitmap_BGRx يُكتب ثابتًا بقيمة 255، وكل قناة تُحصر بين 0 و255 قبل تخزينها. تنشأ تلك الألفا عادة من مدخل قناع ناعم في قاموس الصورة (ISO 32000-1 §11.4)، سبق لـ PDFium أن ركّبها في مخزن BGRA الذي يستلمه معيد أخذ العينات

كيف يزيل PDFiumPas الهالة البيضاء من صور PDF الشفافة في Delphi: تُجمَّع العينات بصيغة مضاعَفة مسبقًا، ويُقسَم لون الوجهة على ألفا المتراكمة بدلًا من مجموع الأوزان بحيث لا تتمكن البكسلات غير المرئية من التصويت
قسمة اللون المضاعَف مسبقًا على ألفا المتراكمة تعيد بناء ما اتفقت عليه العينات المرئية، بينما القسمة على مجموع الأوزان تجرّ الحافة نحو البكسلات غير المرئية
// شكل حلقة التجميع الداخلية، لكل عينة مصدر مساهمة
if SrcFormat = FPDFBitmap_BGRA then
  Alpha := PByte(PAnsiChar(Pixel) + 3)^ / 255
else
  Alpha := 1;
for Channel := 0 to Min(BytesPerPixel, 3) - 1 do
  Accumulated[Channel] := Accumulated[Channel] +
    PByte(PAnsiChar(Pixel) + Channel)^ * Alpha * Weight;
AlphaSum := AlphaSum + Alpha * Weight;
WeightSum := WeightSum + Weight;

// ... وعند بكسل الوجهة، تُلغى المضاعفة المسبقة مقابل مجموع ألفا
if SrcFormat = FPDFBitmap_BGRA then
begin
  if Abs(AlphaSum) > 1E-12 then
    ValueSum := Accumulated[Channel] / AlphaSum
  else
    ValueSum := 0;
end
else
  ValueSum := Accumulated[Channel] / WeightSum;

إبقاء الرسوم الخطية أحادية البت خارج المنطقة الرمادية

أي نواة متصلة تُطبَّق على مسح ثنائي المستوى تُنتج الرمادي، والرمادي هو بالضبط ما لا يجوز لصورة بنمط الفاكس أن تحتويه. لذلك يترك PDFiumPas الصور أحادية البت وشأنها افتراضيًا: PreserveBilevel هو True في TPdfImageOptimizeOptions.Default، وتحطّ تلك الصور في SkippedCount دون مساس. اضبطه على False فيتولى مسار pirfBilevel الأمر بدلًا من نواة تنعيم. يجتاز المستطيل المصدر الدقيق الذي يغطي كل بكسل وجهة، ويحسب متوسط الإضاءة بأوزان 0.114 و0.587 و0.299 بترتيب ذاكرة BGR، ويعتبِر النتيجة عند عتبة 127.5 لتصبح 0 أو 255 بلا وسط. لا يمكن كتابة أي قيمة وسيطة، لذا تبقى الحواف حادة ولا تتكون هالة رمادية حول الضربات الرفيعة؛ قناة ألفا لمصدر BGRA تُحسَب متوسطها كالمعتاد، ووجهة BGRx تأخذ الثابت 255. إذا احتجت البكسلات الأصلية بدلًا من مستند أصغر، فإن استخراج الصور من مستندات PDF هو المسار المنفصل

ماذا يحدث عندما تتجاوز الصورة ميزانية ذاكرة العمل؟

تُترَك الصورة كما كانت تمامًا، وتُعدّ. MaxWorkingBytes افتراضيها 64 MiB وتُفرض مرتين. قبل إنشاء خريطة بكسلات الوجهة، يرفض PDFiumPas الصورة إذا تجاوز العرض × الارتفاع × البايتات لكل بكسل الميزانية. بعد نجاح FPDFBitmap_CreateEx يتحقق مرة أخرى باستخدام الخطوة الحقيقية × الارتفاع، لأن حشو الصفوف قد يدفع التخصيص إلى ما وراء حد أجازه الناتج الساذج. أي من الرفضين يدمر الوجهة ولا يُرجع شيئًا. كن واضحًا بشأن التدهور الذي يعنيه هذا: الصورة المتجاوزة للميزانية لا تُعاد أخذ عيناتها بجودة أدنى، ولا تُقسَّم إلى بلاطات. يبقى الأصل في المستند، ويزداد كل من BudgetExceededCount وSkippedCount، وبالتالي يمكن لتشغيلة أن تُبلّغ عن النجاح بينما المستند مُحسَّن جزئيًا فقط. هذا سلوك أمان فشل مقصود، لكنه يعني أن التقرير ليس قراءة اختيارية. ثمة نمط فشل متميز أيضًا: الصور التي يعجز PDFium عن إنتاج خريطة بكسلات لها إطلاقًا، مثل مصادر CMYK أو JPX أو JBIG2 أو ذات الأقنعة، تزيد FailedCount بدلًا من ذلك وتُترَك هي الأخرى دون مساس

procedure OptimizeBatch(const Files: array of string);
var
  Pdf: TPdf;
  Options: TPdfImageOptimizeOptions;
  Report: TPdfImageOptimizeReport;
  I: Integer;
begin
  Options := TPdfImageOptimizeOptions.Default;
  Options.PreserveBilevel := False;              // استخدام تصويت المساحة الثنائي
  Options.ContentClass := piccPhoto;             // فرض Lanczos لمجموعات الصور الفوتوغرافية
  Options.MaxWorkingBytes := 256 * 1024 * 1024;  // هامش للمسوحات الكبيرة
  Pdf := TPdf.Create(nil);
  try
    for I := Low(Files) to High(Files) do
    begin
      Pdf.FileName := Files[I];
      if not Pdf.OptimizeImages(Options, Report) then
      begin
        WriteLn('optimize failed: ', Report.ErrorMessage);
        Continue;
      end;
      if Report.BudgetExceededCount > 0 then
        WriteLn(Files[I], ': ', Report.BudgetExceededCount,
          ' image(s) over budget and kept at full size');
      if Report.FailedCount > 0 then
        WriteLn(Files[I], ': ', Report.FailedCount,
          ' image(s) could not be decoded to a bitmap');
      if Report.OptimizedCount > 0 then
        Pdf.SaveAs(ChangeFileExt(Files[I], '.opt.pdf'));
    end;
  finally
    Pdf.Free;
  end;
end;

قراءة التقرير قبل تسليم الملف

صُمِّم TPdfImageOptimizeReport ليُشخَّص، لا ليُسجَّل فحسب. إلى جانب OptimizedCount وSkippedCount وFailedCount يكشف عدّادًا واحدًا لكل نواة، لذا فإن BoxFilterCount وBilinearFilterCount وBicubicFilterCount وLanczosFilterCount وBilevelFilterCount تخبرك بما خلصت إليه القاعدة التكيفية فعلًا بشأن مجموعتك. نتيجة كلها box تعني أن التصغيرات كانت حادة أو أن المحتوى صُنِّف رسومًا خطية؛ ونتيجة كلها Lanczos على مستند كنت تعتقد أنه رسوم خطية هي إشارة إلى أن ContentClass يجب ضبطه صراحة. AverageDetailScore هو الرقم الذي تقارنه بعتبة Lanczos البالغة 0.08 عند ضبط PreferredQuality، وPeakWorkingBytes يُظهر قدر ما احتاجته التشغيلة فعلًا من MaxWorkingBytes. الخيارات غير الصالحة تفشل بصوت عالٍ لا بصمت: TargetDpi غير الموجب، أو MinDpiRatio أقل من 1، أو PreferredQuality خارج النطاق من 0 إلى 1، أو MaxWorkingBytes غير الموجب، تُطلق EPdfError قبل لمس أي صفحة. وOptimizeImages يحرّر المستند في الذاكرة فقط؛ كل صفحة معدَّلة تُلتزَم عبر FPDFPage_GenerateContent، وبعدها لا تزال تستدعي SaveAs بنفسك. لمعاينة ما تغيّر، اعرض المستندين قبل وبعد كخرائط بكسلات كما هو موصوف في تحويل صفحات PDF إلى صور JPEG وقارن بينهما عند أقصى تكبير

إعادة أخذ العينات التكيفية هي واحدة من تلك الميزات التي تكون غير مرئية عندما تعمل وتُولّد تذاكر دعم عندما لا تعمل، ولهذا كان القياس ومعالجة ألفا وميزانية الذاكرة يجب أن تصل معًا لا بوصفها ثلاث تحسينات منفصلة. إذا كنت تقيّم هذا لمنتج Delphi أو C++Builder أو Lazarus، فإن سطح API الكامل وتفاصيل الترخيص على صفحة مكوّن PDFiumPas Delphi PDFium