مقال تقني

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

يبدو استخراج نصوص PDF بسيطاً حتى تصادف مستنداً يكون فيه طبقة النص غائبة، أو تالفة، أو مقسمة عبر عشرات من تشغيلات الأحرف الصغيرة دون أي ترتيب ذي معنى. ويمنحك مكون PDFium نقطتي دخول: مصفوفة Character[] للوصول الخام المعتمد على الفهرس لكل جليف (glyph) في الصفحة، وReadablePageContent لعرض منظم يعيد بناء الفقرات والعناوين من شجرة علامات PDF أو التحليل الاستدلالي. ولا يعتبر أي منهما هو الخيار الصحيح دائماً، لذا فإن فهم ما يكشفه كل منهما أمر مهم

فتح المستند وفخ الفشل الصامت

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

Pdf := TPdf.Create(nil);
try
  Pdf.FileName := 'report.pdf';
  Pdf.Active := True;
  if not Pdf.Active then
  begin
    ShowMessage('Could not open PDF (damaged or wrong password)');
    Exit;
  end;
  // extraction follows here
finally
  Pdf.Active := False;
  Pdf.Free;
end;

تحتاج الملفات المحمية بكلمة مرور إلى تعيين Pdf.Password := '...' قبل Active := True. ولا توجد فرصة ثانية: بمجرد فشل Active، فإنك تغلق وتعيد الفتح بكلمة المرور الصحيحة

استخراج صفحة بصفحة باستخدام Character[]

يتصفح النهج الأدنى مستوى كل حرف في كل صفحة. قم بتعيين Pdf.PageNumber لتحميل طبقة النص لتلك الصفحة، ثم كرر إدخالات CharacterCount باستخدام خاصية Character[]. وهناك علامتان في كل إدخال تستحقان التحقق منهما: CharacterGenerated[i] تحدد الجليفات الاصطناعية المدرجة بواسطة العارض (مثل الواصلات الناعمة عند فواصل السطور) والتي ليس لها قيمة يونيكود حقيقية، وCharacterMapError[i] تشير إلى أن PDFium لم يتمكن من تعيين الجليف إلى نقطة رمز، وهو ما يحدث مع ترميزات الخطوط التي تفتقر إلى جدول ToUnicode

procedure ExtractAllText(Pdf: TPdf; Output: TStrings);
var
  Page, I: Integer;
  Line: string;
  Ch: WideChar;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    Line := '';
    for I := 0 to Pdf.CharacterCount - 1 do
    begin
      if Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I] then
        Continue;
      Ch := Pdf.Character[I];
      if Ch = #13 then
        Ch := #10;   // normalize CR to LF
      Line := Line + Ch;
    end;
    Output.Add(Line);
  end;
end;

النتيجة هي سلسلة مسطحة من نقاط ترميز يونيكود بالترتيب الذي يفرضه PDFium، وهو الترتيب الذي تظهر به في تدفق المحتوى، وليس بالضرورة ترتيب القراءة من اليسار إلى اليمين. وبالنسبة لمعظم المستندات اللاتينية التي تنتجها أدوات المكتب القياسية، فإن هذا أمر جيد. وبالنسبة لملفات PDF الممسوحة ضوئياً والتي تم إخضاعها لـ OCR مع تسلسلات جليفات غير عادية، أو للنصوص من اليمين إلى اليسار، يمكن أن يكون الترتيب خاطئاً. وذلك عندما تصبح ReadablePageContent أكثر فائدة

استخراج منظم باستخدام ReadablePageContent

تذهب ReadablePageContent مستوى واحداً أعلى: فهي تعيد سجل TPdfReadableContent الذي تحمل مصفوفة Fragments الخاصة به أجزاء محتوى معلمة، كل منها مع Kind يحدد الفقرات والعناوين وعناصر القائمة وخلايا الجدول وما إلى ذلك. وعندما يحمل ملف PDF شجرة هيكل (تحقق من Pdf.IsTagged)، يكون المصدر هو rosStructure وترتيب القراءة موثوق. وبالنسبة للملفات غير المعلمة، يتراجع PDFium إلى rosHeuristic، الذي يجمع الأحرف حسب مربعات الإحاطة الخاصة بها في وحدات قراءة معقولة ولكنه لا يضمن الدقة

procedure ExtractStructured(Pdf: TPdf; Output: TStrings);
var
  Page: Integer;
  Content: TPdfReadableContent;
  Fragment: TPdfContentFragment;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Content := Pdf.ReadablePageContent(Page);
    for Fragment in Content.Fragments do
    begin
      case Fragment.Kind of
        cfHeading   : Output.Add('# ' + Fragment.Text);
        cfParagraph : Output.Add(Fragment.Text);
        cfListItem  : Output.Add('- ' + Fragment.Text);
      else
        Output.Add(Fragment.Text);
      end;
    end;
  end;
end;

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

ما الذي تمنحه لك CharacterOrigin وCharacterRectangle

تعيد كلتا الخاصيتين موضع الحرف في مساحة الصفحة (نقاط، والأصل في الزاوية اليسرى السفلية، وY يرتفع لأعلى). حيث إن CharacterOrigin[i] هي نقطة مرساة خط الأساس للجليف؛ بينما CharacterRectangle[i] هي مربع الإحاطة الكامل. وهذه هي اللبنات الأساسية لأي شيء يتجاوز النص العادي: اكتشاف حدود الأعمدة، أو تجميع الأحرف في سطور بمقارنة إحداثيات Y ضمن تسامح معين، أو بناء خريطة اختبار نجاح لتحديد النص في العارض. وإذا كنت بحاجة إلى معرفة الحرف الذي يقع تحت نقرة الماوس، فإن CharacterIndexAtPos(X, Y, ToleranceX, ToleranceY) يقوم بهذا البحث مباشرة دون الحاجة إلى تكرار المربعات

تجهيز مكتبة DLL في مكانها

يفوض مكون PDFium كل عمليات تحليل PDF إلى ملف DLL أصلي، إما pdfium32.dll أو pdfium64.dll اعتماداً على النظام المستهدف الخاص بك. ويشحن المكون برنامج نصي CopyDlls.bat ينسخ الملف الصحيح إلى دليل نظام ويندوز. ويكفي تشغيله كمسؤول مرة واحدة على جهاز التطوير؛ وبالنسبة للنشر، فإنك تنسخ ملف DLL جنباً إلى جنب مع التطبيق التنفيذي بدلاً من ذلك. وتعتبر المتغيرات الممكّنة لـ V8 (وهي pdfium32v8.dll، وpdfium64v8.dll) أكبر بكثير وضرورية فقط إذا كانت ملفات PDF الخاصة بك تحتوي على جافا سكريبت يجب تنفيذه. وبالنسبة لاستخراج النصوص النقية، فإن البنية القياسية هي الخيار الصحيح

إذا كان ملف DLL غائباً عند التشغيل، فإن Active := True سيفشل بصمت تماماً كما يفعل مع ملف مفقود، لأن المكون يلتقط خطأ التحميل داخلياً. واختبر دائماً على جهاز نظيف قبل الشحن

استخدام FontSize[] جنباً إلى جنب مع Character[] لتحليل التخطيط

إلى جانب النص العادي، تكشف واجهة برمجة التطبيقات على مستوى الأحرف عن FontSize[i]، والتي تعيد حجم النقطة المعروضة لكل جليف. وتتيح لك هذه، بالاقتران مع CharacterOrigin[i] وCharacterRectangle[i]، تمييز نص الجسم عن العناوين دون الاعتماد على شجرة الهيكل. ويعتبر تشغيل الأحرف حيث يقفز حجم الخط فوق عتبة معينة عنواناً بشكل شبه مؤكد في مستند غير معلم. وتنطبق التقنية نفسها على اكتشاف التعليقات التوضيحية (النص الصغير أسفل مربع إحاطة الصورة) أو الحواشي السفلية (النص الصغير بالقرب من أسفل الصفحة). ولا يتطلب أي من هذا عرضاً؛ حيث تقرأ جميع الخصائص الثلاث مباشرة من طبقة النص التي يبنيها PDFium أثناء Active := True

فارق بسيط واحد: تعكس FontSize[i] الحجم بعد تطبيق مصفوفة التحويل الحالية للصفحة (CTM)، لذا فإن المستند الذي قام المؤلف فيه بتغيير حجم الصفحة بأكملها سيبلغ عن أحجام معدلة نسبياً. وإذا كنت تقارن الأحجام عبر صفحات ذات أبعاد مختلفة، فقم بالتطبيع مقابل ارتفاع MediaBox لكل صفحة قبل اتخاذ قرارات العتبة

كتابة المخرجات إلى ملف

تتعامل TStringList في دلفي مع مخرجات UTF-8 بشكل نظيف منذ إصدار XE. قم بتعيين WriteBOM := False إذا كنت بحاجة إلى ملف خالٍ من علامة ترتيب البايت (BOM) (يختنق العديد من مستهلكي المصب عند وجود علامة BOM بادئة):

var
  Lines: TStringList;
begin
  Lines := TStringList.Create;
  try
    ExtractAllText(Pdf, Lines);
    Lines.WriteBOM := False;
    Lines.SaveToFile('output.txt', TEncoding.UTF8);
  finally
    Lines.Free;
  end;
end;

بالنسبة للمستندات الكبيرة جداً حيث تمثل الذاكرة مصدر قلق، اكتب مباشرة إلى TStreamWriter مع TEncoding.UTF8 داخل حلقة الصفحة بدلاً من تجميع كل شيء في قائمة أولاً

تعتبر واجهات برمجة التطبيقات Character[] وCharacterCount وCharacterOrigin[] وCharacterRectangle[] وReadablePageContent وCharacterIndexAtPos المعروضة هنا جزءاً من مكون PDFium Component لدلفي وC++Builder