مقال تقني

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

كان لدى فريق معالجة المطالبات ثلاثون عامًا من الملفات الورقية تمر عبر ماسح ضوئي بآلية التغذية بالورق. كان الماسح يخرج ملف JPEG واحدًا لكل صفحة إلى مجلد، بأسماء 0001.jpg و0002.jpg وما إلى ذلك. لكن الأرشيف كان يحتاج فعليًا إلى ملف PDF واحد لكل ملف قضية، مع الصفحات بالترتيب، حتى يتمكن المراجع من فتح مستند واحد بدلًا من التنقل بين مئات الصور المصغرة. هذه الخطوة الأخيرة، تحويل رزمة مرقمة من المسوح إلى ملف PDF واحد مرتب، هي المهمة هنا

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

مسار دفعات في Delphi يستخدم استدعاءات AddPage و PageNumber و AddImage و SaveAs في PDFium Component ليحول مجلد مسوحات مرقمة إلى PDF مرتب واحد
يصبح كل مسح صفحة واحدة ينشئها AddPage ويوجّهها PageNumber قبل أن يرسمها AddImage؛ ويكتب SaveAs المستند المكتمل مرة واحدة

بنية التحويل

ثلاثة أشياء يجب أن تحدث لكل مسح. تحدد حجم الصفحة، وتضع الصورة داخل الصفحة مع ترك هامش، ثم تنتقل إلى الصفحة التالية. يوفّر PDFium Component طريقة لكل خطوة: AddPage تنشئ صفحة فارغة بالحجم المحدد، وAddImage (أو AddPicture إذا كانت لديك TPicture بالفعل) ترسم الصورة النقطية في الصفحة الحالية، وPageNumber يخبر المكون بالصفحة التي تستهدفها استدعاءات الرسم اللاحقة

التفصيل الذي يربك الناس هو نظام الإحداثيات. مساحة المستخدم في PDF تضع نقطة الأصل عند الزاوية اليسرى السفلية من الصفحة، ويزداد Y إلى الأعلى، بعكس إحداثيات الشاشة التي يعتاد عليها مطورو Delphi. الإحداثيان X, Y اللذان تمررهما إلى AddImage هما الزاوية اليسرى السفلية لمستطيل الصورة، أما Width, Height فهما حجم الموضع بالنقاط، لا حجم الملف المصدر بالبكسل. اقلب هذا الفهم وستنزل المسوح خارج الصفحة أو مقلوبة عما كنت تتوقع

إنشاء المستند وصفحة لكل مسح

ابدأ بمستند فارغ. CreateDocument يخصص PDF جديدًا ويجعل المكون نشطًا، لذا لا توجد خطوة فتح منفصلة. من هناك تمر على قائمة الملفات الممسوحة، ولكل ملف تضيف صفحة، وتجعلها الحالية، وتضع الصورة. أبعاد الصفحة هنا هي A4 بالنقاط (595 × 842 بوضع عمودي)، وهو الحجم القياسي للمراسلات المؤرشفة

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // عرض A4 بالنقاط
  PageH = 842.0;   // ارتفاع A4 بالنقاط
  Margin = 36.0;   // حد نصف بوصة حول كل مسح
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // جديد، فارغ، نشط بالفعل
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // فهرس صفحة يبدأ من 1
      Pdf.PageNumber := I + 1;                // اجعل الصفحة الجديدة الحالية
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

في كل دورة تُنشئ صفحة ثم تضبط PageNumber إليها مباشرة. هذا السطر الثاني مهم: AddPage يدرج الصفحة، لكن أوامر الرسم تعمل على الصفحة الحالية فقط، لذا فإن ضبط PageNumber هو ما يوجه AddImage إلى الصفحة التي أنشأتها للتو. إذا تجاهلت ذلك، ستتراكم صورك على الصفحة التي كانت محملة من قبل

يوجد افتراض واحد مخفي في تلك الحلقة، وهو ترتيب Files. الماسح يسمي الصفحات من 0001.jpg إلى 0100.jpg، لكن تعداد المجلد لا يعيدها دائمًا مرتبة، وعندما تصل إلى page9.jpg بجوار page10.jpg فإن فرزًا نصيًا بسيطًا يضع الصفحة 10 قبل الصفحة 9. فرّز القائمة صراحة قبل الحلقة، وفضّل أسماءً بصفر بادئ وقت المسح حتى يطابق الترتيب اللفظي ترتيب الصفحات. تسلسل الصفحات هو أول شيء يلاحظه المراجع، وهو أرخص خطأ يمكن منعه

وضع المسح والحفاظ على نسبة الأبعاد

نادراً ما يكون المسح بنفس شكل الصفحة. إذا مددته ليملأ الورقة ستشوّه النص، وإذا وضعته بحجمه البكسلي الكامل فسيتجاوز الحدود. الحل هو التحجيم وفق أصغر النسبتين، ملاءمة العرض أو ملاءمة الارتفاع، ثم توسيط ما يتبقى. ولأن نقطة الأصل تقع في أسفل اليسار، فإن التوسيط يعني تقسيم المساحة المتبقية بالتساوي وإضافتها إلى كل من X وY

مخطط صفحة A4 يوضح كيف يضع AddImage في PDFium Component مسحًا مصغَّر المقياس داخل الهوامش باستخدام كود Delphi وأصل أسفل اليسار
يأخذ AddImage الزاوية السفلى اليسرى لمستطيل الوضع، فيُحسب الملاءمة والتوسيط بنقاط الصفحة من نقطة الأصل
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP و JPG و PNG وغيرها عبر وحدات رسوميات VCL

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // ملاءمة داخل الهوامش دون تشويه المسح.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // توسيط: المساحة المتبقية تُقسَّم بالتساوي. تُقاس Y من أسفل الصفحة.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

يحمّل هذا الملف مرة واحدة لقراءة أبعاده بالبكسل، ويحسب عامل تحجيم موحّدًا واحدًا، ثم يمرر مستطيل التموضع إلى AddImage. يقبل AddImage مسار ملف مباشرة ويمرره عبر نفس مسار الصور الذي يستخدمه AddPicture، لذلك يعمل أي تنسيق تتعرف عليه وحدات رسومات VCL من دون معالجات خاصة. وإذا كانت الصورة مفكوكة الترميز بالفعل داخل TPicture من لوحة معاينة، فاستدعِ AddPicture(Pic, X, Y, DrawW, DrawH) بالمستطيل نفسه وتخطَّ القراءة الثانية للملف

تجاوز فك الترميز لمسوح JPEG

الماسحات تصدر JPEG في الغالب. تحميل JPEG إلى TPicture يفك ترميزه إلى صورة نقطية، ثم يعيد PDFium ترميزه عند الحفظ، وهما مرحلتان خسارتان لا تحتاج إليهما. AddJpegImage يضمّن البايتات المضغوطة الأصلية مباشرة في الصفحة من دفق، وهو أسرع وأنظف بصريًا على دفعة كبيرة

يضمّن AddJpegImage بايتات JPEG الأصلية في صفحة PDFium Component بينما يفك AddImage ويعيد SaveAs ترميز البكسلات في Delphi
يضمّن AddJpegImage بايتات الماسح المضغوطة كما هي، متجنبًا تمريرتَي فك الترميز وإعادة الترميز اللتين ينفذها AddImage وSaveAs
var
  Stream: TFileStream;
begin
  // ... بعد AddPage + PageNumber للصفحة الحالية ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // يضمّن بايتات JPEG كما هي؛ بلا دورة فك ترميز/إعادة ترميز.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

ما زلت تحسب X وY وDrawW وDrawH بالطريقة نفسها، لأنك تحتاج إلى الأبعاد بالبكسل للتحجيم. اقرأها من الملف أو من تحليل سريع للرأس، ثم سلّم الدفق الخام إلى AddJpegImage. أما مسوح PNG أو TIFF فمسار AddImage هو الصحيح؛ احجز اختصار JPEG للصيغة التي ينطبق عليها فعلًا

وضع تسمية لكل صفحة

تكون المسوح المؤرشفة أسهل في المراجعة عندما تحمل كل صفحة اسم ملفها الأصلي. AddText يرسم سلسلة نصية عند إحداثي في مساحة المستخدم، لذا تظهر التسمية أسفل الصورة مباشرة. تذكّر انعكاس محور Y: لوضع تسمية أسفل المسح، تطرح من الحافة السفلية للصورة بدل أن تضيف إليها

// تسمية أسفل المسح: Y تتناقص باتجاه أسفل الصفحة.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

ملاحظة أخيرة حول الحفظ. SaveAs دالة ترجع قيمة منطقية، لذا تحقّق من نتيجتها في الكود الإنتاجي بدل افتراض نجاح الكتابة؛ فالقرص الممتلئ أو مسار الإخراج المقفل يفشل بصمت وإلا. بعد انتهاء الحلقة وكتابة الملف، تحصل بالضبط على ما يحتاجه الأرشيف: ملف PDF واحد مرتب لكل ملف قضية، والصفحات بحجم مناسب وجاهزة للقراءة في أي عارض

تغطي اللبنات نفسها مهامًا قريبة. بدّل قاعدة التحجيم لكل صفحة فتحصل على كتاب صور فيه صورة واحدة في كل ورقة؛ واحتفظ بالحَلقة لكن اقرأ من مصدر TIFF متعدد الصفحات فتحصل على محول أرشيف فاكس. إذا أردت الصورة الأوسع لبناء ملفات PDF برمجيًا، فراجع إنشاء مستندات PDF من الصفر باستخدام PDFium Component؛ ولإعادة عرض النتيجة على الشاشة لاحقًا، فراجع تحويل صفحات PDF إلى صور JPEG باستخدام PDFium Component

PDFium Component من loslab.com يجمع واجهات إنشاء المستندات والعرض والنص المستخدمة طوال هذه السلسلة