تكون إحداثيات PDF بوحدة النقاط، وتكون إحداثيات الطابعة بوحدات الجهاز، ولا علاقة مباشرة بينهما حتى تحوّلها عمداً. هذا التباين هو أصل معظم مخرجات الطباعة السيئة في تطبيقات Delphi: فالكود يرسل الملف الصحيح، لكن الصفحة تخرج مقصوصة أو ممدودة أو فارغة. يتولى PDFium VCL جانب العرض بوضوح، أما توصيل الطابعة فهو VCL قياسي. ويتكامل الجانبان مع مقدار معقول من الشيفرة متى فهمت ما يتوقعه كل جانب.
كيف تعمل سلسلة العرض ثم الطباعة
لا يتحدث PDFium VCL مع الطابعات مباشرة. النمط هو: اعرض الصفحة إلى TBitmap بالدقة التي تريدها، ثم انقل تلك الصورة إلى لوحة الطابعة بواسطة StretchDIBits. تعيد TPdf.RenderPage صورة يملكها المستدعي، لذلك تتحكم أنت في أبعاد البكسل. مرّر [rePrinting] في مجموعة الخيارات فيحوّل PDFium مسار العرض إلى مسار يستبعد المؤثرات الخاصة بالشاشة مثل تحسين الحواف الفرعي لشاشات LCD، ويتعامل مع MediaBox الخاص بالصفحة على نحو صحيح لمخرجات الطباعة. إذا تركت rePrinting خارجاً، فسترسل إلى الطابعة عرض شاشة، وهو يبدو جيداً على الشاشة لكنه يميل إلى إنتاج مخرجات أنعم على الطابعات عالية الدقة لأن قرارات تحسين الحواف المصممة لشاشات 96 DPI لا تناسب الطباعة عند 300 أو 600 DPI.
TPdf.Active هو البوابة الوحيدة التي ينبغي التحقق منها قبل لمس أي خاصية صفحة. يبتلع المكوّن أخطاء التحميل بصمت: فتعريف Active := True على ملف تالف أو محمي بكلمة مرور لا يرمي استثناء، بل يترك Active على False. تحقّق منه دائماً بعد الإسناد. إن قراءة PageCount أو PageWidth على مستند غير نشط تعيد صفراً، وهذا يولد عمليات لا تُنفَّذ بصمت ويصعب تشخيصها عندما تصل إلى مجمّع الطباعة.
حلقة طباعة مصغرة
أبسط حالة عاملة تحمل ملفاً، وتفتح مهمة طباعة، وتكرر الصفحات، ثم تغلقها. التفصيل الوحيد الذي يحتاج انتباهاً هو أن Printer.NewPage يجب ألا يستدعى قبل الصفحة الأولى، لذلك نستخدم الراية FirstPage. يمر نقل StretchDIBits عبر GetDIBSizes وGetDIB لسحب البتات المستقلة عن الجهاز من مقبض الصورة، ثم يرسمها على لوحة الطابعة بالحجم الكامل للصفحة:
procedure PrintPdfFile(const FileName: string);
var
Pdf: TPdf;
I: Integer;
Bitmap: TBitmap;
InfoHeaderSize, ImageSize: DWORD;
InfoHeader: PBitmapInfo;
Image: Pointer;
FirstPage: Boolean;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
if not Pdf.Active then
Exit; // load failed silently; bail out
Printer.Title := Pdf.Title;
Printer.BeginDoc;
try
FirstPage := True;
for I := 1 to Pdf.PageCount do
begin
if FirstPage then
FirstPage := False
else
Printer.NewPage;
Pdf.PageNumber := I;
// Render at printer resolution; rePrinting adjusts the render path
Bitmap := Pdf.RenderPage(
0, 0,
Printer.PageWidth,
Printer.PageHeight,
ro0,
[rePrinting]
);
try
GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);
InfoHeader := AllocMem(InfoHeaderSize);
try
Image := AllocMem(ImageSize);
try
GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);
StretchDIBits(
Printer.Canvas.Handle,
0, 0, Printer.PageWidth, Printer.PageHeight,
0, 0, Bitmap.Width, Bitmap.Height,
Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY
);
finally
FreeMem(Image);
end;
finally
FreeMem(InfoHeader);
end;
finally
Bitmap.Free;
end;
end;
finally
Printer.EndDoc;
end;
finally
Pdf.Active := False;
Pdf.Free;
end;
end;
إن تمرير Printer.PageWidth وPrinter.PageHeight كأبعاد للصورة يعني أنك تعرض بالحجم البكسلي الأصلي للطابعة، وهو ما يأخذ بالفعل كثافة الجهاز في الحسبان. ثم يربط استدعاء StretchDIBits تلك البكسلات على نحو 1:1 بالصفحة. يمنحك ذلك أفضل دقة ممكنة من دون أي حسابات صريحة لكثافة النقاط، لكنه يعمل فقط عندما يكون حجم صفحة PDF وحجم الورق الفعلي متطابقين. وعندما يختلفان، تحتاج إلى تحجيم صريح.
التحجيم عندما يختلف حجم الصفحة عن حجم الورق
إن صفحة PDF بحجم A4 بوضع رأسي لا تلائم تلقائياً طابعة US Letter، وستُقص الصفحة الأفقية المرسلة إلى طابعة بوضع رأسي. الأسلوب القياسي هو حساب عامل تحجيم موحد من نسبة بكسلات الطابعة إلى نقاط PDF، ثم تطبيقه على البعدين للحفاظ على نسبة الأبعاد. تعرض Pdf.PageWidth وPdf.PageHeight أبعاد الصفحة الحالية بالنقاط، حيث إن النقطة الواحدة تساوي 1/72 بوصة. ويحوّل الضرب في DPI مستهدف والقسمة على 72 إلى بكسلات عند تلك الدقة. خذ Min من نسبتي X وY للحصول على أكبر تحجيم ما يزال ملائماً داخل مساحة الطباعة:
// Fit PDF page to printable area, preserving aspect ratio
var
ScaleX, ScaleY, Scale: Double;
DestWidth, DestHeight: Integer;
Dpi: Integer;
begin
Dpi := 300; // target render resolution
Pdf.PageNumber := PageIndex;
ScaleX := Printer.PageWidth / (Pdf.PageWidth * Dpi / 72);
ScaleY := Printer.PageHeight / (Pdf.PageHeight * Dpi / 72);
Scale := Min(ScaleX, ScaleY);
// Clamp to 1.0 for shrink-to-fit only (no enlargement)
if Scale > 1.0 then Scale := 1.0;
DestWidth := Round(Pdf.PageWidth * Dpi / 72 * Scale);
DestHeight := Round(Pdf.PageHeight * Dpi / 72 * Scale);
Bitmap := Pdf.RenderPage(0, 0, DestWidth, DestHeight, ro0,
[rePrinting, reAnnotations]);
// ... transfer with StretchDIBits as above
end;
تناسِب Dpi = 300 معظم الطابعات المكتبية. وعند 600 DPI، تصل صورة صفحة A4 واحدة إلى نحو 34 مليون بكسل، أي ما يقارب 100 MB بوصفها صورة 32-bit؛ أما التحسن في جودة المستندات النصية العادية فطفيف، وتكلفة الذاكرة لكل صفحة كبيرة. احتفظ بـ 600 DPI لمطابع الطباعة أو الرسومات التقنية الثقيلة بالمتجهات حيث يكون الأمر مهمّاً فعلاً.
تكون الراية reAnnotations في كتلة الشيفرة الثانية مستقلة عن rePrinting. أدرجها عندما يتوقع المستخدم ظهور الأختام والتمييزات ومربعات التعليقات على الورق. واحذفها لمخرجات المحتوى فقط. ويمكن دمج الرايتين بحرية.
تدوير الصفحة
يخزن PDFium تدوير الصفحة في PDF على هيئة إدخال /Rotate، ويمكن الوصول إليه عبر Pdf.PageRotation، الذي يعيد قيمة TRotation (ro0، ro90، ro180، ro270). يعكس نظام إحداثيات الطابعة تدوير 90 و270 درجة مقارنة بالشاشة. إذا مررت قيمة PageRotation الخام مباشرةً إلى RenderPage من دون أي تعديل، فسوف تُطبع الصفحات الأفقية المضمّنة في مستند رأسي مقلوبة في معظم برامج تشغيل طابعات Windows. الحل بسيط قبل استدعاء العرض: حوّل ro90 إلى ro270 وأعد ro270 إلى ro90، واترك ro0 وro180 من دون تغيير.
تحقّق من هذا السلوك على الطابعة المستهدفة نفسها قبل الإطلاق. فالسلوك الخاص بالتدوير ليس موحداً بين الموردين، وبعض برامج التشغيل تطبق تصحيحها الخاص للتدوير على مستوى GDI. إذا رأيت تدويراً مزدوجاً فاحذف التبديل، وإذا رأيت عدم وجود تصحيح على الإطلاق فأضفه. إن مستنداً مختلط الاتجاهات تتناوب فيه الصفحات الرأسية والأفقية هو أسرع وسيلة لاكتشاف أي من نمطي الفشل أثناء الاختبار.
إدارة الذاكرة عبر مهمة طباعة طويلة
كل استدعاء إلى RenderPage ينشئ TBitmap جديداً يملكه المستدعي ويجب تحريره. في الحلقة أعلاه، تتكفل كتلة try/finally Bitmap.Free بذلك على نحو صحيح صفحة بصفحة. لا تجمع الصور النقطية عبر الصفحات: فعرض مستند من 200 صفحة عند 300 DPI سيستهلك عدة غيغابايت قبل أن تصل الصفحة الأولى إلى مجمّع الطباعة. حرر كل صورة قبل الانتقال إلى الصفحة التالية.
تتبع مجموعة AllocMem / FreeMem داخل كتلة النقل القاعدة نفسها. يخبرك GetDIBSizes بمقدار الذاكرة التي يحتاجها رأس DIB وبيانات البكسل؛ فتقوم بالحجز والملء والطباعة والتحرير كلها ضمن نطاق صفحة واحدة. وأي تسرب في أي من الكتلتين سيؤدي إلى استنفاد ذاكرة العملية في مستندات أطول من بضع عشرات الصفحات.
إذا احتجت إلى تشغيل مهام الطباعة في خيط خلفي، فأبقِ TPdf وجميع استدعاءات VCL الخاصة بالطابعة في الخيط نفسه. إن TPdf نفسه ليس آمناً للاستخدام عبر خيوط متعددة عند مشاركة الحالة العالمية لمكتبة PDFium بين النسخ، وأأمن نموذج هو TPdf واحد لكل خيط، يحمّل كل منها نسخته الخاصة من الملف.
تعد واجهة العرض وواجهة المستند المعروضة هنا جزءاً من مكوّن PDFium VCL لـ Delphi و C++Builder.