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