مقاله فنی

رندر صفحه‌های 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 کجا باید انجام شود، و پاسخ در PDFlibPas در نهایت چیز مفیدی دربارهٔ این‌که چگونه یک renderer را که ترجیح می‌دهید از نو ننویسید گسترش بدهید، می‌گوید

PDFlibPas یک کتابخانهٔ بومی 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 درست است

var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // 200 DPI is the classic Group 4 fax resolution; page index is 1-based
    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 واحد جابه‌جا می‌شوند:

// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmp
MonoBmp.PixelFormat := pf1bit;
MonoBmp.Width  := ColorBmp.Width;
MonoBmp.Height := ColorBmp.Height;
// HALFTONE tells GDI to dither the 24-bit source down to 1-bit
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 ساده است:

// Clip is "Left,Top,Width,Height" in PDF points (72 pt = 1 inch)
// Here: a 2.5in x 1in box, one inch in from the top-left of the page
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ِ

// inside TPDFPageTree.RenderPageToDC, when Clip is non-empty
ScaleFactor := DPI / 72;
SaveDC(TargetDC);
IntersectClipRect(TargetDC,
  Round(ClipLeft * ScaleFactor),
  Round(ClipTop * ScaleFactor),
  Round((ClipLeft + ClipWidth) * ScaleFactor),
  Round((ClipTop + ClipHeight) * ScaleFactor));
// ... renderer draws the page here ...
// in the finally block:
RestoreDC(TargetDC, -1);

/ SaveDC همان چیزی است که این را برای callهای مکرر امن نگه می‌دارد: clip region روی state stackِ DC push می‌شود، page رسم می‌شود، و clip اصلی بدون توجه به این‌که render چگونه تمام می‌شود دوباره pop می‌شود. RestoreDC(-1) most recently saved state را restore می‌کند، که idiom استاندارد برای save/restore متوازن است. restore را فراموش کنید و callerی که همان DC را برای render تمام‌صفحهٔ بعدی reuse می‌کند آن را به‌طور مرموزی در region آخر clipped خواهد یافت. fix کردن parameter مرده، RestoreDC(TargetDC, -1) را هم مجانی درست کرد، چون آن method جدید دقیقاً از همین path عبور می‌کند.RenderPageRegionToFileیک point رفتاری که باید درونی شود: 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 رندرشده، پس و خود را از بالای page به پایین برنامه‌ریزی کنید. برای یک tour عمیق‌تر از این‌که PDFlibPas چگونه device context را برای خروجی روی صفحه هدایت می‌کند، مقالهٔ همراه دربارهٔ Leftpreview چاپ و خروجی device contextTop همان plumbing DC را از سمت display بررسی می‌کند.مرز صادقانه: 1-bit BMP، نه G4 TIFFآسان است که این را به‌عنوان «fax-ready output» بیش از حد بزرگ جلوه دهیم، پس حد را به‌روشنی بیان می‌کنیم

یک

1-bit BMP تولید می‌کند. این کار یک CCITT Group 4 TIFF تولید نمی‌کند، که همان فرمت مورد انتظار یک workflow واقعی fax یا معمولاً یک TIFF archive است. دلیل آن concrete است نه یک oversight: unit CCITT در PDFlibPas در حال حاضر G4 streamها را decode می‌کند اما هیچ G4 RenderPageToMonochromeFile ندارد. بدون encoder جایی برای نوشتن runهای فشردهٔ monochrome وجود ندارد، پس مسیر monochrome در یک DIB 1-bit بدون فشرده‌سازی متوقف می‌شود.pf1bitدر عمل این هنوز مفید است. یک 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،

ستPDFlibPas Delphi PDF Library تصویر کامل را دارد.PDFlibPas Delphi PDF Library صفحهٔ محصول تصویر کامل را دارد