مقاله فنی

بازنمونه‌گیری تطبیقی تصویر PDF در Delphi با PDFiumPas

دو شکایت هفته بعد از انتشار یک قابلیت فشرده‌سازی می‌رسد: قرارداد اسکن‌شده حالا حروف پله‌پله و کرکی دارد، و لوگوی شفاف روی صفحه جلد داخل یک هاله کم‌رنگ نشسته است. PDFiumPas هر دو را در یک‌جا جواب می‌دهد. TPdf.OptimizeImages هر تصویر را قبل از کوچک کردنش اندازه می‌گیرد، بعد یک کرنل بازنمونه‌گیری برمی‌گزیند و رنگ را به‌شکل آگاه از آلفا انباشته می‌کند

همیشه این‌طور نبود. قبل از v3.100.0 همان متد هر تصویر غیر باینری را با یک گام nearest-neighbour ثابت کم‌نمونه می‌کرد، که دقیقاً همان الگوریتمی است که هر دو شکایت را تولید می‌کند: به ازای هر پیکسل خروجی یک پیکسل مبدأ را نقطه‌نمونه می‌گیرد، و RGB نشسته زیر یک پیکسل کاملاً شفاف را طوری رفتار می‌کند که انگار خواننده‌ای روزی آن را خواهد دید. بازنویسی در v3.100.0 آن تک‌مسیر را با پنج کرنل، یک قاعده انتخاب اندازه‌گیری‌شده و یک بودجه صریح حافظه کاری جایگزین می‌کند

چرا کم‌نمونه‌سازی متن اسکن‌شده را دندانه‌دار نشان می‌دهد؟

چون نقطه‌نمونه‌گیری به پرسش غلط جواب می‌دهد. وقتی یک اسکن 300 DPI به 150 DPI باز هدف‌گیری می‌شود، هر پیکسل مقصد نماینده یک بلوک دوبه‌دو از پیکسل‌های مبدأ است، و nearest neighbour یکی از چهار تا را نگه می‌دارد و بقیه را دور می‌ریزد. اینکه کدام زنده می‌ماند به گرد کردن بستگی دارد، پس یک لبه خط که در مبدأ به‌نرمی antialias شده بود به یک شیر یا خط به ازای هر پیکسل تبدیل می‌شود. نتیجه همان پله aliased کلاسیک در امتداد لبه‌های گلیف است، به‌علاوه موآر روی ناحیه‌های هافتون که نمونه‌های دورریخته اتفاقاً الگو را حمل می‌کردند. این در PDF بیشتر از روی صفحه اهمیت دارد چون آسیب دائمی است. یک XObject تصویر داده نمونه‌اش را کنار /Width و /Height و /BitsPerComponent حمل می‌کند (ISO 32000-1 §8.9.5)، و بازنمونه‌گیری هر سه را داخل فایل بازنویسی می‌کند. یک زوم بد در یک viewer قابی است که می‌توانید دوباره بکشید، و PDFiumPas برای آن ماشین‌آلات جداگانه‌ای در کارایی کش رندر و زوم دارد. یک کم‌نمونه‌سازی بد یک سند جدید است که به مشتری تحویل می‌دهید

چرا کم‌نمونه‌سازی nearest-neighbour متن اسکن‌شده را در PDFiumPas برای Delphi خراب می‌کند: هر پیکسل خروجی یکی از چهار پیکسل مبدأ را نگه می‌دارد و بقیه را دور می‌ریزد، و لبه‌های گلیف aliased و موآر تولید می‌کند، که پنج کرنل بازنمونه‌گیری جایگزینش می‌شوند
نقطه‌نمونه‌گیری به ازای هر پیکسل خروجی یک پیکسل مبدأ نگه می‌دارد و سه تای دیگر را دور می‌ریزد، و دلیلش این است که PDFiumPas حالا پنج کرنل به‌جای یک کرنل ارائه می‌دهد

PDFiumPas چطور جزئیات را اندازه می‌گیرد و کرنل برمی‌گزیند

PDFiumPas به ازای هر تصویر تصمیم می‌گیرد، نه به ازای هر سند. قبل از انتخاب کرنل یک امتیاز جزئیات روشنایی نرمال‌شده از یک شبکه نمونه‌گیری محدود محاسبه می‌کند: گام‌های افقی و عمودی برابر (Width + 63) div 64 و (Height + 63) div 64 هستند، پس یک اسکن 12000 پیکسلی و یک بندانگشتی 300 پیکسلی هر دو حدوداً همان جاروب 64در64 را هزینه دارند. در هر موقعیت نمونه‌گیری‌شده قدر مطلق تفاوت با همسایه سمت راست و همسایه پایین را روی تا سه کانال جمع می‌زند، بعد بر شمارش نمونه ضرب در 255 تقسیم می‌کند. امتیاز در بازه 0 تا 1 فرود می‌آید، جایی که گرافیک‌های مسطح تجاری نزدیک صفر می‌نشینند و بافت عکاسی متراکم بالا می‌رود

نردبان انتخاب بعد به‌شکل ثابت اجرا می‌شود. اگر ResampleFilter هر چیز دیگری جز pirfAdaptive باشد، همان فیلتر عیناً استفاده می‌شود. وگرنه: محتوای 1-بیتی 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 یک کرنل بازنمونه‌گیری برمی‌گزیند: یک جاروب محدود 64در64 امتیاز جزئیات نرمال‌شده تولید می‌کند، بعد یک نردبان ثابت از شرایط هر تصویر را به فیلتر bilevel یا box یا Lanczos یا bicubic یا bilinear می‌فرستد
امتیاز جزئیات روی اسکن 12000 پیکسلی همان هزینه بندانگشتی را دارد، و نردبان زیر آن در اولین شرطی که match شود می‌ایستد
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 مگابایت.
    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 به‌طور پیش‌فرض، به‌عنوان آیکون یا خط‌کش رد می‌شوند

چرا لوگوهای شفاف حاشیه سفید می‌گیرند؟

چون رنگ زیر یک پیکسل کاملاً شفاف دلخواه است، و یک میانگین وزنی ساده اجازه می‌دهد رأی بیاورد. یک لوگو را از یک ابزار طراحی خروجی بگیرید و حاشیه نامرئی اغلب سفید است، یا سیاه، یا هر چه بوم بود؛ کانال آلفا پنهانش می‌کند، و یک جمع مستقیم روی footprint کرنل فوراً آن را دوباره داخل لبه مرئی می‌آمیزد. PDFiumPas این را با انباشتن نمونه‌های BGRA به‌شکل premultiplied و خنثی کردن premultiplication فقط در پیکسل مقصد اجتناب می‌کند

به‌طور مشخص، هر نمونه مشارکت‌کننده channel * alpha * weight به انباشتگر رنگ اضافه می‌کند، alpha * weight به یک انباشتگر آلفا، و weight به جمع وزن‌ها. رنگ مقصد بعد بر انباشتگر آلفا تقسیم می‌شود نه بر جمع وزن‌ها، و همان گامی است که اهمیت دارد: تقسیم بر جمع وزن‌ها رنگ را به سمت پیکسل‌های نامرئی می‌کشید، در حالی که تقسیم بر آلفای انباشته رنگی را بازسازی می‌کند که نمونه‌های مرئی واقعاً بر سرش توافق داشتند. آلفای مقصد یک کمیت جداست، 255 * AlphaSum / WeightSum. قالب‌های بدون آلفا طبق معمول بر جمع وزن‌ها تقسیم می‌کنند، بایت padding مقصد FPDFBitmap_BGRx به‌شکل ثابت 255 نوشته می‌شود، و هر کانال قبل از ذخیره در بازه 0 تا 255 گیر می‌افتد. آن آلفا معمولاً از یک مدخل soft mask در دیکشنری تصویر منشأ می‌گیرد (ISO 32000-1 §11.4)، که PDFium از قبل به بافر BGRA که بازنمونه‌گیر دریافت می‌کند کامپوزیت کرده است

PDFiumPas چطور در Delphi هاله سفید را از تصاویر شفاف PDF حذف می‌کند: نمونه‌ها به‌شکل premultiplied انباشته می‌شوند، و رنگ مقصد بر آلفای انباشته تقسیم می‌شود نه جمع وزن‌ها تا پیکسل‌های نامرئی نتوانند رأی بیاورند
تقسیم رنگ premultiplied بر آلفای انباشته بازمی‌سازد آنچه نمونه‌های مرئی بر سرش توافق داشتند، در حالی که تقسیم بر جمع وزن‌ها لبه را به سمت پیکسل‌های نامرئی می‌کشد
// شکل حلقه انباشت داخلی، به ازای هر نمونه مبدأ مشارکت‌کننده
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;

// ... و در پیکسل مقصد، unpremultiply نسبت به جمع آلفا
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;

نگه داشتن line art یک‌بیتی بیرون از ناحیه خاکستری

هر کرنل پیوسته‌ای که روی اسکن باینری اعمال شود خاکستری تولید می‌کند، و خاکستری دقیقاً چیزی است که یک تصویر به سبک فکس اجازه ندارد داشته باشد. پس PDFiumPas تصاویر 1-بیتی را به‌طور پیش‌فرض ول می‌کند: PreserveBilevel در TPdfImageOptimizeOptions.Default مقدار True است، و چنین تصاویری دست‌نخورده داخل SkippedCount فرود می‌آیند. آن را روی False ست کنید و مسیر pirfBilevel به‌جای یک کرنل نرم‌کننده کار را می‌گیرد. مستطیل مبدأ دقیقی که هر پیکسل مقصد را پوشش می‌دهد را می‌پیماید، روشنایی را با وزن‌های 0.114 و 0.587 و 0.299 در ترتیب حافظه BGR میانگین می‌گیرد، و نتیجه را در 127.5 آستانه‌گذاری می‌کند به یک 0 یا 255 مسطح. هیچ چیز میانی قابل نوشتن نیست، پس لبه‌ها تیز می‌مانند و هیچ هاله خاکستری دور خطوط نازک شکل نمی‌گیرد؛ کانال آلفای یک مبدأ BGRA طبق معمول میانگین می‌شود، و مقصد BGRx ثابت 255 می‌گیرد. اگر خود پیکسل‌ها را لازم دارید نه یک سند کوچک‌تر را، استخراج تصاویر از اسناد PDF مسیر جداگانه است

وقتی یک تصویر از بودجه حافظه کاری فراتر می‌رود چه می‌شود؟

دقیقاً همان‌طور که بود رها می‌شود، و شمرده می‌شود. MaxWorkingBytes به‌طور پیش‌فرض 64 مگابایت است و دو بار اعمال می‌شود. قبل از ساخته شدن bitmap مقصد، PDFiumPas اگر عرض ضرب در ارتفاع ضرب در بایت به ازای هر پیکسل از بودجه فراتر رود تصویر را رد می‌کند. بعد از موفقیت FPDFBitmap_CreateEx دوباره با stride واقعی ضرب در ارتفاع چک می‌کند، چون padding ردیف می‌تواند یک تخصیص را از حدی که حاصل‌ضرب ساده‌لوحانه پاک کرده بود عبور دهد. هر رد شدن مقصد را نابود می‌کند و هیچ چیز برنمی‌گرداند. روشن باشید درباره تخریبی که این به همراه دارد: یک تصویر فراتر از بودجه با کیفیت پایین‌تر بازنمونه نمی‌شود، و به تایل تقسیم نمی‌شود. اصل سند در سند می‌ماند، BudgetExceededCount و SkippedCount هر دو افزایش می‌یابند، و پس یک اجرا می‌تواند موفقیت گزارش کند در حالی که سند فقط تا حدی بهینه شده. آن رفتار fail-safe عمدی است، اما یعنی گزارش خواندنی اجباری است. یک حالت خرابی متمایز هم وجود دارد: تصاویری که bitmapشان 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;              // استفاده از رأی مساحتی bilevel
  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 یعنی کاهش‌ها تند بوده‌اند یا محتوا به‌شکل line art طبقه‌بندی شده؛ یک نتیجه تمام-Lanczos روی سندی که فکر می‌کردید line art است نشانه آن است که ContentClass باید صریح ست شود. AverageDetailScore عددی است که هنگام تنظیم PreferredQuality با آستانه 0.08 لاکزوس مقایسه می‌شود، و PeakWorkingBytes نشان می‌دهد اجرا واقعاً چه مقدار از MaxWorkingBytes را لازم داشت. آپشن‌های نامعتبر با صدا شکست می‌خورند نه بی‌سروصدا: TargetDpi غیرمثبت، MinDpiRatio زیر 1، PreferredQuality بیرون از بازه 0 تا 1، یا MaxWorkingBytes غیرمثبت، پیش از لمس هر صفحه‌ای EPdfError پرتاب می‌کند. و OptimizeImages فقط سند درون-حافظه‌ای را ویرایش می‌کند؛ هر صفحه اصلاح‌شده با FPDFPage_GenerateContent کامیت می‌شود، بعد از آن شما همچنان خودتان SaveAs را صدا می‌زنید. برای اینکه با چشم ببینید چه عوض شد، اسناد قبل و بعد را به bitmapها رندر کنید آن‌طور که در تبدیل صفحات PDF به تصاویر JPEG توضیح داده شده و در زوم کامل مقایسه‌شان کنید

بازنمونه‌گیری تطبیقی یکی از آن قابلیت‌هاست که وقتی کار می‌کند نامرئی است و وقتی نمی‌کند تیکت پشتیبانی تولید می‌کند، و به همین دلیل بود که اندازه‌گیری و هندل کردن آلفا و بودجه حافظه باید با هم فرود می‌آمدند نه به‌شکل سه بهبود جداگانه. اگر این را برای یک محصول Delphi یا C++Builder یا Lazarus ارزیابی می‌کنید، سطح کامل API و جزئیات لایسنس روی صفحه کامپوننت PDFium دلفی PDFiumPas است