مقاله فنی

رندر صفحه‌های PDF به monochrome 1-bit در Delphi

یک gateway فکس تصویر 24-bit page render شما را نمی‌خواهد. pipeline آرشیو که یک میلیون invoice شبیه اسکن را ذخیره می‌کند هم نمی‌خواهد، و front-end OCR که قبل از هر چیز همه‌چیز را به سیاه و سفید threshold می‌کند تا اصلاً دنبال character بگردد هم نمی‌خواهد. هر سه یک چیز می‌خواهند: یک bitmap تمیز 1-bit، یک bit برای هر pixel، که هر dot آن یا ink است یا paper. اگر یک BMP تمام‌رنگی به آن‌ها بدهید، در هر صورت 23 bit در هر pixel را دور می‌ریزند، معمولاً با یک dithering pass بدتر از چیزی که خودتان می‌توانستید انجام دهید. سؤال جالب این است که این down-conversion کجا باید انجام شود، و پاسخ در PDF Library for Delphi در نهایت چیز مفیدی دربارهٔ این‌که چگونه یک renderer را که ترجیح می‌دهید از نو ننویسید گسترش بدهید، می‌گوید

PDF Library for Delphi یک کتابخانهٔ بومی Object Pascal PDF برای Delphi و C++Builder است. هستهٔ rendering آن یک page را به bitmap rasterize می‌کند و می‌تواند BMP، PNG، JPEG، WMF و چند format دیگر را emit کند. چیزی که تا همین اواخر انجام نمی‌داد این بود که یک bitmap واقعاً monochrome پس بدهد، یا فقط بخشی از یک page را render کند. هر دو در v3.83.0 اضافه شدند، و هر دو به‌عنوان لایه‌های convenience نازک روی renderer موجود ساخته شدند، نه به‌صورت تغییر در خود rasterizer. همین constraint تمام داستان است

چرا بعد از rendering down-convert کنیم، نه داخل renderer

راه بدیهی برای تولید یک تصویر 1-bit این است که به rasterizer بگویید 1-bit رسم کند. این همچنین همان راهی است که همه‌چیز را می‌شکند. bitmap داخلی renderer با یک PixelFormat := pf24bit سخت‌کد شده در constructor PDFlibRenderer ایجاد می‌شود، و آن surface 24-bit با هر render pathی share می‌شود: export PNG، preview با device context، خروجی JPEG، همهٔ آن. اگر آن را در source به pf1bit تغییر دهید، یک feature monochrome اضافه نکرده‌اید، بلکه fidelity رنگ را برای هر caller در library پایین آورده‌اید و خودتان را برای debug کردن دوازده regression downstream ثبت کرده‌اید

پس RenderPageToMonochromeFile مسیر opposite را برمی‌گزیند. page را به‌طور معمول، به یک BMP موقت 24-bit، render می‌کند، و فقط بعد آن را در یک post-processing step به 1-bit فرو می‌کاهد. renderer دست‌نخورده می‌ماند. رفتار monochrome کاملاً در method convenience زندگی می‌کند، و این یعنی نمی‌تواند روی کسی که آن را صدا نمی‌زند تأثیر بگذارد. این همان نوع trade-offی است که ارزش دارد صریح نام برده شود: یک post-process یک allocation bitmap اضافه و یک temp file پرداخت می‌کند، و در عوض یک core باربر را کاملاً از scope خارج نگه می‌دارد. برای featureی که برای fax و archive edge caseها وجود دارد، این سمتِ ledger درست است

خط لوله PDF Library for Delphi که صفحه PDF به یک BMP موقت 24 بیتی رندر می‌شود، با یک blit گودی HALFTONE به بیت‌مپ تک‌رنگ 1 بیتی فشرده می‌شود و در گردش‌کارهای فکس، بایگانی و OCR مصرف می‌شود
روش‌های جدید ابتدا صفحه تمام‌رنگ را رندر می‌کنند، سپس رستر آماده را بیرون از رندرکننده down-convert می‌کنند. گیت‌وی‌های فکس و مخازن آرشیوی و فرانت‌اندهای OCR یک بیت‌مپ واقعی pf1bit دریافت می‌کنند
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI رزولوشن کلاسیک فکس Group 4 است؛ اندیس صفحه مبنا-1 است
    Pdf.RenderPageToMonochromeFile(200, 1, 'invoice-page1.bmp');
  finally
    Pdf.Free;
  end;
end;

این‌که down-conversion 1-bit واقعاً چگونه اتفاق می‌افتد

down-conversion به‌جای یک threshold loop دست‌نویس به GDI تکیه می‌کند، و این انتخاب برای کیفیت خروجی مهم است. داخل method، bitmap موقت 24-bit در یک TBitmap بارگذاری می‌شود، یک TBitmap دوم با PixelFormat := pf1bit در همان dimensionها ساخته می‌شود، و pixelها با یک blit واحد جابه‌جا می‌شوند:

PDF Library for Delphi: جزئیات فشرده‌سازی GDI در مقایسه stretch blit گودی HALFTONE که خاکستری‌ها را به الگوهای نقطه‌ای dither می‌کند با آستانه پیش‌فرض بلوکی BLACKONWHITE
درون RenderPageToMonochromeFile یک StretchBlt هر پیکسل را روی سطح pf1bit هم‌اندازه منتقل می‌کند. با فعال‌بودن HALFTONE، خاکستری‌ها به الگوهای نقطه جوهر dither شده تبدیل می‌شوند نه شکل‌های بلوکی که آستانه پیش‌فرض می‌سازد
// درون RenderPageToMonochromeFile، پس از بارگذاری ColorBmp 24-بیتی
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE به GDI می‌گوید منبع 24-بیتی را با dithering به 1-بیت فروبکاهد
SetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);
StretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,
  ColorBmp.Canvas.Handle, 0, 0, ColorBmp.Width, ColorBmp.Height, SRCCOPY);
MonoBmp.SaveToFile('out.bmp');

ترفند SetStretchBltMode همراه با HALFTONE است. هرچند source و destination هم‌اندازه‌اند، پس scaling رخ نمی‌دهد، modeِ stretch همچنان نحوهٔ map شدن colorها به palette 1-bit را در GDI تعیین می‌کند. HALFTONE باعث می‌شود halftone dithering اعمال شود و regionهای gray و لبه‌های text antialiased را به patternهایی از dotهای سیاه و سفید تبدیل کند، نه این‌که به نزدیک‌ترین یکی از دو color به‌صورت سخت clip کند. اگر آن call mode را حذف کنید، یا از default BLACKONWHITE استفاده کنید، content خاکستری به شکل‌های blockی و thresholdی posterize می‌شود. برای خروجی اسکن‌-سند و OCR-preprocessing، نتیجهٔ dithered تقریباً همیشه همان چیزی است که می‌خواهید

یک جزئیات غیرقابل‌مذاکره و آسان برای اشتباه وجود دارد: render موقت باید BMP باشد. RenderPageToMonochromeFile renderer عمومی را با یک options code برابر 0 صدا می‌زند، که BMP است. argument options در RenderPageToFile یک enum کوچک integer است، و valueها برای این منظور قابل‌جایگزینی نیستند: 0 BMP است، 1 JPEG، 2 WMF، 3 EMF، 5 PNG، و همین‌طور ادامه. down-converter سپس روی temp file TBitmap.LoadFromStream انجام می‌دهد. اگر به آن WMF بدهید، با پاس دادن 2، آن load «Bitmap image is not valid» برمی‌گرداند، چون یک Windows Metafile یک vector record stream است، نه یک DIB. down-conversion monochrome از ابتدا تا انتها یک raster operation است، پس intermediate باید یک raster format باشد

رندر کردن فقط یک sub-region از یک page

روش دوم، RenderPageRegionToFile، فقط یک rectangle از page را به‌جای کل آن render می‌کند. use caseها وقتی یک بار document viewer ساخته باشید آشنا هستند: بریدن یک signature block از یک contract، تولید یک tile برای یک map بزرگ که zoom شده است، یا بیرون کشیدن یک region مهرخورده برای یک thumbnail بدون این‌که مجبور شوید کل page را با DPI بالا rasterize کنید. signature ساده است:

PDF Library for Delphi: رندر کلیپ ناحیه در واحد point در PDF: مستطیل 72,72,180,72 روی صفحه PDF رندرشده با رزولوشن کامل به بیت‌مپ 375 در 150 پیکسل در 150 DPI تبدیل می‌شود چون کلیپ برش می‌زند نه مقیاس
RenderPageRegionToFile پنجره‌ای با اندازه‌گیری نقطه PDF از یک رندر تمام‌وضوح برش می‌زند. بیت‌مپ خروجی از width و height ضربدر DPI تقسیم بر 72 اندازه می‌گیرد، هرگز از یک صفحه کامل کوچک‌شده نه
// Clip به‌صورت "Left,Top,Width,Height" در واحد نقطه PDF است (72 pt = 1 اینچ)
// اینجا: یک جعبه 2.5×1 اینچی، یک اینچ از گوشه بالا-چپ صفحه فاصله دارد
Pdf.RenderPageRegionToFile(150, 1, '72,72,180,72', 'sig-block.bmp');

رشتهٔ clip چهار double جداشده با comma در PDF pointهاست، که درون method به‌صورت دستی parse می‌شود تا از quirks locale و DelimitedText دور بماند. از width و height، method اندازهٔ bitmap خروجی را به‌صورت Round(Width * DPI / 72) در Round(Height * DPI / 72) محاسبه می‌کند، یک bitmap pf24bit در memory دقیقاً با همان size تخصیص می‌دهد، و آن را از طریق RenderPageToDCClip در device context خودش render می‌کند. file نتیجه فقط rectangle بریده‌شده را دارد، با اندازهٔ region نه کل page

پارامتر clipی که هیچ کاری نمی‌کرد

اینجا جایی است که کار sharp‌تر از چیزی بود که به نظر می‌رسد. RenderPageToDCClip مدت زیادی یک Clip parameter با خود داشت، و دروغ بود. call argument را می‌پذیرفت، آن را به TPDFPageTree.RenderPageToDC می‌داد، و آن پیاده‌سازی آن را کاملاً نادیده می‌گرفت و هیچ‌وقت به renderer تحویلش نمی‌داد. می‌توانستید هر rectangleی را که دوست داشتید پاس بدهید و کل page را پس بگیرید. هر کسی که RenderPageToDCClip را با انتظار crop وصل کرده بود، یک render تمام‌صفحه می‌گرفت و بسته به layout خود شاید متوجه نمی‌شد

v3.83.0 این سیم را وصل کرد. RenderPageToDC اکنون همان "Left,Top,Width,Height" point rectangle را parse می‌کند و آن را به‌عنوان یک GDI clip region واقعی روی target device context قبل از این‌که renderer رسم کند اعمال می‌کند. conversion از point به pixel دستگاه همان scale factor معمول است، که روی هر چهار edge اعمال می‌شود. sequence اطراف render همان dance استاندارد save/clip/restore است:DPI / 72pairِ

// درون TPDFPageTree.RenderPageToDC، وقتی Clip خالی نیست
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer اینجا صفحه را رسم می‌کند ...
// در بلوک finally:
RestoreDC(TargetDC, -1);

SaveDC / RestoreDC(-1) همان چیزی است که این را برای callهای مکرر امن نگه می‌دارد: clip region روی state stackِ DC push می‌شود، page رسم می‌شود، و clip اصلی بدون توجه به این‌که render چگونه تمام می‌شود دوباره pop می‌شود. RestoreDC(TargetDC, -1) most recently saved state را restore می‌کند، که idiom استاندارد برای save/restore متوازن است. restore را فراموش کنید و callerی که همان DC را برای render تمام‌صفحهٔ بعدی reuse می‌کند آن را به‌طور مرموزی در region آخر clipped خواهد یافت. fix کردن parameter مرده، RenderPageRegionToFile را هم مجانی درست کرد، چون آن method جدید دقیقاً از همین path عبور می‌کند

clip crops می‌کند، نه scale. page هنوز در DPIی که خواسته‌اید، در موقعیت عادی خودش rasterize می‌شود، و clip region صرفاً هر چیزی بیرون از rectangle را discard می‌کند. شما region را برای پر کردن output zoom نمی‌کنید؛ در حال بریدن یک window از render با resolution کامل هستید. اگر region magnified می‌خواهید، DPI را بالا ببرید. coordinateهای rectangle در device space پس از scaling point-to-pixel تفسیر می‌شوند، با اندازه‌گیری از گوشهٔ بالا-چپ surface رندرشده، پس Left و Top خود را از بالای page به پایین برنامه‌ریزی کنید. برای یک tour عمیق‌تر از این‌که PDF Library for Delphi چگونه device context را برای خروجی روی صفحه هدایت می‌کند، مقالهٔ همراه دربارهٔ preview چاپ و خروجی device context همان plumbing DC را از سمت display بررسی می‌کند

مرز صادقانه: 1-bit BMP، نه G4 TIFF

آسان است که این را به‌عنوان «fax-ready output» بیش از حد بزرگ جلوه دهیم، پس حد را به‌روشنی بیان می‌کنیم. RenderPageToMonochromeFile pf1bit 1-bit BMP تولید می‌کند. این کار یک CCITT Group 4 TIFF تولید نمی‌کند، که همان فرمت مورد انتظار یک workflow واقعی fax یا معمولاً یک TIFF archive است. دلیل آن concrete است نه یک oversight: unit CCITT در PDF Library for Delphi در حال حاضر G4 streamها را decode می‌کند اما هیچ G4 encoder ندارد. بدون encoder جایی برای نوشتن runهای فشردهٔ monochrome وجود ندارد، پس مسیر monochrome در یک DIB 1-bit بدون فشرده‌سازی متوقف می‌شود

در عمل این هنوز مفید است. یک BMP 1-bit فرمت pixel درست است، dithered و آماده، و بیشتر toolchainهای fax، archive، یا OCR با خوشحالی آن را ingest می‌کنند یا در یک step downstream آن را خودشان به G4 تبدیل می‌کنند. اما اگر requirement شما literally یک Group 4 TIFF مستقیم از library است، این هنوز آن نیست، و باید یک compression stage خودتان را برنامه‌ریزی کنید. دانستن این‌که یک feature کجا متوقف می‌شود به اندازهٔ دانستن کارهایی که می‌کند ارزش دارد

هر دو method عمداً کوچک هستند، و این درس designی است که ارزش دارد از این page با خود ببرید: یک convenience API که روی یک renderer می‌نشیند می‌تواند capability واقعی اضافه کند، monochrome output، region cropping، بدون این‌که به rasterizer دست ببرد و همهٔ callerهای دیگر را destabilize کند. وقتی واقعاً لازم باشد برای rasterization زیربنایی بین engineها انتخاب کنید، مرور رندر PDF چند-engine در Delphi trade-offها را عمیق پوشش می‌دهد. برای دیدن surface کامل rendering و بقیهٔ API، صفحهٔ محصول PDF Library for Delphi Delphi PDF Library تصویر کامل را دارد