يبدو استخراج نص PDF بسيطًا إلى أن تصادف مستندًا تكون فيه طبقة النص غائبة أو تالفة أو موزعة على عشرات المقاطع الصغيرة من المحارف بلا ترتيب ذي معنى. يوفّر PDFium VCL نقطتي دخول: المصفوفة Character[] للوصول الخام القائم على الفهرس إلى كل محرف في الصفحة، و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] تشير إلى المحارف الاصطناعية التي يدرجها العارض، مثل الواصلة اللينة عند نهاية السطر، والتي لا قيمة Unicode حقيقية لها، و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;
النتيجة سلسلة مسطحة من رموز Unicode بالترتيب الذي يعدّه 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 VCL كل تحليل PDF إلى DLL أصلي، إما pdfium32.dll أو pdfium64.dll بحسب منصة الهدف. يضم المكوّن برنامج CopyDlls.bat الذي ينسخ الملف المناسب إلى دليل نظام Windows. يكفي تشغيله مرة واحدة بامتيازات المسؤول على جهاز تطوير؛ أما للنشر فانسخ DLL بجوار ملف التطبيق التنفيذي بدلًا من ذلك. أما الإصدارات المفعلة بـ V8 (pdfium32v8.dll، pdfium64v8.dll) فهي أكبر بكثير ولا تحتاجها إلا إذا كانت ملفات PDF لديك تتضمن JavaScript يجب تنفيذه. لاستخراج النص البحت، فالبناء القياسي هو الخيار المناسب.
إذا كان DLL غائبًا وقت التشغيل، فستفشل Active := True بصمت تمامًا كما يحدث عند غياب الملف، لأن المكوّن يلتقط خطأ التحميل داخليًا. اختبر دائمًا على جهاز نظيف قبل الشحن.
استخدام FontSize[] مع Character[] لتحليل التخطيط
إلى جانب النص الخام، تتيح واجهة مستوى المحرف الخاصية FontSize[i] التي تعيد حجم النقطة المعروض لكل محرف. ومع CharacterOrigin[i] وCharacterRectangle[i] يمكنك التمييز بين متن النص والعناوين من دون الاعتماد على شجرة البنية. فمجموعة محارف يرتفع فيها الحجم فوق عتبة معينة هي على الأرجح عنوان في مستند غير موسوم. وتنطبق التقنية نفسها على رصد التسميات التوضيحية، أي النص الصغير أسفل صندوق الصورة، أو الحواشي، أي النص الصغير قرب أسفل الصفحة. لا يتطلب أي من ذلك عرضًا مرئيًا؛ فجميع هذه الخصائص تُقرأ مباشرة من طبقة النص التي يبنيها PDFium أثناء Active := True.
ملاحظة واحدة: تعكس FontSize[i] الحجم بعد تطبيق CTM الخاص بالصفحة، أي مصفوفة التحويل الحالية، لذلك فإن المستند الذي قام فيه المؤلف بتكبير الصفحة كلها سيعرض أحجامًا معدلة نسبيًا. إذا كنت تقارن الأحجام عبر صفحات ذات أبعاد مختلفة، فطبّع القيم بالنسبة إلى ارتفاع MediaBox لكل صفحة قبل اتخاذ قرار العتبة.
كتابة الناتج إلى ملف
يتعامل TStringList في Delphi مع إخراج UTF-8 بسلاسة منذ XE. اضبط WriteBOM := False إذا كنت تحتاج ملفًا من دون 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 VCL في Delphi وC++Builder.