مقال تقني

عرض صفحات PDF إلى أحادي اللون 1-بت في Delphi

لا تريد بوابة الفاكس عرض صفحتك بعمق 24 بت. ولا تفعل ذلك أيضًا سلسلة الأرشفة التي تخزن مليون فاتورة تبدو ممسوحة ضوئيًا، ولا الواجهة الأمامية لـ OCR التي تحوّل كل شيء إلى أسود وأبيض قبل أن تنظر حتى إلى حرف واحد. الثلاثة جميعًا يريدون الشيء نفسه: خريطة نظيفة بعمق 1-بت، بت واحد لكل بكسل، حيث كل نقطة إما حبر أو ورق. إذا أعطيتهم BMP ملونًا كاملًا فسيرمون 23 بت لكل بكسل على أي حال، وغالبًا مع تمريرة توزيع ألوان أسوأ مما كنت ستفعله بنفسك. السؤال المهم هو أين يجب أن يحدث هذا التخفيض، والجواب في PDFlibPas يثبت أنه يقول شيئًا مفيدًا عن كيفية توسيع محرك عرض لا تريد إعادة كتابته

PDFlibPas هي مكتبة PDF أصلية بلغة Object Pascal لـ Delphi وC++Builder. ومحرك العرض فيها يرسم الصفحة إلى خريطة بكسلية ويمكنه إخراج BMP وPNG وJPEG وWMF وعددًا قليلًا من الصيغ الأخرى. لكن ما لم يكن يفعله حتى وقت قريب هو إعادة خريطة أحادية اللون حقيقية، أو عرض جزء من الصفحة فقط. كلاهما وصل في v3.83.0، وكلاهما بُني كطبقات ملاءمة رفيعة فوق العارض الموجود بدلًا من أن يكون تغييرات في الراستر نفسه. ذلك القيد هو القصة كلها

لماذا التخفيض بعد العرض، لا داخله

الطريقة الواضحة لإنتاج صورة 1-بت هي أن تطلب من محرك الراستر أن يرسم بعمق 1-بت. وهذه أيضًا الطريقة التي تكسر كل شيء آخر. الخريطة الداخلية للعارض تُنشأ مع <code>PixelFormat := pf24bit</code> ثابتًا داخل المُنشئ <code>PDFlibRenderer</code>، وذلك السطح 24-بت مشترك بين كل مسارات العرض: تصدير PNG، والمعاينة عبر device context، وإخراج JPEG، وكل شيء. إذا قلبته إلى <code>pf1bit</code> في المصدر فأنت لم تضف ميزة أحادية اللون، بل أفسدت دقة الألوان لكل من يستدعي المكتبة ووقعت على نفسك مهمة تتبع عشرات الانحدارات اللاحقة.PixelFormat := pf24bitلذلك يأخذ <code>RenderPageToMonochromeFile</code> المسار المعاكس. فهو يعرض الصفحة بشكل عادي إلى BMP مؤقت بعمق 24 بت، ثم يهبط بها إلى 1-بت كخطوة معالجة لاحقة. العارض نفسه لا يُمس. السلوك الأحادي اللون يعيش بالكامل داخل طريقة الملاءمة، وهذا يعني أنه لا يمكنه التأثير في أي أحد لا يستدعيه. هذا هو النوع من المقايضة الذي يستحق أن يُسمى صراحة: المعالجة اللاحقة تدفع ثمناً إضافيًا يتمثل في تخصيص خريطة بكسلية إضافية وملف مؤقت، وفي المقابل تُبقي نواة أساسية حاملة للحِمل خارج النطاق تمامًا. بالنسبة إلى ميزة موجودة لخدمة حالات الفاكس والأرشفة الطرفية، هذا هو الجانب الصحيح من الميزان.PDFlibRendererكيف يحدث الهبوط إلى 1-بت فعليًاpf1bitالتحويل إلى الأسفل يعتمد على GDI بدلًا من حلقة عتبة مكتوبة يدويًا، وهذا الاختيار مهم لجودة المخرجات. داخل الطريقة تُحمّل الخريطة المؤقتة 24-بت إلى <code>TBitmap</code>، ثم تُنشأ <code>TBitmap</code> ثانية مع <code>PixelFormat := pf1bit</code> على الأبعاد نفسها، وتنتقل البكسلات عبر blit واحد:

// inside RenderPageToMonochromeFile, after loading the 24-bit ColorBmpRenderPageToMonochromeFileMonoBmp.PixelFormat := pf1bit;

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;

MonoBmp.Width := ColorBmp.Width;

MonoBmp.Height := ColorBmp.Height;TBitmap// HALFTONE tells GDI to dither the 24-bit source down to 1-bitTBitmapSetStretchBltMode(MonoBmp.Canvas.Handle, HALFTONE);PixelFormat := pf1bitStretchBlt(MonoBmp.Canvas.Handle, 0, 0, MonoBmp.Width, MonoBmp.Height,

// 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');

السر هو <code>SetStretchBltMode</code> مع <code>HALFTONE</code>. حتى عندما يكون المصدر والوجهة بالحجم نفسه، فلا يحدث أي تحجيم، فإن نمط التمديد لا يزال يحدد كيف تطابق GDI الألوان إلى لوحة 1-بت. يجعل <code>HALFTONE</code> النظام يطبق halftone dithering، فيحوّل المناطق الرمادية وحواف النص المضادة للتسنين إلى أنماط من نقاط سوداء وبيضاء بدلًا من قصّها قصًا حادًا إلى أقرب واحد من لونين. إذا أسقطت استدعاء النمط، أو استخدمت الافتراضي <code>BLACKONWHITE</code>، فإن المحتوى الرمادي ينقلب إلى أشكال متدرجة على هيئة كتل. بالنسبة إلى مخرجات المستندات الممسوحة وضوئيًا ومرحلة ما قبل OCR، فإن النتيجة ذات التوزيع المبعثر هي غالبًا ما تريده.SetStretchBltModeمعلومة واحدة غير قابلة للتفاوض وسهلة الخطأ: يجب أن تكون عملية العرض المؤقتة BMP. <code>RenderPageToMonochromeFile</code> يستدعي العارض العام مع رمز خيارات يساوي <code>0</code>، وهو BMP. وسيط الخيارات في <code>RenderPageToFile</code> هو تعداد صحيح صغير، والقيم ليست قابلة للتبادل لهذا الغرض: <code>0</code> هو BMP، و<code>1</code> هو JPEG، و<code>2</code> هو WMF، و<code>3</code> هو EMF، و<code>5</code> هو PNG، وهكذا. بعد ذلك يجرى التحويل إلى الأسفل <code>TBitmap.LoadFromStream</code> على الملف المؤقت. إذا أرسلت له WMF بتمرير <code>2</code> فإن هذا التحميل يرمي <em>"Bitmap image is not valid"</em>، لأن Windows Metafile هو تدفق سجلات متجهية، لا DIB. التحويل الأحادي اللون عملية نقطية من البداية إلى النهاية، لذا يجب أن يكون الوسيط صيغة نقطية.HALFTONE. Even though source and destination are the same size, so no scaling happens, the stretch mode still governs how GDI maps colors into the 1-bit palette. HALFTONE makes it apply halftone dithering, turning gray regions and antialiased text edges into patterns of black and white dots rather than a hard clip to the nearest of two colors. Drop the mode call, or use the default BLACKONWHITE, and grayscale content posterizes into blocky thresholded shapes. For scanned-document and OCR-preprocessing output, the dithered result is almost always what you want

One detail is non-negotiable and easy to get wrong: the temporary render must be a BMP. RenderPageToMonochromeFile calls the general renderer with an options code of 0, which is BMP. The options argument on RenderPageToFile is a small integer enum, and the values are not interchangeable for this purpose: 0 is BMP, 1 JPEG, 2 WMF, 3 EMF, 5 PNG, and so on. The down-converter then does TBitmap.LoadFromStream on the temp file. Feed it a WMF, by passing 2, and that load throws "Bitmap image is not valid", because a Windows Metafile is a vector record stream, not a DIB. Monochrome down-conversion is a raster operation end to end, so the intermediate has to be a raster format

عرض منطقة فرعية من الصفحة

The second method, RenderPageRegionToFile, renders just a rectangle of the page instead of the whole thing. The use cases are familiar once you have built any document viewer: cropping a signature block out of a contract, generating a tile for a zoomed map of a large drawing, or pulling one stamped region for a thumbnail without paying to rasterize the entire page at high DPI. The signature is straightforward:

// 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');

سلسلة القص هي أربعة أعداد عشرية مفصولة بفواصل في نقاط PDF، وتحلل يدويًا داخل الطريقة لتفادي مشكلات الإعداد المحلي وDelimitedTextDelimitedTextRound(Width * DPI / 72). من العرض والارتفاع تحسب الطريقة حجم الخريطة الناتجة على أنه Round(Height * DPI / 72) في pf24bit، وتخصص خريطة بكسلية داخل الذاكرة بعمق RenderPageToDCClip بالحجم نفسه تمامًا، ثم ترسم إلى واصف جهازها عبر

العلم clip الذي لم يفعل شيئًا

هنا تصبح القصة أشد صرامة مما تبدو. RenderPageToDCClip كانت تحمل معاملاً لـ Clip منذ زمن طويل، وكان ذلك كذبًا. الاستدعاء يقبل الوسيط، ويمرره إلى TPDFPageTree.RenderPageToDC، وذلك التنفيذ كان يتجاهله بالكامل، من دون أن يمرره أبدًا إلى العارض. كان بإمكانك تمرير أي مستطيل يعجبك والحصول على الصفحة كاملة. كل من ربط RenderPageToDCClip وهو يتوقع قصًا كان يتلقى عرضًا لصفحة كاملة وقد لا يلاحظ ذلك، بحسب تخطيطه

وصل v3.83.0 السلك. RenderPageToDC الآن يحلل نفس مستطيل النقاط "Left,Top,Width,Height" ويطبقه كمنطقة قص حقيقية في GDI على واصف جهاز الهدف قبل أن يرسم العارض. التحويل من النقاط إلى بكسلات الجهاز هو عامل القياس المعتاد DPI / 72، مطبقًا على الحواف الأربع كلها. التسلسل حول العرض هو رقصة الحفظ/القص/الاستعادة المعتادة:

// 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);

زوج SaveDCSaveDCRestoreDC(-1)RestoreDC(-1)RestoreDC(TargetDC, -1) هو ما يبقي هذا آمنًا عند الاستدعاء المتكرر: تُدفع منطقة القص إلى مكدس حالة DC، تُرسم الصفحة، ثم تُستعاد منطقة القص الأصلية بغض النظر عن كيفية خروج العرض. RenderPageRegionToFileRestoreDC(TargetDC, -1)

استخدم مرة أخرى: clip يقص، ولا يُكبّر. الصفحة لا تزال تُرسم بالمعدل DPI الذي طلبته، وفي موضعها الطبيعي، ومنطقة القص تطرح ببساطة كل ما هو خارج المستطيل. أنت لا تكبّر المنطقة لتملأ المخرجات؛ بل تقطع نافذة من العرض الكامل. إذا أردت منطقة مكبّرة، فارفع DPI. تُفسَّر إحداثيات المستطيل في فضاء الجهاز بعد تحويل النقاط إلى بكسلات، مقاسة من الزاوية العلوية اليسرى للسطح المرسوم، لذا خطط لـ LeftLeftTop وTop من أعلى الصفحة إلى أسفلها. ولجولة أعمق في كيفية دفع PDFlibPas لوصف جهاز من أجل الإخراج على الشاشة، فإن القطعة المرافقة عن

معاينة الطباعة والإخراج عبر device context

It would be easy to oversell this as "fax-ready output," so here is the limit stated plainly. RenderPageToMonochromeFile produces a pf1bit BMP. It does not produce a CCITT Group 4 TIFF, which is the format a real fax workflow or a TIFF archive usually expects. The reason is concrete rather than an oversight: PDFlibPas's CCITT unit currently decodes G4 streams but has no G4 encoder. Without an encoder there is nowhere to write compressed monochrome runs, so the monochrome path stops at an uncompressed 1-bit DIB

In practice that is still useful. A 1-bit BMP is the correct pixel format, dithered and ready, and most fax, archival, or OCR toolchains will happily ingest it or convert it to G4 themselves with one downstream step. But if your requirement is literally a Group 4 TIFF straight out of the library, this is not that yet, and you should plan a compression stage of your own. Knowing where a feature stops is worth as much as knowing what it does

Both methods are deliberately small, and that is the design lesson worth carrying off this page: a convenience API that sits on top of a renderer can add real capability, monochrome output, region cropping, without reaching into the rasterizer and destabilizing every other caller. When you do need to pick between rendering engines for the underlying rasterization, the overview of multi-engine PDF rendering in Delphi covers the trade-offs in depth. To see the full rendering surface and the rest of the API, the تغطي المفاضلات بعمق. ولرؤية سطح العرض الكامل وبقية الـ API، فإن صفحة منتج PDFlibPas Delphi PDF Library