مقال تقني

طباعة مستندات PDF باستخدام PDFium Component في Delphi

تكون إحداثيات PDF بوحدة النقاط، وتكون إحداثيات الطابعة بوحدات الجهاز، ولا علاقة مباشرة بينهما حتى تحوّلها عمداً. هذا التباين هو أصل معظم مخرجات الطباعة السيئة في تطبيقات Delphi: فالكود يرسل الملف الصحيح، لكن الصفحة تخرج مقصوصة أو ممدودة أو فارغة. يتولى PDFium Component جانب العرض بوضوح، أما توصيل الطابعة فهو VCL قياسي. ويتكامل الجانبان مع مقدار معقول من الشيفرة متى فهمت ما يتوقعه كل جانب

كيف تعمل سلسلة العرض ثم الطباعة

لا يتحدث PDFium Component مع الطابعات مباشرة. النمط هو: اعرض الصفحة إلى TBitmap بالدقة التي تريدها، ثم انقل تلك الصورة إلى لوحة الطابعة بواسطة StretchDIBits. تعيد TPdf.RenderPage صورة يملكها المستدعي، لذلك تتحكم أنت في أبعاد البكسل. مرّر [rePrinting] في مجموعة الخيارات فيحوّل PDFium مسار العرض إلى مسار يستبعد المؤثرات الخاصة بالشاشة مثل تحسين الحواف الفرعي لشاشات LCD، ويتعامل مع MediaBox الخاص بالصفحة على نحو صحيح لمخرجات الطباعة. إذا تركت rePrinting خارجاً، فسترسل إلى الطابعة عرض شاشة، وهو يبدو جيداً على الشاشة لكنه يميل إلى إنتاج مخرجات أنعم على الطابعات عالية الدقة لأن قرارات تحسين الحواف المصممة لشاشات 96 DPI لا تناسب الطباعة عند 300 أو 600 DPI

TPdf.Active هو البوابة الوحيدة التي ينبغي التحقق منها قبل لمس أي خاصية صفحة. يبتلع المكوّن أخطاء التحميل بصمت: فتعريف Active := True على ملف تالف أو محمي بكلمة مرور لا يرمي استثناء، بل يترك Active على False. تحقّق منه دائماً بعد الإسناد. إن قراءة PageCount أو PageWidth على مستند غير نشط تعيد صفراً، وهذا يولد عمليات لا تُنفَّذ بصمت ويصعب تشخيصها عندما تصل إلى مجمّع الطباعة

مسار التصيير ثم الطباعة في Delphi مع PDFium Component: تُصيَّر صفحة PDF المحمّلة عبر TPdf.RenderPage بعلم rePrinting إلى TBitmap بحجم طابعة، ثم ينقلها StretchDIBits إلى لوحة الطابعة، مقارنةً بالمخرج الناعم المصيَّر شاشةً عندما يُغفل rePrinting
لا يتحدث PDFium Component إلى قائمة الطباعة مباشرة أبداً. ينتج RenderPage الصورة النقطية، وينقلها StretchDIBits إلى لوحة الطابعة، وينتقي rePrinting مسار التصيير المعدّ للورق

حلقة طباعة مصغرة

أبسط حالة عاملة تحمل ملفاً، وتفتح مهمة طباعة، وتكرر الصفحات، ثم تغلقها. التفصيل الوحيد الذي يحتاج انتباهاً هو أن 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;  // فشل التحميل بصمت؛ أوقف التنفيذ

    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;

        // ارسم بالدقة الطابعة؛ يضبط rePrinting مسار الرسم
        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 للحصول على أكبر تحجيم ما يزال ملائماً داخل مساحة الطباعة:

تحجيم الملاءمة-إلى-الصفحة لطباعة PDF في Delphi: تجلس صفحة PDF بـ 595×842 نقطة مصغّرة المقياس داخل المنطقة القابلة للطباعة من بكسلات الطابعة، محوَّلة عند النقاط مضروبة في Dpi على 72، ومقاسة بمصغّر نسبتَي العرض والارتفاع، ومقيّدة إلى التصغير-للملاءمة، ومحجمة في الصورة النقطية الوجهة
تصير النقاط بكسلات عند النقاط مضروبةً في Dpi مقسومةً على 72، وتفوز النسبة الأصغر فلا يُقص شيء. والحصر عند 1.0 يُبقي الملاءمة تصغيراً للملاءمة فقط
// لاءِم صفحة PDF مع المساحة القابلة للطباعة مع الحفاظ على aspect ratio
var
  ScaleX, ScaleY, Scale: Double;
  DestWidth, DestHeight: Integer;
  Dpi: Integer;
begin
  Dpi := 300;  // دقة الرسم المستهدفة
  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]);
  // ... انقل باستخدام StretchDIBits كما سبق ...
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 Component لـ Delphi و C++Builder