يبدو استخراج نصوص 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