Technical Article

استخراج الصور من ملفات PDF باستخدام PDFium VCL في Delphi

يخزن PDF الصور كعناصر من الدرجة الأولى داخل تدفقات المحتوى الخاصة به. عندما تشير صفحة إلى صورة فوتوغرافية أو مسح ضوئي أو مخطط، تبقى بيانات البكسل داخل قاموس XObject إلى جانب هندسة الصفحة. يوفّر PDFium VCL ذلك عبر خاصيتين في TPdf: BitmapCount التي تعيد عدد الصور النقطية المضمّنة في الصفحة الحالية، وBitmap[Index] التي تفك ترميز إحداها إلى TBitmap تملكه أنت ويجب أن تحرره. هذا هو نموذج الاستخراج الكامل. الحلقة لا تتجاوز أربعة أسطر، وما يحتاج إلى حكم هو ما يحيط بها

فتح المستند

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

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // proceed to extraction
  finally
    Pdf.Free;
  end;
end;

تتبع الملفات المحمية بكلمة مرور النمط نفسه: أسند Pdf.Password قبل ضبط Active := True. إذا كانت كلمة المرور خاطئة فسيبقى Active False ولن تحصل على استثناء تلتقطه. في أداة دفعية تعالج مئات الملفات، يكون هذا السلوك الصامت مفيدا فعلا: تجمع الإخفاقات في قائمة بدلا من تفكيك مكدس الاستدعاء عند كل ملف

التنقل بين الصفحات واستخراج الصور النقطية

BitmapCount خاص بكل صفحة، لذلك تضبط Pdf.PageNumber قبل قراءته. أرقام الصفحات تبدأ من 1، والقيمة الافتراضية هي 0 أي لا توجد صفحة محمّلة. الخاصية Bitmap[Index] تبدأ من 0 وتعيد TBitmap يملكه المستدعي. يجب أن تحرره. إهمال التحرير داخل حلقة طويلة على مستند كبير يجعل الذاكرة ترتفع بسرعة، لأن كل صورة نقطية قد تستهلك عدة ميغابايت من بيانات البكسل الخام قبل أي ضغط

procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

يهم فحص Assigned. فعدد صغير من مولدات PDF يكتب كائنات صورة XObject بأبعاد بكسل صفرية أو ببيانات معطوبة بطريقة أخرى، وفي تلك الحالات يعيد المكوّن nil بدلا من صورة نقطية فارغة. التعامل مع nil على أنه خطأ ووقف الاستخراج رد فعل غير صحيح: تجاوز العنصر، وسجل الصفحة والفهرس إذا كنت تحتاج إلى أثر تدقيق، ثم تابع. قد لا تزال بقية الصفحة تنتج صورا صالحة

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

اختيار صيغة الإخراج

BMP غير ضياعي ومتوفر دائما من دون وحدات إضافية، لذا فهو خيار افتراضي جيد عندما لا تعرف بعد ما الذي تحتويه الصورة. عندما يهم حجم الملف، تخبرك صيغة البكسل في TBitmap الذي أُعيد لك أي برنامج ترميز هو المناسب. صورة 32 بت تحمل قناة ألفا، وPNG يحافظ عليها من دون فقد. أما صورة 24 بت كبيرة ذات تدرج مستمر فهي مرشحة لـ JPEG. الصور الأصغر أو المرسومة بباليت محدودة يفضّل غالبا إبقاؤها BMP بدلا من تمريرها عبر JPEG، لأنه يضيف آثار التكتل عند إعدادات الجودة المنخفضة ولا يوفر الكثير عند الإعدادات العالية

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: lossless, no extra units
  end;
end;

عمليا، يحدد Bmp.PixelFormat والأبعاد اختيار الصيغة. إذا كانت PixelFormat = pf32bit فأنت تحتاج إلى صيغة تحمل ألفا، وPNG هو الخيار الواضح، لكنه يتطلب وحدة PNGImage في إصدارات Delphi الأقدم. بالنسبة للصور 24 بت التي يزيد عرضها على نحو 300 بكسل، يمنح JPEG بجودة 85 خفضا في الحجم بثلاثة إلى واحد مقارنة بـ BMP من دون فقد ملحوظ في معظم المحتوى الفوتوغرافي. تحت تلك العتبة يكون BMP مقاربا في الحجم ويتجنب قرار الجودة بالكامل

ما الذي يحسبه BitmapCount وما الذي لا يحسبه

يميز PDF بين كائنات الصورة XObject والرسوم المتجهة المرسومة بعوامل المسار. قد تعيد الصفحة التي تبدو معقدة بصريا قيمة BitmapCount تساوي الصفر إذا كانت كل العناصر متجهة. الصفحات الممسوحة ضوئيا تعيد تقريبا دائما واحدا بالضبط: فالمسح الضوئي كله يُكتب ككائن صورة XObject واحد بصفحة كاملة وبأي دقة كانت المضبوطة في الماسح. الصفحات التي تمزج النص المنسق مع الصور المضمّنة تعيد عنصرا واحدا لكل صورة فوتوغرافية. عادة لا تظهر الخطوط الزخرفية أو الخلفيات المظللة أو حدود الجداول في عدد الصور النقطية على الإطلاق

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

تفصيل يستحق الانتباه: قيمة BitmapCount التي تقرأها تخص الصفحة الحالية كما كانت عند آخر إسناد لـ PageNumber. إذا فرّع الكود أو استدعى أي دالة تغير PageNumber بين العد والجلب فقد تقرأ صورا أقل من المساحة التي خصصتها، أو تتجاوز النهاية. أبقِ قراءة العدد وحلقة Bitmap[] على الصفحة نفسها من دون لمس PageNumber بينهما

استخدام TPdfView في تطبيق نماذج

الذاكرة والأداء في المهام الدفعية

عبر أرشيف كبير تكون ميزانية الذاكرة هي أهم ما يجب مراقبته. كل استدعاء لـ Bitmap[] ينشئ TBitmap جديدا على الكومة، وعلى صفحة ممسوحة بدقة 300 DPI قد يكون ذلك بسهولة 25 ميغابايت من بيانات البكسل الخام قبل أي ترميز. إذا عالجت الصفحات في حلقة ضيقة من دون التحرير بين التكرارات فستنمو مجموعة العمل خطيا مع عدد الصور. الشكل الصحيح دائما هو: اجلب صورة واحدة، نفذ ما تحتاج إليه، حررها، ثم اجلب التالية. إذا احتجت إلى الاحتفاظ بعدة صور في الوقت نفسه لخطوة مقارنة، فاحصها أولا بـ BitmapCount وخصص الحاوية وفقا لذلك، ثم حرر كل واحدة بمجرد الانتهاء منها بدلا من تأجيل ذلك إلى تنظيف نهاية المستند. في مستند يضم 500 صفحة ممسوحة قد يعني هذا الفرق بين 25 ميغابايت و12 غيغابايت من ذروة RSS

يعرض المكوّن TPdfView الخصائص نفسها BitmapCount وBitmap[]، لكن الصفحة التي يقرأ منها هي الصفحة المعروضة حاليا في العرض، لا TPdf.PageNumber. مؤشرا الصفحة مستقلان، وضبط أحدهما لا يحرك الآخر. في تطبيق نماذج VCL مع عارض حي، يمكنك استدعاء Pdf.PageNumber := N لتوجيه الاستخراج عبر TPdf بينما يبقى العارض على آخر موضع تصفحه المستخدم. هذا الفصل مقصود ويحافظ على حالة العرض نظيفة أثناء تشغيل الاستخراج في الخلفية

تعد الخصائص BitmapCount وBitmap[] المعروضة هنا جزءا من مكوّن PDFium VCL لـ Delphi و C++Builder