مقال تقني

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

يخزن PDF الصور ككائنات من الدرجة الأولى داخل تدفقات المحتوى الخاصة به. عندما تشير صفحة إلى صورة فوتوغرافية أو مسح ضوئي أو رسم تخطيطي، تعيش بيانات البكسل في قاموس XObject جنبًا إلى جنب مع هندسة الصفحة. يظهر PDFium Component ذلك من خلال خاصيتين في 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 صورة XObjects بأبعاد بكسل صفرية أو بيانات تالفة بطريقة أخرى؛ في هذه الحالات يُرجع المكون 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 بين صورة XObjects والرسومات المتجهة المرسومة باستخدام مشغلات المسار. قد تُرجع الصفحة التي تبدو معقدة بصريًا BitmapCount يساوي صفرًا إذا كان كل عنصر عبارة عن متجه. تُرجع الصفحات الممسوحة ضوئيًا دائمًا واحدًا بالضبط: يكتب الماسح الضوئي الفحص بأكمله كصورة XObject واحدة كاملة الصفحة بأي دقة تم ضبط الماسح الضوئي عليها. تُرجع الصفحات التي تمزج نص التنضيد مع الصور الفوتوغرافية المضمنة إدخالًا واحدًا لكل صورة فوتوغرافية. عادةً لا تظهر خطوط القواعد الزخرفية، والخلفيات المظللة، وحدود الجداول في عدد الصور النقطية على الإطلاق

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

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

استخدام TPdfView في تطبيق نموذج (form application)

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

الذاكرة والأداء في وظائف الدُفعات (batch jobs)

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

خصائص BitmapCount و Bitmap[] الموضحة هنا هي جزء من PDFium Component لـ Delphi و C++Builder