يجعل HotPDF صفحات PDF الممسوحة قابلة للبحث بـ RapidOCR داخل العملية عبر HPDFCreateRapidOCRDLLOCREngine، مصنع أضيف في v2.774.0 يحمّل HotPDFRapidOCR.dll، ويبقي نماذج ONNX للكشف وتصنيف الزاوية والتعرف مقيمة في الذاكرة، ويعيد IHPDFOCREngine. تمرر ذلك المحرك إلى THotPDF.ApplyLoadedOCRTextLayer، التي تعرض كل صفحة وتجري الاستدلال على CPU دون Python أو عملية ابنة، وتثبت طبقة نص Unicode غير مرئية
الدافع هو التكلفة لكل صفحة. كان مُكيِّف عملية RapidOCR الذي شُحن سابقاً، HPDFCreateRapidOCREngine، يطلق عامِل Python لكل نداء Recognize، وكان ذلك العامل يستورد بيئة تشغيله ويحمّل نماذج ONNX قبل أن يقرأ بكسلاً واحداً. على أرشيف من 500 صفحة تتكرر ضريبة البدء تلك 500 مرة، والنشر يعني شحن بيئة Python بجوار تنفيذي Delphi. يحمل الـ DLL الأصلي النماذج مرة واحدة، عند إنشاء المحرك، وينكمش النشر إلى الـ DLL وملفات نماذجه وقاموس محارف. وما تدفعه مقابل ذلك هو القدرة على قتل مُعَرِّف عالق، وأغلب هندسة هذا المُكيِّف تدور حول العيش مع ذلك بصدق
كيف تجعل PDF ممسوحاً قابلاً للبحث بـ DLL الـ RapidOCR؟
إنشاء PDF قابل للبحث بـ DLL الـ RapidOCR الأصلي يأخذ نداء مصنع واحداً ونداء ApplyLoadedOCRTextLayer نفسه الذي تستخدمه كل محركات OCR في HotPDF. يسكن المصنع في وحدة HPDFRapidOCRRecognition ويُتحقق منه بحماس: يجب أن يوجد الـ DLL ومجلد النماذج، وأن يفاضل كل ملف نموذج وقاموس، وأن تكون نسخة الـ ABI هي 1، وأن تكون كل التصديرات المطلوبة حاضرة قبل تهيئة أي نموذج. أخطاء الضبط ترفع EArgumentException؛ ونموذج يفشل تحميله يرفع EInvalidOperation حاملاً النص التشخيصي الذي كتبه الـ DLL
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// تُحمَّل النماذج هنا، خارج أي مهلة تعرف.
// أسماء النماذج النسبية في THPDFRapidOCRDLLOptions.Default تفاضل
// مقابل مجلد النماذج.
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models');
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Options := THPDFOCRTextLayerOptions.Default; // 300 DPI، MinimumConfidence 0.5
// قائمة صفحات فارغة تعني كل الصفحات؛ والصفحات ذات نص مسبق تُتخطى
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
raise Exception.Create(string(Info.Diagnostic));
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' lines accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
يسمّي THPDFRapidOCRDLLOptions.Default القيم ch_PP-OCRv3_det_infer.onnx و ch_PP-OCRv3_rec_infer.onnx و ch_ppocr_mobile_v2.0_cls_infer.onnx و ppocr_keys_v1.txt، بخيط CPU واحد، وحد إدخال 16,777,216 بكسل، ومهلة تعرف 60,000 ms. ومنذ v2.775.0 يستبدل THPDFRapidOCRDLLOptions.ForLanguage نموذج تعرف وقاموساً متطابقَين للصينية التقليدية والروسية واليابانية والعربية وغيرها من الملفات التعريفية؛ وسبب وجوب تغيّر النموذج والقاموس معاً مغطى في نماذج RapidOCR متعددة اللغات وقواميس CTC في HotPDF. يبلّغ المحرك عن نفسه بوصفه RapidOCR (native DLL) في Info.EngineName، فيبقي السجلات بلا لبس بجوار مُكيِّف عملية Tesseract OCR الخارجي و محرك OCR المدمج بمطابقة القوالب
لماذا لا يتحدث C ABI إلا بلغة int32_t وبايتات UTF-8؟
يستخدم ABI الـ HotPDFRapidOCR.dll أعداداً صحيحة عريضة ثابتة ومؤشرات خام وأطوال بايتات صريحة فقط، لأن Delphi و C++Builder و Free Pascal لا يتشاركان شيئاً مع MSVC خارج اصطلاح النداء C. فـ std::string أو std::vector أو استثناء C++ له تخطيط ونموذج فك تراص يخصان مترجماً ومكتبة تشغيل واحدة. دع أي واحد منها يعبر الحد فيكون الفشل مكدساً فاسداً أو كتلة كومة يحررها مخصِّص خاطئ، لا خطأ نظيفاً
لذلك تتبع ABI نسخة 1 قائمة قصيرة من القواعد. كل تصدير هو cdecl ويعيد حالة int32_t، حيث 1 تعني نجاحاً و 0 تعني فشلاً. وكل تابع قد يفشل يأخذ مخزن تشخيص يملكه المستدعي وسعته بالبايتات؛ يكتب الـ DLL رسالة UTF-8 منتهية بـ NUL مقطوعة لتناسب، ويفكها المُكيِّف بمنهٍّ قاسٍ في آخر بايت من مخزنه ذي 4,096 بايت. وكل جسم تصدير مغلف بـ try مع catch (const std::exception &) و catch (...) معاً، فيصير خطأ ONNX Runtime أو تأكيد OpenCV أو قاموس غير صالح حالةَ 0 مع نص، لا استثناءً يهرب إلى كود Pascal قط
| التصدير | الدور | متى تفاضله المُكيِّف |
|---|---|---|
HPDFRapidOCRAbiVersion | يعيد 1؛ وأي قيمة أخرى تُرفض | أولاً قبل أي شيء |
HPDFRapidOCRCreate | يحمّل نماذج الكشف والتصنيف الاختياري والتعرف والقاموس | في المصنع |
HPDFRapidOCRRecognize | يجري bitmap واحداً ويصدر callback واحداً لكل سطر نص | في المصنع |
HPDFRapidOCRDestroy | يحرر نسخة النموذج | في المصنع |
HPDFRapidOCRSetReadingDirection | ترتيب صفوف من اليمين إلى اليسار اختياري، أضيف في v2.775.0 | فقط حين تُضبط RightToLeft |
والتصدير الاختياري يُفاضل بكسل عمداً: DLL من v2.774.0 يفتقده ما زال يخدم الطلبات من اليسار إلى اليمين. ويُحمَّل الـ DLL بـ LoadLibraryEx بأعلام بحث تغطي مجلد الـ DLL نفسه زائد الدلائل الآمنة الافتراضية، فتُوجد اعتماديات ONNX Runtime أو OpenCV الموضوعة بجوار HotPDFRapidOCR.dll دون لمس PATH. وتسافر مسارات النماذج والقاموس UTF-8 ويحوّلها الـ DLL بـ MultiByteToWideChar في الوضع الصارم قبل فتح الملفات عبر APIs المحارف الواسعة، فمجلد نماذج تحت اسم مستخدم صيني أو سيريلي يعمل بدل أن يُوسَّع بايتاً ببايت إلى هراء
وقاعدة واحدة تسكن البناء لا الترويسة. يربط الـ DLL ثابتاً ONNX Runtime و OpenCV، ويستخدم ضبط CMake الافتراضي CRT للإصدار الثابت (/MT). ومكتبات ثابتة جمِعت مقابل /MD ممزوجة في DLL من نوع /MT تنتج أخطاء ربط في أحسن الأحوال وكومتين مستقلتين في أسوأها، فعلى المكتبات المزوَّدة أن تطابق أي وضع CRT يستخدمه الـ DLL
ماذا يحدث بين TBitmap وسطر نص؟
يسلّم HotPDF إلى الـ DLL لقطة BGR مستقلة من أعلى إلى أسفل للصفحة المعروضة، ويسلّم الـ DLL عائداً callback واحداً لكل سطر نص معروف بنص UTF-8 مستعار عليه أن ينسخه المُكيِّف قبل العودة
على Delphi يسند المُكيِّف bitmap الصفحة إلى TBitmap خاص، ويفرض pf24bit، ويقرأ الصفوف بـ GetDIBits بقيمة biHeight سالبة، وهي تعطي صفوفاً من أعلى إلى أسفل مبطنة لمحاذاة أربع بايتات؛ ويُمرر ذلك الـ stride صراحة. وعلى FPC يقرأ عبر CreateIntfImage، لأن كتابة scanline في LCL تستطيع تحديث الصورة الخام دون تحديث مقبض GDI. ولا يُعدَّل bitmap المستدعي قط، وتُفحص ميزانية البكسل (MaxPixels، 16,777,216 افتراضياً وقابلة للضبط حتى 67,108,864) وحد 32,767 بكسل لكل بُعد قبل تخصيص مخزن اللقطة
داخل الـ DLL تُبطَّن اللقطة بـ 50 بكسل أبيض، وتُكشف مناطق النص بحد جانبي أقصى 1,024 بكسل، وتُرتَّب الصناديق في صفوف أفقية، وتُدار كل قصاصة اختيارياً بمصنِّف الزاوية قبل التعرف. ثم يمر كل سطر نص عبر callback يتلقى const char* وعدد بايتات وصندوقاً صحيحاً ببكسلات الصورة الأصلية ومتوسط ثقة المحارف. ومؤشر النص صالح فقط أثناء الـ callback، فينسخه المُكيِّف فوراً، وهو صارم فيما يقبل:
- يُفك UTF-8 بـ
MB_ERR_INVALID_CHARS؛ فتسلسل مشوه يُسقط الصفحة بدل إنتاج محارف بديلة في طبقة قابلة للبحث - محارف التحكم C0 و C1 مرفوضة، والأسطر ذات المسافات فقط تُتخطى
- يجب أن يستقر الصندوق داخل الـ bitmap وأن تكون الثقة قيمة محدودة من 0 إلى 1
- يُعَدّ النص مقابل
MaxTextCodeUnitsللطلب بسقف قاسٍ 1,048,576 وحدة UTF-16 لكل نداء، ومحارف المستوى التكميلي تكلف وحدتين - أي استثناء Pascal داخل الـ callback يُلتقط هناك ويُخزن ويُحوَّل إلى إعادة 0، وهو ما يجعل الـ DLL يتوقف ويبلّغ فشلاً؛ وتصير الرسالة المخزنة هي التشخيص
ونتيجتان تهمان الضبط. الأولى أن وحدة المخرج سطر لا كلمة: كل سطر يستهلك خانة MaxWords واحدة، و Info.AcceptedWordCount و Info.DroppedWordCount تعدّان أسطراً، وتمييز نتائج البحث يمتد عبر صندوق السطر. والثانية أن MinimumConfidence (0.5 افتراضياً) تُقارن بمتوسط ثقة محارف السطر، فسطر بمحرف غير مقروء واحد بين عشرين نظيفاً ينجو عادة. ولا يوفر الـ DLL خط أساس، فيقدّره مسار طبقة النص من الصندوق. والصفحة الفارغة تنجح بأصفار أسطر، وأي فشل يمسح النتائج الجزئية فيبقى التثبيت متعدد الصفحات الكل أو لا شيء
ملكية النموذج وسلامة الخيوط
يمتلك كل محرك RapidOCR DLL نسخة نموذج واحدة بالضبط طوال حياته، وتُسلسَل نداءات Recognize على ذلك المحرك بقسم حرج. إمساك واجهة IHPDFOCREngine هو ما يبقي النماذج دافئة، فالنمط الصحيح للعمل الدفعي هو إنشاء المحرك مرة واحدة وإعادة استخدامه عبر المستندات
procedure OcrBatch(const Files: TStrings; const OutputDir: string);
var
Models: THPDFRapidOCRDLLOptions;
Engine: IHPDFOCREngine;
Doc: THotPDF;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
I: Integer;
begin
Models := THPDFRapidOCRDLLOptions.Default;
Models.UseAngleClassifier := False; // مسوح معتدلة: لا يُحمَّل نموذج مصنِّف
Models.Threads := 4; // 1..64، مقيدة بعدد المعالجات المنطقية
Models.TimeoutMilliseconds := 120000; // لكل نداء Recognize، تعاونية
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
Options := THPDFOCRTextLayerOptions.Default;
for I := 0 to Files.Count - 1 do
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if (Doc.LoadFromFile(Files[I]) > 0) and
Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
Doc.SaveLoadedDocument(IncludeTrailingPathDelimiter(OutputDir) +
ExtractFileName(Files[I]))
else
Writeln(Files[I], ': ', string(Info.Diagnostic));
finally
Doc.Free;
end;
end;
end; // آخر مرجع أفلت: تُدمَّر النماذج ثم يُفرَّغ الـ DLL
تضبط قيمة Threads كلا عددي خيوط intra-op و inter-op لكل جلسة ONNX، ويقيّدها الـ DLL بعدد المعالجات النشطة. خيطان يتشاركان محركاً لا يجريان بالتوازي؛ الثاني ينتظر القفل. وذاك الانتظار ليس EnterCriticalSection عمياء: يستدعي المُكيِّف TryEnterCriticalSection كل 25 ms ويفحص رمز الإلغاء والمهلة بين المحاولات، فيمكن لطلب في الطابور أن يُلغى أو ينتهي وقتُه مع ذلك. وإن احتجت توازياً حقيقياً فأنشئ محركاً لكل عامل وقبل أن يحمل كل محرك نسخته الخاصة من النماذج في الذاكرة
وترتيب التفكيك مثبت بمدمر المحرك: يحرر HPDFRapidOCRDestroy نسخة النموذج أولاً، ثم يفرّغ FreeLibrary الـ DLL. وعلى الجانب الأصلي تهيئة النموذج حذرة بالقدر نفسه؛ حين يفشل نموذج التعرف بعد أن بُنيت جلساتا الكاشف والمصنِّف أصلاً، تُحرَّر تلك الجلسات قبل التبليغ عن الخطأ، ويُفحص عدد أصناف القاموس مقابل مخرج النموذج أثناء التهيئة لا عند الصفحة الأولى
لماذا لا يستطيع نداء OCR أصلي أن يُقتل في منتصف الاستدلال؟
لا يستطيع نداء RapidOCR أصلي أن يُقتل في منتصف الاستدلال لأنه يجري على خيطك، داخل عمليتك، في وسط جلسة ONNX Runtime لا تقبل الانقطاع. الإلغاء في مُكيِّف DLL لدى HotPDF إذن تعاوني: يستدعي الـ DLL callback إحباطاً قبل الكشف وبعده، وبعد التصنيف، وبعد كل سطر معروف، ويتوقف عند أول نقطة تفتيش يعيد فيها الـ callback القيمة 0. فجلسة ONNX Run واحدة بدأت ستنتهي أولاً
والبدائل أسوأ من الانتظار. كان TerminateThread سيترك قفل كومة CRT ومسبَح خيوط ONNX Runtime وأي حالة OpenCV في الحالة التي صادفها، مسموماً بقية العملية. و FreeLibrary أثناء نداء ما زال يجري تفريغُ كودٍ على المكدّس. ولا يمكن جعل أيٍّ منهما آمناً، فلا يحاول المُكيِّف أيَّاً منهما قط. مهلة TimeoutMilliseconds مهلةٌ تعاونية بالتالي، والمهلة المنتهية تظهر خطأَ محرك مع تشخيص انتهاء وقت، بينما يظهر الرمز الملغى otlsCancelled:
// ينشئ المستدعي الـ Token ويشاركه مع خيط الـ UI،
// وهو يستدعي Token.Cancel حين يضغط المستخدم Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
case Info.Status of
otlsCancelled:
// يعود عند حد المرحلة أو السطر التالي؛ والمستند بلا تغيير
Writeln('Cancelled');
otlsEngineError:
// يتضمن انتهاء مهلة تعاونية وتشخيصات أصلية
Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded:
Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
هذه هي المقايضة الجوهرية بين مُكيِّفات العمليات لدى HotPDF و DLL داخل العملية، ولا يفوز أي طرف في كل صف:
- تكلفة البدء: يطلق مُكيِّفا Tesseract و Python RapidOCR عملية ويحملان النماذج لكل صفحة؛ ويحمل الـ DLL النماذج مرة لكل محرك
- الإيقاف: تستطيع عملية ابنة أن تُنهى فوراً، ويعمل عامل Python داخل Job Object يُقتل عند الإغلاق فتموت شجرته العملية كلها معه؛ ولا يستطيع الـ DLL التوقف إلا عند حدود المراحل والأسطر
- احتواء الأعطال: انهيار في
tesseract.exeيُسقط صفحة واحدة؛ وانتهاك وصول داخل الـ DLL يُسقط عمليتك أنت - النشر: تحتاج مُكيِّفات العمليات برنامجاً مثبتاً أو بيئة Python؛ ويحتاج الـ DLL نفسه ونماذجه وقاموسه، المطابقة لعرضة التطبيق
- الذاكرة: يفرّغ مُكيِّفا العمليات كل شيء حين تخرج الابنة؛ ويبقي محرك DLL نماذجه مقيمة حتى يفلت آخر مرجع واجهة
لتطبيق مكتبي تفاعلي يجرّ OCR صفحةً في كل مرة، عادةً ما تتفوق استجابة الـ DLL. ولخادم يبتلع مسوحاً غير موثوقة على مدار الساعة، جدارُ العملية يستحق تكلفة بدئه
بناء ونشر HotPDFRapidOCR.dll
يُبنى HotPDFRapidOCR.dll من مصادر C++ في Native/RapidOCR بـ MSVC و C++17 و Windows SDK و CMake 3.20 أو أحدث، عبر سكربت مساعد يأخذ مصادر الشبكة الأصلية ودلائل ONNX Runtime و OpenCV زائد منصة Win32 أو Win64. ابنِ الاثنين إن شحنت الاثنين، لأن تطبيق Delphi ذا 32-بت لا يستطيع تحميل DLL ذي 64-بت، وعلى المكتبات الثابتة التي تزودها أن تطابق البنية الهدف كذلك مع وضع CRT
ولجانب النموذج حدود توافقه الخاصة. الكاشف كاشف نص DB؛ ويقبل المُعَرِّف نماذج CTC بتخطيط NCHW بارتفاع إدخال مثبت 32 أو 48، ويستخدم 48 للنماذج بارتفاع ديناميكي. لا يستطيع ONNX Runtime الثابت المرفق تحميل نماذج حُفظت بنسخة IR أحدث، فصادرات PP-OCRv5 الحديثة تفشل تهيئةً بتشخيص بدل التحميل الجزئي. ويجب أن يكون القاموس UTF-8 بلا BOM، بترتيب محارف النموذج بالضبط، ويجب أن يطابق عدد أصنافه مخرج النموذج؛ وتُقبل نهايات أسطر CRLF. والتعرف دون اتصال: لا ينزّل الـ DLL نموذجاً مفقوداً أبداً
مرجع سريع
- المصنع:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])فيHPDFRapidOCRRecognition، متاح منذ v2.774.0 في بناءات Delphi و C++Builder و FPC/Lazarus على Windows - أبقِ
IHPDFOCREngineالمعادة حية عبر الصفحات والمستندات؛ فإفلاتها يدمّر النماذج ويفرّغ الـ DLL - محرك واحد يجري تعرفاً واحداً في كل مرة؛ أنشئ عدة محركات للعاملين المتوازيين وخصص ذاكرة لكل نسخة نموذج
- المخرج مدخل واحد لكل سطر نص بمتوسط ثقة المحارف، مصفّى بـ
THPDFOCRTextLayerOptions.MinimumConfidence - الإلغاء و
TimeoutMillisecondsتعاونيان؛ جلسة ONNX جارية تكتمل دائماً - طابق عرضة الـ DLL مع التطبيق ووضع CRT لمكتبتَي ONNX Runtime و OpenCV الثابتتين مع الـ DLL
- اختر ملفاً تعريفياً للغة لكل محرك بـ
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0)؛ محرك واحد لا يكتشف اللغات بنفسه
المُكيِّف الأصلي لـ RapidOCR ومُكيِّفات OCR القائمة على العمليات ومحرك عرض الصفحات الذي يغذيها وكاتب طبقة النص Unicode غير المرئية كلها تُشحن معاً في HotPDF، مكوّن VCL أصلي لـ PDF لـ Delphi و C++Builder. وإن كان تطبيق التقاط أو أرشفة مستندات يحتاج مخرجاً قابلاً للبحث دون بيئة Python على الجهاز الهدف، يوفر مكوّن HotPDF Delphi PDF component المسار كله بلا شيء يُنشر سوى الـ DLL ونماذجه