مقال تقني

‏OCR عبر DLL الـ Tesseract في HotPDF: نداء C API من Delphi

يشغّل HotPDF ‏Tesseract داخل عملية Delphi لديك عبر HPDFCreateTesseractDLLOCREngine، مصنع أُضيف في v2.772.0 يحمّل ديناميكياً DLL متوافقاً مع Tesseract 5، ويقود الـ C API عنده‏(TessBaseAPIInit2 و TessBaseAPIRecognize ومُكرِّر النتيجة)، ويعيد IHPDFOCREngine. وتستخدم THotPDF.ApplyLoadedOCRTextLayer ذلك المحرك لإضافة طبقة نص Unicode غير مرئية قابلة للبحث إلى صفحات PDF الممسوحة

وكان المُعَرِّف نفسه مبلغاً إليه عبر مُكيِّف tesseract.exe الخارجي الذي يكتب BMP ويحلل TSV. ذلك المسار يعمل، لكن كل صفحة تدفع ثمن إطلاق عملية وملف bitmap مؤقت وصيغة نص بلا خطوط أساس وبلا تحكم في تقسيم الصفحة. ونداء الـ DLL يزيل الثلاثة. كما يزيل جدار العملية، أي أن ربطاً بلغة Pascal يجلس فوق بنى C وقيم bool في C وسلاسل مخصصة من C مباشرةً. وأغلب ما يستحق المعرفة عن هذا المُكيِّف هو المواضع التي قد يخطئ فيها ذلك الربط بهدوء

كيف تشغّل Tesseract داخل العملية من Delphi عبر HotPDF؟

تشغيل Tesseract داخل العملية عبر HotPDF يأخذ نداء مصنع واحداً في وحدة HPDFTesseractRecognition ونداء ApplyLoadedOCRTextLayer نفسه الذي تستخدمه كل محركات OCR لدى HotPDF. ويتحقق المصنع بحماس. يجب أن يوجد ملف الـ DLL ومجلد tessdata، وأن يحوي معرّف اللغة حروفاً ASCII وأرقاماً و _ و + فقط، وأن يملك كل نموذج في تركيبة مثل chi_sim+eng ملف .traineddata مطابقاً، وأن تفاضل كل التصديرات المطلوبة الـ 21 قبل إعادة المحرك. أخطاء الضبط ترفع EArgumentException؛ و DLL يفشل تحميله يرفع EOSError مع كود خطأ Windows وتلميح بفحص البنية والاعتماديات

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // تطبيق ‏Win64 يحتاج DLL ذا ‏64-بت؛ وتقبع DLLs الاعتماديات بجواره
  Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
    'C:\OCR\tessdata', 'chi_sim+eng');   // ‏THPDFTesseractOptions.Default
  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,
      ' words accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

يضبط THPDFTesseractOptions.Default قيمة PageSegMode إلى tpsAuto، و EngineMode إلى temDefault، و TimeoutMilliseconds إلى 60,000 و MaxPixels إلى 16,777,216. وميزانية البكسل أهم مما تبدو. صفحة US Letter عند 300 DPI الافتراضية تعرض إلى 2,550 × 3,300 بكسل، نحو 8.4 مليون، فتتسع. والصفحة نفسها عند 600 DPI هي 5,100 × 6,600، نحو 33.7 مليون، فيرفضها المُكيِّف قبل أن يرى Tesseract بكسلاً. ارفع MaxPixels ‏(السقف 67,108,864) أو أبقِ الـ DPI كما هو؛ وكل جانب أيضاً مسقوف بـ 32,767 بكسل

ويُحمَّل الـ DLL بـ LoadLibraryEx بأعلام البحث الخاصة بمجلد الـ DLL نفسه زائد الدلائل الآمنة الافتراضية، فتستطيع مكتبات الصور التي يعتمد عليها Tesseract أن تسكن بجواره دون لمس PATH أو الدليل الحالي. ولا تحزم HotPDF ولا تنزّل أي بيئة تشغيل OCR أو نموذج؛ فأنت تزوّد الاثنين

ما الذي يتغير مقارنةً بمُكيِّف tesseract.exe؟

يقايض مُكيِّف الـ DLL عزلَ العملية بمخرج أغنى وعبءٍ لكل صفحة أدنى. كلا المُكيِّفَين يندمجان في مسار طبقة النص نفسه، فالتعيين الإحداثي وتصفية الثقة والتثبيت الكل أو لا شيء متطابقة؛ وما يختلف هو كيف تدخل البكسلات وتخرج الكلمات

الجانبمُكيِّف tesseract.exeمُكيِّف Tesseract ‏DLL
المصنعHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
البكسلات داخلملف BMP في دليل مؤقت خاصمخزن رمادي 8-بت في الذاكرة
الكلمات خارج‏TSV على مستوى الكلمة، مسقوف بـ 64 MiBمُكرِّر نتيجة، ‏UTF-8 لكل كلمة
خطوط الأساسغير متاحةتُمرر من TessPageIteratorBaseline
تقسيم الصفحة ووضع المحركالتقسيم التلقائي فقطTHPDFTesseractPageSegMode، THPDFTesseractEngineMode
المهلةقاسية: تُنهى العملية الابنةتعاونية: على Tesseract أن يلاحظ
عزل الانهيار والذاكرةعملية منفصلةلا شيء، يتشارك فضاء عناوينك

وتكلفة واحدة لا تختفي. كل نداء Recognize ينشئ نسخة API خاصة به ويستدعي TessBaseAPIInit2، فتُهيَّأ نماذج اللغة لكل صفحة لا مرة لكل محرك. مخزن الملفات في نظام التشغيل يلين إعادة التحميل، لكن على مجموعات نماذج كبيرة متعددة اللغات يظل التكلفة الثابتة المهيمنة لكل صفحة، ويُعَدّ مقابل مهلة التعرف. يأخذ محرك DLL الـ RapidOCR داخل العملية التصميم المعاكس ويبقي نماذج ONNX مقيمة طوال عمر المحرك؛ ومشكلات الحدود ‏(C ABI ومخازن مستعارة وعمل أصلي لا يُقاطَع) من العائلة نفسها

لماذا لا تستطيع Delphi نسخ بنية مراقب Tesseract؟

لا تستطيع Delphi عكس مراقب تقدم Tesseract بأمان لأن ETEXT_DESC يحوي حقولاً داخلية متغيرة بنسخة الإصدار، فسجل منسوخ يدوياً يضع callback الإلغاء والمهلة عند إزاحات خاطئة على بعض البناءات. ولا يفشل شيء بصوت عالٍ حين يحدث ذلك. يقرأ Tesseract ببساطة مؤشر callback الخاص بك من حقل صار يحمل شيئاً آخر، أو لا يرى المهلة إطلاقاً

لذلك تعامل HotPDF المراقب مؤشراً معتماً ولا تلمسه إلا عبر التوابع المصدَّرة: ‏TessMonitorCreate و TessMonitorSetCancelThis و TessMonitorSetCancelFunc و TessMonitorSetDeadlineMSecs و TessMonitorDelete. وإن ربطت الـ C API بنفسك لغرض آخر فالنمط نفسه يسري. الهيكل أدناه كود ربطك أنت لا API من HotPDF، وهو يعكس التصريحات التي تستخدمها HotPDF داخلياً

معالجة مراقب DLL الـ Tesseract لدى HotPDF: نسخ سجل ETEXT_DESC المتغير بنسخة الإصدار يضع callback الإلغاء والمهلة عند إزاحات خاطئة ويفشل بصمت، بينما تعامل HotPDF المراقب معتماً وتقود TessMonitorCreate و TessMonitorSetCancelThis و TessMonitorSetCancelFunc و TessMonitorSetDeadlineMSecs وتبقي الـ callback ذا cdecl بلا استثناءات
مؤشر معتم زائد خمس تصديرات هو العقد كله؛ ويبقى الـ callback قيمة Boolean من بايت واحد تقرأ علماً وساعة فقط
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ‏ETEXT_DESC*، لا تُفاضَل قط
  TTessMonitorDelete = procedure(Monitor: Pointer); cdecl;
  TTessMonitorSetCancelFunc = procedure(Monitor: Pointer; Func: TTessCancelFunc); cdecl;
  TTessMonitorSetCancelThis = procedure(Monitor, CancelThis: Pointer); cdecl;
  TTessMonitorSetDeadlineMSecs = procedure(Monitor: Pointer; MSecs: Integer); cdecl;
  TTessBaseAPIRecognize = function(Handle, Monitor: Pointer): Integer; cdecl;

  TOCRJob = record
    CancelRequested: Boolean;
    DeadlineTick: UInt64;
  end;
  POCRJob = ^TOCRJob;

function ShouldCancel(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
begin
  // يجري على مكدّس Tesseract: اقرأ الأعلام والساعة ولا ترفع أبداً
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// الاستخدام، مع مؤشرات التوابع المفاضَلة عبر ‏GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

تفصيلان في ذلك الهيكل مقصودان. يعيد الـ callback قيمة Boolean، وهي بايت واحد في Delphi و Free Pascal كليهما، مطابقةً لقيمة bool في C داخل TessCancelFunc. وقيمة BOOL ذية الأربع بايتات في Windows أو LongBool في Delphi تبدو قابلة للتبادل وليست كذلك: حين يكتب طرف بايتاً واحداً ويقرأ الآخر أربعة، تكون البايتات العليا من مسجل الإعادة كل ما تبقى فيه، وقد تصل قيمة false بوصفها true. ويعقّد الترويسةُ نفسها الأمور أكثر، لأن توابع مثل TessPageIteratorBoundingBox تعيد int، مصرّحاً عنها لدى HotPDF بوصفها Integer. اقرأ نوع C لكل قيمة عائدة بدل افتراض اصطلاح واحد للـ API كله

والتفصيل الثاني أن الـ callback لا يرفع أبداً. استثناء Delphi يفك تراصه عبر أُطَر ++C الخاصة بـ Tesseract سلوك غير معرّف، لذا يقرأ callback الخاص بـ HotPDF رمزَ الإلغاء وقيمة GetTickCount64 رتيبةَ العدّ فقط. ويحوّل المُكيِّف النتيجة إلى تشخيص إلغاء أو انتهاء مهلة بعد عودة TessBaseAPIRecognize، ويجري ذلك الفحص بغض النظر عن كود الإعادة الأصلي

أي مؤشرات أصلية يملكها جانب Delphi؟

يملك مُكيِّف Tesseract ‏DLL لدى HotPDF ثلاثة كائنات أصلية لكل طلب، نسخة الـ API والمراقب ومُكرِّر النتيجة، ويستعير كل ما عداهما. كل نداء Recognize ينشئ مجموعته الخاصة ويحررها في كتلة finally: ‏TessResultIteratorDelete ثم TessMonitorDelete ثم TessBaseAPIDelete. وإفلات واجهة المحرك يفرّغ المكتبة

ملكية كائنات DLL الـ Tesseract لدى HotPDF لكل نداء Recognize: مُكرِّر النتيجة والمراقب ونسخة الـ API مملوكة وتُحرَّر بهذا الترتيب داخل finally، ومُكرِّر الصفحة من TessResultIteratorGetPageIterator منظر مستعار يجب ألا يُحرَّر قط، وسلاسل GetUTF8Text تُنسخ وتُعاد عبر TessDeleteText
ثلاثة كائنات مملوكة وكل ما عدى ذلك مستعار: حُرِّر بالترتيب الثابت، ولا تحرر مُكرِّر الصفحة مرتين أبداً، ولا تخلط المخصِّصات أبداً
  • يعيد TessResultIteratorGetPageIterator منظرَاً مستعاراً داخل مُكرِّر النتيجة، لا كائناً جديداً. تستخدمه HotPDF لـ TessPageIteratorBoundingBox و TessPageIteratorBaseline ولا تحرره أبداً؛ وحذفه منفرداً كان سيحرر الذاكرة نفسها مرتين
  • يعيد TessResultIteratorGetUTF8Text سلسلة خصصتها بيئة تشغيل الـ DLL الخاصة. تنسخها HotPDF وتعيدها عبر TessDeleteText في كتلة finally؛ فـ FreeMem في Pascal كان سيحررها على الكومة الخاطئة
  • يُفك نص الكلمة بتحقق UTF-8 صارم ويُفحص طوله قبل التحويل. الكلمات ذات محارف التحكم أو UTF-8 المشوه أو الصناديق خارج الصورة أو المستطيلات المقلوبة أو الثقة خارج 0–100 تُسقط الطلبَ بدل ترقيعها بصمت
  • النص الكلي لكل طلب مسقوف بـ 1,048,576 وحدة كود UTF-16، ويجب أن يتسع عدد الكلمات في ميزانية الطلب المسلَّمة من ApplyLoadedOCRTextLayer

وتصل الثقة على مساحة 0–100 وتُكيَّف إلى 0–1، فتعني THPDFOCRTextLayerOptions.MinimumConfidence الشيءَ نفسه لكل محرك. وحين يبلّغ Tesseract خطَّ أساس تمرَّر طرفاه معاً؛ وإلا لجأ مسار طبقة النص إلى تقديره الهندسي، تماماً كما يفعل لدخل TSV

لماذا يتحقق من enum قبل أن يبلغ الـ DLL؟

تنسخ HotPDF الترتيبي الخام لقيم PageSegMode و EngineMode إلى Integer قبل فحص المدى، لأن المترجم قد يفترض أن متغير enum يحمل دائماً قيمة مصرّحاً عنها ويطوي Ord(X) > Ord(High(T)) إلى false ثابتة. والترتيبيات ليست زينة: يتبع THPDFTesseractPageSegMode ترقيم تقسيم الصفحات في Tesseract من 0 إلى 13، و THPDFTesseractEngineMode يتبع ترقيم وضع المحرك من 0 إلى 3، وكلاهما يذهب إلى الـ DLL أعداداً صحيحة مجردة. وسجل خيارات بُني بـ FillChar أو امتلأ من تيار أو مُرِّر من C++Builder بعدد صحيح مسبوك يستطيع أن يحمل بايتاً مثل 200. والتحقق من الترتيبي المنسوخ يحوّل ذلك إلى EArgumentException عند وقت المصنع بدل وضع غير معرّف داخل كود أصلي. ويرفض المصنع أيضاً tpsOSDOnly و tpsAutoOnly لأنهما لا ينتجان كلمات، ويتطلب osd.traineddata لقيمتَي tpsAutoOSD و tpsSparseTextOSD

ماذا تضمن مهلة التعرف فعلاً؟

مهلة Tesseract ‏DLL تعاونية: تستطيع HotPDF أن توقف عملها الخاص وأن تطلب من Tesseract أن يتوقف، لكنها لا تستطيع إجبار الكود الأصلي على العودة. يبدأ الساعة حين يبدأ Recognize، فتحويل الـ bitmap وتهيئة النموذج يستهلكان الميزانية نفسها التي يستهلكها التعرف. تفحص HotPDF الوقت المنقضي ورمز الإلغاء أثناء تحويل الرمادي وبين الكلمات أثناء تكرار النتائج، وتمرر الميلي ثواني المتبقية إلى TessMonitorSetDeadlineMSecs قبل استدعاء TessBaseAPIRecognize

والفجوة داخل النداء الأصلي. يُستشار مراقب Tesseract أثناء التعرف على الكلمات، لا أثناء TessBaseAPIInit2 أو تحليل تخطيط الصفحة، فقد يجري تحميل نموذج بطيء أو تخطيط مرضي بعد المهلة قبل أن تُبلَّغ. وميزانيتا البكسل والمخرج لا تسقفان أيضاً استهلاك الذاكرة الخاص بالمكتبة الأصلية نفسها. إن احتجت عاملاً تستطيع قتله فاستخدم مُكيِّف العملية؛ تلك هي المقايضة الصادقة لا ميزة ناقصة

تشريح المهلة التعاونية لـ DLL الـ Tesseract لدى HotPDF: تبدأ الساعة حين يبدأ Recognize وتغطي تحويل الرمادي و TessBaseAPIInit2 وتحليل التخطيط، لكن المراقب لا يُستشار إلا أثناء التعرف على الكلمات، فتستطيع تحميلات النماذج والتخطيط تجاوز المهلة قبل أن تبلّغ HotPDF ‏otlsEngineError أو otlsCancelled
المهلة هنا طلب لا ضمان: تستطيع التهيئة وتحليل التخطيط أن تطولا، وعامل تستطيع قتله حقاً يحتاج مُكيِّف العملية

وتقسيم الصفحة هو حيث يكسب مُكيِّف الـ DLL مقامه على الدخل الصعب. النماذج والتسميات والجداول الممسوحة بحقول متفرقة كثيراً ما تُعرَف أحسن بـ tpsSparseText من التقسيم التلقائي الذي يحاول تجميع أعمدة وفقرات غير موجودة

procedure OCRFormPages(Doc: THotPDF; const Pages: array of Integer);
var
  Engine: IHPDFOCREngine;
  TessOptions: THPDFTesseractOptions;
  LayerOptions: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  TessOptions := THPDFTesseractOptions.Default;
  TessOptions.PageSegMode := tpsSparseText;  // حقول متفرقة بلا تجميع أعمدة
  TessOptions.EngineMode := temLSTMOnly;     // يحتاج نماذج LSTM في tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // يشمل تهيئة النموذج
  Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
    'C:\OCR\tessdata', 'eng+deu', TessOptions);

  LayerOptions := THPDFOCRTextLayerOptions.Default;
  LayerOptions.MinimumConfidence := 0.6;
  if not Doc.ApplyLoadedOCRTextLayer(Pages, Engine, LayerOptions, Info) then
    case Info.Status of
      otlsCancelled:
        Writeln('OCR cancelled, document unchanged');
      otlsEngineError:
        Writeln('Tesseract failed or timed out: ', string(Info.Diagnostic));
    else
      Writeln(string(Info.Diagnostic));
    end;
end;

المهلة المنتهية تظهر otlsEngineError مع التشخيص Tesseract DLL OCR timed out، بينما يظهر الرمز الملغى otlsCancelled. وفي الحالتين تكون ApplyLoadedOCRTextLayer قد عرفت كل صفحة مختارة قبل أن تبدأ معاملة التثبيت، فيترك فشلُ الصفحة 40 من 50 المستندَ المحمّل كما كان تماماً. ولاحظ أن tpsSingleLine و tpsSingleBlock و tpsSparseText تغيّر التقسيم فقط؛ ولا شيء منها يستقيم مسحاً مائلاً

Free Pascal و Lazarus: بكسلات متقادمة وصينية مفقودة

كلا مصنعَي Tesseract يعملان في Free Pascal و Lazarus على Windows ببناءَي Win32 و Win64 منذ v2.772.1، بعد إصلاحين خاصين بـ FPC. أعد بناء حزمة Lazarus للبنية الهدف أولاً؛ والانتقال العام مغطى في ‏HotPDF على Free Pascal و Lazarus ‏Win64

الإصلاح الأول يخص البكسلات. إن TBitmap في LCL مكتوب عبر scanlines يستطيع تحديث صورته الخام دون تحديث مقبض bitmap الخاص بـ Windows، فـ GetDIBits على ذلك المقبض يعيد البكسلات القديمة. وكان العَرَض حيراً: نص مرسوم مباشرة على bitmap عُرف، بينما صفحة معروضة بمحرك PDF لدى HotPDF أنتجت قائمة كلمات فارغة. على FPC يقرأ المُكيِّف الآن لقطة واعية بالصيغة عبر CreateIntfImage، وهي تحترم صيغة بكسل الصورة الخام وترتيب صفوفها. ويبقي بناء Delphi مسار GetDIBits على نسخة خاصة 24-بت. ولا يعدّل أيٌّ من البناءين bitmap المستدعي

والإصلاح الثاني يخص مُكيِّف tesseract.exe. يخزّن TStringList في FPC سلاسل ANSI، فإسناد نص TSV المفكوك من UTF-8 إلى Lines.Text كان يُسقط بصمت كل محرف صيني أو من المستوى التكميلي لم تستطع صفحة كود ANSI للنظام تمثيله. يحتفظ مسار FPC الآن بـ TSV بوصفه بايتات UTF-8، ويسلخ الـ BOM على مستوى البايت ويفك كل كلمة إلى UnicodeString منفردة. لم يكن لمُكيِّف الـ DLL هذه المشكلة قط لأنه يفك كل كلمة مباشرة من المُكرِّر

مرجع سريع

  • المصنع: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) في HPDFTesseractRecognition، أضيف في v2.772.0، ودعم FPC في v2.772.1
  • الافتراضات: ‏tpsAuto، ‏temDefault، ‏60,000 ms، ‏16,777,216 بكسل؛ مدى المهلة 1–3,600,000 ms وسقف البكسل 67,108,864
  • طابق عرضة الـ DLL مع التطبيق وضعَ DLLs الاعتماديات بجوار DLL الـ Tesseract
  • عامل المراقب معتماً؛ لا تنسخ ETEXT_DESC إلى سجل Pascal أبداً
  • صرّح عن callback الإلغاء بـ cdecl ونتيجة Boolean من بايت واحد، ولا تدع استثناءً يهرب منه أبداً
  • حرّر نص المُكرِّر بـ TessDeleteText؛ ولا تحرر مُكرِّر الصفحة المأخوذ من مُكرِّر النتيجة أبداً
  • توقع أن تكون المهلة تعاونية: تستطيع تهيئة النموذج وتحليل التخطيط تجاوزها
  • استخدم مُكيِّف tesseract.exe حين تحتاج إنهاءً قاسياً أو عزل انهيارات

مُكيِّف Tesseract ‏DLL ومُكيِّفات العمليات ومحرك OCR المدمج كلها تُشحن مع مكوّن HotPDF Delphi PDF component لـ Delphi و C++Builder و Free Pascal؛ وانظر صفحة منتج HotPDF للإصدارات والتنزيلات