مقال تقني

تحويل صفحات PDF إلى صور JPEG في Delphi باستخدام PDFium VCL

تحويل صفحة PDF إلى JPEG هو عمليتان يميل الناس إلى جمعهما في خطوة واحدة ثم تصحيح كل واحدة منهما على حدة. أولًا تُحوّل الصفحة إلى bitmap بالبكسل عند دقة تختارها. ثم تُسلم هذه الخريطة النقطية إلى مُرمّز JPEG وتحدد الجودة. يتولى PDFium VCL النصف الأول عبر RenderPage، أما النصف الثاني فهو VCL عادي عبر TJPEGImage من Vcl.Imaging.jpeg. تكمن التفاصيل المهمة في الفاصل بينهما، لأن الدقة التي تختارها في جهة العرض والجودة التي تختارها في جهة الترميز تؤثر كل منهما في الأخرى وفي حجم الملف بطرق يسهل الخطأ فيها

ما ينبغي تثبيته في الذهن قبل أي سطر برمجي هو أن صفحة PDF لا تحتوي على بكسلات. إنها موصوفة بالنقاط، حيث تمثل النقطة الواحدة 1/72 من البوصة، والصفحة نفسها رسم متجهي مقاس بهذه النقاط. عندما تطلب من PDFium أن يعرض الصفحة، فأنت تختار عدد البكسلات التي سيُسقِط هذا الرسم عليها، وهذا الاختيار هو DPI. إذا أخطأت الحساب فستحصل إما على صورة مصغرة ضبابية عندما كنت تريد نسخة مناسبة للطباعة، أو على bitmap بحجم 200 مليون بكسل من أجل معاينة لا تتجاوز 120 بكسل

من DPI إلى أبعاد البكسل

RenderPage يحتاج إلى Width وHeight بالبكسل كقيم صحيحة، لا إلى DPI. لذلك تبدأ العملية بالتحويل. تعرض الصفحة حجمها بالنقاط عبر PageWidth وPageHeight وكلاهما من النوع Double، والتحويل هنا هو نفسه الذي تستخدمه كل أدوات التحويل النقطي: البكسلات تساوي النقاط مضروبة في DPI الهدف ثم مقسومة على 72. صفحة US Letter هي 612 في 792 نقطة. عند 150 DPI تصبح 1275 في 1650 بكسل، وعند 72 DPI تبقى 612 في 792، أي بكسل واحد لكل نقطة، وهو ما ينسى الناس أنه مجرد حالة المطابقة المباشرة

// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free;   // the function-form RenderPage hands you ownership

هناك تفصيلان في هذه الأسطر الأربعة يحددان ما إذا كان الكود صحيحًا. الأول أن النسخة الوظيفية من RenderPage تعيد TBitmap تملكه أنت. PDFium أنشأه ثم مضى، وإذا لم تنفذ Free في كل تكرار فإن معالجة مئات الصفحات ستسرّب مئات الـ bitmap ويتضخم الاستهلاك حتى يتعثر البرنامج. التفصيل الثاني هو وسيط Color، وهنا قيمته clWhite. صفحات PDF تُرسم عادة على أساس خلفية بيضاء غير شفافة، وإذا عرضت صفحة فيها شفافية فوق لون خلفية غير مناسب فستحصل على حواف موحلة أو هالات داكنة متفرقة. الأبيض هو الخيار الافتراضي المناسب تقريبًا لكل المستندات، أما هذا الوسيط فموجود للحالات النادرة التي لا ينطبق فيها ذلك

القيمتان 0, 0 هما الإزاحتان Left وTop داخل الصفحة في فضاء الإحداثيات بعد التحجيم، وتبقيهما على الصفر ما لم تكن تريد القص. أما ro0 فهو الدوران، وتركه على الصفر يجعل PDFium يحترم أي تدوير تذكره الصفحة أصلًا في إدخالها /Rotate، فتخرج الصفحة المؤلفة بوضع أفقي كما هي من دون أي تدخل منك

ترميز bitmap بصيغة JPEG

بعد توفر bitmap تصبح JPEG هي الجزء السهل، وهي Delphi خالصة. يقوم TJPEGImage.Assign بنسخ bitmap إلى الداخل، وتحدد CompressionQuality الجودة على مقياس من 1 إلى 100، ثم يكتب SaveToFile الملف. القاعدة الوحيدة هنا هي أن تضبط الجودة قبل الحفظ، لأنها تتحكم في عملية الترميز التي يبدأها SaveToFile

uses
  Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;

procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
  const FileName: string);
var
  Bitmap: TBitmap;
  Jpeg: TJPEGImage;
begin
  Pdf.PageNumber := PageNumber;
  Bitmap := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Dpi / 72),
    Round(Pdf.PageHeight * Dpi / 72),
    ro0, [], clWhite);
  try
    Jpeg := TJPEGImage.Create;
    try
      Jpeg.Assign(Bitmap);
      Jpeg.CompressionQuality := Quality;   // 1..100
      Jpeg.SaveToFile(FileName);
    finally
      Jpeg.Free;
    end;
  finally
    Bitmap.Free;
  end;
end;

يبدو هذا التداخل في try/finally متكلفًا لمساعد خاص بصفحة واحدة، لكنه صحيح تمامًا عند المعالجة على دفعات. الكتلة الداخلية تحرر المُرمِّز، والخارجية تحرر bitmap، وأي استثناء يقع في أي منهما يحرر ما يملكه ذلك الجزء. إذا دمجتهما في كتلة واحدة فقد يعلق bitmap إذا وقع استثناء أثناء الترميز. وعلى مدى تشغيل طويل يكون هذا الفارق هو الفرق بين محول ينهي العمل وآخر يتوقف عند الصفحة 300 مع ملف تالف ورسالة نقص في الذاكرة

اختيار DPI والجودة معًا

المقبضان هنا غير مستقلين عن هدف المخرجات، والخطأ الشائع هو رفعهما معًا بدافع الحذر. صورة مصغرة للويب تُعرض عند 300 DPI وتُحفظ بجودة 95 تصبح عدة مئات من الكيلوبايتات تتظاهر بأنها صورة بعرض 120 بكسل، ثم يتخلص المتصفح تقريبًا من كله عند التصغير. طابق الدقة مع عدد البكسلات الذي يحتاجه الناتج فعلًا، ثم اختر جودة تتحمل ضغط JPEG الفاقد من دون آثار مرئية

المخرجDPIجودة JPEG
صورة مصغرة للقائمة7260-70
معاينة على الشاشة96-15080-85
عرض عالي التفاصيل200-30085-95
نسخة للطباعة300-60090-100

جودة JPEG تستحق التحذير وحدها. فهي ليست قرص ضبط خطيًا. القفزة من 70 إلى 85 تمنح تحسنًا بصريًا حقيقيًا مقابل زيادة معتدلة في الحجم؛ أما القفزة من 95 إلى 100 فتضاعف الملف تقريبًا مقابل فرق لا يلاحظه معظم الناس، لأن الجودة 100 ليست بلا فقدان، بل تتوقف فقط عن حذف كثير من البيانات. في الصفحات الكثيفة النصوص تمسح آلية الضغط الكتلية في JPEG الحواف الحادة للأحرف إلى أثر تموّج خافت، ولهذا تبدو النصوص الممسوحة أقل وضوحًا عندما تنخفض الجودة إلى ما دون نحو 80. إذا كانت الصفحات في معظمها نصًا ويمكنك تغيير الصيغة، فإن PNG يعرض هذا النص من دون ذلك التموج، أما JPEG فيبرر وجوده مع المحتوى الفوتوغرافي والمختلط حيث يكون ضغطه أصغر فعلًا

صور مصغرة أسرع وأصغر

عندما يكون الهدف صورة مصغرة لا نسخة مطابقة تمامًا، يمكنك أن تطلب من المُعرِض أن يعمل أقل. الوسيط Options يقبل مجموعة من الوسوم TRenderOption، وبعضها يقايض الدقة مقابل السرعة بالطريقة التي تحتاجها المعاينة الصغيرة. الوسم reGrayscale يزيل اللون، وهذا يسرّع العرض ويعطي bitmap أصغر للترميز. أما reNoSmoothImage وreNoSmoothPath فهما يتجاوزان تنعيم الحواف الذي لن يكون مرئيًا أصلًا على حجم صورة مصغرة

function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Fit the page inside MaxW x MaxH while preserving aspect ratio.
  Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
  Result := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Scale),
    Round(Pdf.PageHeight * Scale),
    ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;

تُظهر حالة الصورة المصغرة أيضًا الطريقة الأنظف للتفكير في التحجيم. بدل المرور عبر DPI، احسب عامل قياس واحدًا يضع الصفحة داخل صندوق محدد ويحافظ على نسبة الأبعاد، وهذا ما يفعله Min بين النسبتين. صفحة عمودية وأخرى أفقية تنتهيان كلتاهما داخل الصندوق نفسه من دون تشويه، ولن تضطر أبدًا إلى التفكير في DPI الذي يعادل "الملاءمة داخل 200 في 280". وهناك ملاحظة على reGrayscale: فهو يحول المحتوى النقطي إلى درجات الرمادي، لكن التعبئات المتجهية والنصوص تحتفظ بقيمها اللونية داخل المحرك، لذلك قد تعود الصفحة التي يغلب عليها الفن المتجهي أقل أحادية اللون مما يوحي به اسم الوسم. وللحصول على نتيجة رمادية كاملة فعلية فإن المسار الموثوق هو GrayscalePdfBitmap بعد العرض كما في الملخص المرجعي

تجميع المستند كله

تجميع ذلك في مستند كامل يعني حلقة عبر PageCount مع تحريك PageNumber صفحة واحدة في كل مرة. الصفحات هنا تبدأ من 1، أي أن الصفحة الأولى هي PageNumber := 1، وتدور الحلقة حتى PageCount شاملًا، لا حتى PageCount - 1. وهناك أمر آخر يجب أن يلتزم به التنفيذ الدفعي، وهو عقد التحميل الصامت. تعيين Active := True لا يرفع استثناء أبدًا مع ملف تالف أو كلمة مرور خاطئة، بل يترك Active على False. افحص هذه الحالة قبل عرض أي صفحة، وإلا فستعمل أول RenderPage على مستند لم يُفتح أصلًا

procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
  Pdf: TPdf;
  I, Digits: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := PdfPath;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.Create('Could not open ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // zero-pad so files sort right
    for I := 1 to Pdf.PageCount do
      SavePageAsJpeg(Pdf, I, Dpi, Quality,
        Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

تعبئة الأرقام بعدد الخانات عبر Digits أمر صغير لكنه يوفر عليك وقتًا لاحقًا. إذا سميت الملفات page_1.jpg حتى page_10.jpg فإن أي أداة ترتبها كنصوص ستضع page_10 مباشرة بعد page_1 فتختل الترتيب. أما التعبئة بعرض أكبر رقم صفحة، بحيث ينتج عن مستند من 300 صفحة اسم مثل page_001.jpg, فتبقي الترتيب المعجمي وترتيب الصفحات متطابقين في كل المراحل اللاحقة

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

هناك ازدواجية أخيرة تستحق المعرفة. تحويل الصفحة إلى bitmap يسقط طبقة النص منها، فتصبح JPEG مجرد بكسلات، ولم تعد الكلمات قابلة للتحديد أو البحث. عندما تحتاج إلى الصورة والنص الأساسي معًا، اعرض الصورة ثم استخرج النص بصورة منفصلة، وهو ما تغطيه المقالة المرافقة عن استخراج النص من مستندات PDF باستخدام PDFium VCL. أما أوضاع RenderPage وخيارات العرض المعروضة هنا فهي جزء من مكوّن PDFium VCL لـ Delphi وC++Builder