يُضيف PDFium Component طبقة نص قابلة للبحث إلى صفحات PDF الممسوحة ضوئيًا من Delphi عبر ApplyOcrSearchLayer. فهو يرسم كل صفحة مختارة، ويُسلِّم البكسلات إلى مزوّد OCR توفّره أنت، ويكتب الكلمات المتعرَّف عليها مرة أخرى ككائنات نص غير مرئية موضوعة فوق الكلمات في المسح الضوئي. ولا تُفكَّك صورة الصفحة الأصلية أو تُعاد ترميزها أو تُستبدَل أبدًا، بحيث تكون النتيجة البصرية مطابقة بايتًا ببايت للصفحة التي بدأت بها
ومحرك التعرف ليس جزءًا من المكتبة عمدًا. فـ PDFium يكشف عن رسم الصفحات، وتحويل الإحداثيات، وتحميل الخطوط، وإنشاء كائنات النص، وأوضاع الرسم غير المرئية، لكنه لا يحتوي على محرك OCR، والتظاهر بغير ذلك كان سيعني تضمين منتج تعرف يخص طرفًا آخر داخل مكوّن PDF. وبدلًا من ذلك، يقيم التعرف خلف واجهة IPdfOcrProvider: تُمرّر المكتبة بكسلات بتخطيط ثابت بترميز BGRA بأصل علوي، ويُعيد المزوّد نصًا بترميز يونيكود، وقيم ثقة، ورباعيات كلمات
ما هي طبقة النص القابلة للبحث بالضبط؟
ملف PDF الممسوح ضوئيًا صورة لمستند. فمحتوى الصفحة صورة كبيرة واحدة، ولا يوجد شيء يمكن تحديده أو البحث فيه أو نسخه أو فهرسته. وتُضيف طبقة النص القابلة للبحث كائنات نص حقيقية فوق تلك الصورة بوضع رسم مضبوط على غير مرئي، بحيث لا يرسم العارض شيئًا لكن التحديد والبحث والاستخراج يجد الكلمات بالضبط حيث تظهر
والتموضع هو اللعبة بأكملها. فإذا انزاح النص غير المرئي ببضع نقاط، تسقط تظليلات التحديد بجانب الكلمات لا عليها، وتُنتج نسخ فقرة نصًا بترتيب خاطئ. وهذا هو سبب وجوب استقاء الهندسة من التحويلات نفسها التي يستخدمها PDFium لرسم الصفحة بدلًا من تخمين نسبي
تنفيذ المزوّد
عقد المزوّد دالة واحدة. فهو يستقبل سجل صورة صفحة يحمل الأبعاد، والخطوة، ودقة DPI، وصيغة البكسل، وبايتات البكسل نفسها، إضافةً إلى رمز إلغاء، ويُعيد الكلمات أو رسالة خطأ:
uses
PDFium;
type
TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
public
function RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
end;
function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
I: Integer;
begin
// يحمل Image.Pixels صفوفًا بترميز BGRA بأصل علوي بطول Image.Stride بايت
// سلّمها إلى محركك، ثم املأ مُدخلًا واحدًا لكل كلمة مُتعرَّف عليها
SetLength(Words, RecognisedCount);
for I := 0 to RecognisedCount - 1 do
begin
Words[I].Text := EngineWordText(I);
Words[I].Confidence := EngineWordConfidence(I); // 0..1
Words[I].Quad := TPdfOcrQuad.FromRectangle(
EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
end;
ErrorMessage := '';
Result := True;
end;
وتُستخدَم رباعيات لا مستطيلات، لأن المسح الضوئي نادرًا ما يكون مربّعًا تمامًا مع الصفحة. فكلمة على صفحة مائلة قليلًا تشغل متوازي أضلاع، وتحمل TPdfOcrQuad أربع نقاط زاوية بحيث تحافظ الكلمات المائلة والمُدارة على منطقة تحديد دقيقة. ويمكن للمحركات التي تُبلغ فقط عن مربعات محاذية للمحاور استخدام FromRectangle، التي تبني الرباعية المتحلِّلة
لماذا لا يمكن تحجيم مواضع الكلمات نسبيًا؟
من المغري تحويل إحداثية بكسل إلى إحداثية صفحة بالقسمة على عرض الرسم والضرب في عرض الصفحة. وهذا لا ينجح إلا مع صفحات بلا دوران، وبصندوق قص مطابق لصندوق الوسائط، وبأصل عند الصفر، والعديد من المستندات الممسوحة ضوئيًا تُخلّ بواحد على الأقل من هذه الشروط
يُحوِّل PDFium Component كل زاوية من زوايا الرباعية الأربع على حدة عبر FPDF_DeviceToPage، وهو التحويل نفسه الذي استخدمه الراسم لإنتاج البكسلات، بحيث تُعالَج مُدخَلات /Rotate وصناديق القص المُزاحة ببنية التصميم نفسها. ثم تُبنى المصفوفة التآلفية لكائن النص من ثلاث من النقاط المُحوَّلة، الزاوية السفلية اليسرى، والسفلية اليمنى، والعلوية اليسرى، وهو ما يكفي بالضبط للتعبير عن الموضع والتحجيم والدوران والقص المائل
ويُنشَأ كائن النص نفسه بحجم خط الوحدة بحيث يمكن قياس حدود خطه الحقيقية، ثم تُحوَّل حدود الكائن المقيسة إلى الرباعية الهدف. أما التحجيم بحجم نقطة مُخمَّن على أمل أن يطابق الكلمة الممسوحة ضوئيًا فسينحرف مع كل استبدال خط؛ والقياس أولًا يجعل الملاءمة مستقلة عن الخط الذي تستخدمه الطبقة
تشغيلها على مستند
يتحكم سجل الخيارات بالدقة، والتصفية، وكل ميزانية. وتصفية الثقة أهم مما تبدو: فالكلمات المهملة بثقة منخفضة تلوّث نتائج البحث بشكل دائم، وعلى عكس الرسم الخاطئ، لا يلاحظ أحد ذلك حتى يُعيد بحث ما نتيجة عبثية:
var
Pdf: TPdf;
Options: TPdfOcrOptions;
Report: TPdfOcrReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'scanned-contract.pdf';
Pdf.LoadDocument;
Options := TPdfOcrOptions.Default;
Options.Dpi := 300; // دقة التعرف
Options.MinConfidence := 0.60; // إسقاط الكلمات غير المؤكدة
Options.SkipPagesWithText := True; // عدم لمس الصفحات الرقمية أصلًا
Options.ContinueOnError := True; // يجب ألا توقف صفحة واحدة سيئة المهمة بأكملها
Options.MaxPixelsPerPage := 40 * 1000 * 1000;
if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
Pdf.SaveAs('scanned-contract-searchable.pdf');
for I := 0 to High(Report.Pages) do
if Report.Pages[I].Status = popsFailed then
Writeln(Format('page %d failed: %s',
[Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
[Report.InsertedWordCount, Report.RejectedWordCount,
Report.SkippedPageCount]));
finally
Pdf.Free;
end;
end;
وتستحق SkipPagesWithText تأكيدًا خاصًا في الأرشيفات المختلطة. فملف PDF يحمل بالفعل نصًا حقيقيًا، سواء كان رقميًا أصلًا أو سبقت معالجته، سيحصل على طبقة نص ثانية إذا شغّلت OCR عليه بشكل أعمى، والتكرار يجعل الاستخراج يُعيد كل كلمة مرتين. وتخبرك حالة كل صفحة popsSkippedExistingText بالضبط بأي الصفحات تُرِكت دون لمس
الميزانيات، والإلغاء، واحتواء الفشل
وكل كمية يمكن أن يُضخِّمها مستند عدائي أو ضخم فحسب لها سقف: البكسلات لكل صفحة وإجمالًا، والكلمات لكل صفحة وإجمالًا، والأحرف لكل كلمة. ويُتحقَّق من جميعها قبل كتابة الصفحة، لا بعدها، ويُحسَب تقدير البكسلات من أبعاد الصفحة ودقة DPI قبل تخصيص أي صورة نقطية. ورفع DPI من 150 إلى 300 يُضاعف الذاكرة أربع مرات لكل صفحة، لذا فإن سقف كل صفحة هو المعامِل الذي يجب ضبطه أولًا حين تبدأ مهمة دفعية بالفشل مع الصيغ الكبيرة
ويمر رمز الإلغاء عبر المسار بأكمله: الرسم التقدمي، واستدعاء المزوّد، وحلقة إدراج الكلمات. وهذا يعني أن مستخدمًا يُلغي أثناء التعرف على ملف من 400 صفحة يتوقف خلال صفحة واحدة بدلًا من نهاية المستند، ونمط الرمز نفسه المستخدَم في مواضع أخرى من المكوّن، الموصوف في الرسم التقدمي القابل للإلغاء، ينطبق هنا دون تغيير
واحتواء الفشل يكون لكل صفحة. فتجمّع المكتبة مقابض الكائنات التي أدرجتها في صفحة وتستدعي FPDFPage_GenerateContent مرة واحدة، بعد وضع جميع الكلمات. وإذا فشل أي شيء في المنتصف، سواء كان خطأ مزوّد أو مشكلة خط، تُزال الكائنات المُدرَجة في تلك الصفحة بالترتيب المعاكس ويُعاد توليد محتوى الصفحة، بحيث تعود صفحة فشلت إلى حالتها الأصلية بدلًا من الاحتفاظ بنصف طبقة نص. ثم تستمر حلقة المستند أو تتوقف وفق ContinueOnError، وتُستعاد الصفحة النشطة دائمًا
التحقق من أن الصورة لم تُلمَس فعليًا
أقوى فحص متاح هو أيضًا الأبسط: ارسم الصفحة قبل تطبيق الطبقة وبعده بالحجم نفسه وقارن الصور النقطية. وينبغي أن تكون متطابقة بايتًا ببايت، لأن النص غير المرئي لا يرسم شيئًا ولم يُفكَّك تدفق الصورة قط. وأي فرق يعني أن شيئًا آخر غير طبقة النص قد غيّر الصفحة
وبعد ذلك، تحقق من جانب النص باستخراج النص من الملف المُعالَج والتأكد من أن مواضع الكلمات تقع على المسح الضوئي. ومسار الاستخراج هو نفسه الموصوف في استخراج النص من مستندات PDF، وللتحقق البصري السريع من المحاذاة، يتيح لك رسم الصفحات كصور كما في تحويل صفحات PDF إلى JPEG تراكب مربعات الكلمات فوق المسح الضوئي
وتعمل طبقة OCR، والرسم، والاستخراج، والتحرير جميعها على كائن المستند نفسه في Delphi وC++Builder وLazarus؛ وتُوصَف واجهة البرمجة الكاملة على صفحة PDFium Component لـ Delphi