يحوّل HotPDF صفحات PDF الممسوحة إلى PDF قابل للبحث عبر Tesseract من خلال HPDFCreateTesseractOCREngine، مصنع يغلف ملفاً تنفيذياً لـ Tesseract مثبتاً محلياً بوصفه IHPDFOCREngine. تمرر ذلك المحرك إلى ApplyLoadedOCRTextLayer، التي تعرض كل صفحة، وتشغل Tesseract مرة عن كل صفحة، تفكك ناتجه TSV على مستوى الكلمات، وتوثق طبقة نص Unicode غير مرئية لكل الصفحات المطلوبة في عملية واحدة، أو لأي منها
سبب وجود هذا المهايئ هو النطاق. محرك OCR المدمج بمطابقة القوالب ضيق عمداً: حروف وأرقام ASCII مطبوعة آلياً، ولا شيء غير ذلك. الفواتير ذات الأسماء المشكولة بعلامات، والعقود الصينية، والأرشيفات متعددة اللغات تحتاج معرّفاً حقيقياً بنماذج لغة مدرَّبة، و Tesseract هو المرشح البديهي لأنه برنامج سطر أوامر يمكنك توفيره بجوار تطبيقك. استدعاء برنامج خارجي من مكتبة مستندات يبدو تافهاً. وهو ليس كذلك، ومعظم الكود المثير في المهايئ يتعلق بما يحدث حين يسلك البرنامج سلوكاً سيئاً، أو يعلق، أو يُلغى، أو يرث أشياء لا ينبغي أن يراها أبداً
كيف يقود HotPDF Tesseract من تطبيق Delphi؟
يشغّل HotPDF Tesseract عمليةً ابنة خفية عن كل صفحة، يغذيها صورة نقطية معروضة ويقرأ منها ملف TSV، ويعرض النتيجة عبر شق IHPDFOCREngine نفسه الذي يستخدمه المحرك المدمج. لا شيء في اتجاه المصب يتغير: مطابقة الإحداثيات، ومعالجة الدوران، والتحقق من Unicode، وترشيح الثقة، والتوثيق الذري هي خط أنابيب طبقة النص الذي لديك بالفعل. يقع المصنع في وحدة HPDFTesseractRecognition ويتحقق بحرص: يجب أن يوجد الملف التنفيذي، ويجب أن يوجد دليل tessdata، ويجب أن تكون المهلة بين 1 و 3,600,000 ميلي ثانية، ولا يجوز لمعرف اللغة أن يحوي سوى حروف ASCII وأرقام و _ و +. ذلك الفحص الأخير مهم لأن سلسلة اللغة تنتهي على سطر أوامر، و eng+chi_sim قيمة مشروعة في Tesseract بينما أي شيء بعلامات اقتباس أو فراغات ليس كذلك
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// يرفع EArgumentException لملف تنفيذي مفقود أو tessdata مفقودة،
// أو معرف لغة سيئ، أو مهلة خارج 1..3600000 ms
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // عدة نماذج موصولة بـ '+'
120000); // حد لكل صفحة، الافتراضي 60000
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
Options.CancellationToken := Token;
// قائمة صفحات فارغة تعني كل صفحة؛ الصفحات ذات النص تُتخطى افتراضياً
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
begin
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
end
else
case Info.Status of
otlsCancelled: Writeln('Cancelled, document unchanged');
otlsEngineError: Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
finally
Doc.Free;
end;
end;
عن كل صفحة تنشئ Recognize دليلاً خاصاً تحت مسار temp باسم HotPDF-OCR-{GUID}، وتحفظ الصورة النقطية المعروضة input.bmp، وتطلق tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1، مع اقتباس كل وسيط مسار بقواعد الهرب سطر الأوامر في Windows للمائلات الخلفية وعلامات الاقتباس المضمّنة. قيمة --dpi هي DPI للعرض من THPDFOCRTextLayerOptions.DPI، فلا يضطر Tesseract أبداً لتخمين الدقة من بيانات الصورة الوصفية، و --psm 3 يطلب تقسيماً صفحياً آلياً تاماً. ويعلن المحرك نفسه Tesseract (local CLI)، وهو ما يحل في Info.EngineName. Tesseract ونماذج لغاته ليستا محزومتين مع HotPDF؛ وتثبيتهما شأن التطبيق
لماذا هو مفكك TSV بهذه الصرامة؟
يفشل مفكك TSV في HotPDF الصفحة كلها عند أي صف مشوه، لأن قائمة كلمات مفككة جزئياً تنتج طبقة نص تخالف الصورة بصمت. ناتج TSV من Tesseract ترويسة ثابتة من اثني عشر عموداً، من level إلى text، ويقارن HotPDF السطر الأول بتلك الترويسة الحرفية بعد إزالة علامة ترتيب بايتات اختيارية. كل سطر لاحق يجب أن ينقسم إلى اثني عشر حقلاً بالضبط، ويتوقف التقسيم بعد علامة الجدولة الحادية عشرة حتى تبقى علامة جدولة داخل النص المعترف به جزءاً من الكلمة بدل أن تنشئ عموداً ثالث عشر. صفوف المستوى 5 وحدها كلمات؛ أما المستويات من 1 إلى 4 فتصف صفحات وكتل وفقرات وأسطر، وتُتخطى. وصفوف المستوى 5 التي نصها فارغ أو فراغ أبيض صرف تُتخطى هي الأخرى، لأن الكلمة الفارغة لها صندوق ولا شيء ليُحدد موضع أو يُبحث. كل ما عداه يُفحص بحدّة: هندسة عددية، وثقة مفكوكة بتنسيق en-US ثابت حتى لا تقرأ البيئة الألمانية 93.5 خردة، وصندوق يقع كلياً داخل الصورة النقطية، وثقة بين 0 و 100. فشل واحد يرفع استثناء، ويعيد المحرك False، وتصفَّر مصفوفة الكلمات. واختبارات الانحدار تضم تلك الحالة بالذات: كلمة صحيحة واحدة يليها صف مكسور يجب أن تعطي صفر كلمات، لا واحدة
// مكثف من حلقة المستوى 5 في HPDFLocalTSVRecognition
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue; // صفوف صفحة/كتلة/فقرة/سطر
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // كلمات الفراغ بلا موضع
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
not TryStrToFloat(Fields[10], Confidence, Settings) then
raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
(Int64(X) + W > Request.Bitmap.Width) or
(Int64(Y) + H > Request.Bitmap.Height) or
not ((Confidence >= 0) and (Confidence <= 100)) then
raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100; // خط الأنابيب يتوقع 0..1
يتفاعل ذلك السطر الأخير مع افتراض قد لا تتوقعه. ثقة Tesseract تمتد من 0 إلى 100، وخط الأنابيب يعمل من 0 إلى 1، و THPDFOCRTextLayerOptions.MinimumConfidence افتراضها 0.5، فكل كلمة Tesseract دون 50 تُحسب في Info.DroppedWordCount ولا تصل الصفحة أبداً. على مسحة نظيفة بـ 300 DPI ذلك أرضية معقولة. وعلى فاكس مشوش يمكن أن يُسقط حصة مفاجئة من الصفحة، والتحرك الصحيح أن تنظر إلى عدد المسقوطين قبل خفض العتبة، فالكلمات منخفضة الثقة هي بالضبط الأكثر احتمالاً أن تكون خاطئة
ماذا ترث عملية Tesseract الابنة؟
ترث عملية Tesseract الابنة مقبضين بالضبط من HotPDF: مقبض NUL للمدخل والخرج القياسيين، ومقبض ملف للخطأ القياسي. تلك الدقة هي المغزى. CreateProcess بـ bInheritHandles = True هي طريقك لتمرير المقابض القياسية إلى ابنة، لكنها وحدها تمرر كل مقبض قابل للوراثة في عملية المضيف، بما فيه الملفات والأنابيب والأحداث التي فتحها كود غير ذي صلة في تطبيقك. وتبقي الابنة تلك الكائنات حية حتى تخرج، فيبقى ملف مقفلاً أو يرى أنبوب نهايته أبداً بينما تطحن Tesseract صفحة. يغلق HotPDF تلك الفجوة بسجل بدء ممتد: STARTUPINFOEX، وقائمة خصائص تحمل PROC_THREAD_ATTRIBUTE_HANDLE_LIST، وعلم الإنشاء EXTENDED_STARTUPINFO_PRESENT. وبوجود قائمة المقابض ما زال يجب أن يكون bInheritHandles True، لكن المقابض المسرودة وحدها تعبر الحد. نفس التفكير الاحتوائي يقود عزل مرمزات صور PDF في عمليات عاملة، حيث تكون الابنة كوداً غير موثوق؛ وهنا الابنة موثوقة، لكن المضيف ليس المالك الوحيد لجدول مقابضه
// الثوابت معروضة بالاسم؛ ويمرر المصدر قيمها العددية
// كلا المقبضين ينشأ بـ bInheritHandle = True
InheritedHandles[0] := NullHandle; // stdin و stdout
InheritedHandles[1] := ErrorHandle; // stderr.txt في الدليل الخاص
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
@InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
True, // يشترطه قائمة المقابض
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
لماذا قد تبدو جولة OCR ملغاة وكأنها فشل محرك؟
تبدو جولة OCR ملغاة وكأنها فشل محرك لأن IHPDFOCREngine.Recognize تعيد Boolean واحداً، و False تعني «فشلت Tesseract» و«ضغط المستخدم إلغاء» معاً. يستقصي المهايئ رمز الإلغاء والمهلة كل 25 ميلي ثانية ما دامت الابنة تعمل، وحين ينطلق الرمز يرفع استثناء داخل Recognize، ويلتقط استثناءه الخاص، وينظف، ويعيد False مع تشخيص. ولو عامل خط الأنابيب ذلك خطأ محرك، لرأى المستدعي otlsEngineError لمهمة أوقفها المستخدم عمداً. لذا يفحص ApplyLoadedOCRTextLayer الرمز أولاً كلما أعادت Recognize False، ولا يحول النتيجة إلى فشل محرك إلا إذا لم يكن الرمز مضبوطاً. ذلك الترتيب يحفظ عقد الصفحات المتعددة: التعرف والتحقق ومحاسبة الميزانية وبناء المحتوى تجري لكل صفحة مطلوبة قبل أن تنفتح عملية الرسمية، فإلغاء في الصفحة 40 من 50 يبلّغ otlsCancelled ويترك المستند، بما فيه الصفحات التسع والأربعين الأولى، بلا مساس. لا ملف قابل للبحث جزئياً ليُشرح لاحقاً، وبقية معالجة الفشل تسير بالأسلوب المحدود نفسه:
- المهلة عن كل استدعاء
Recognize، تُقاس من بدايته، فالافتراضي 60,000 ms ينطبق على كل صفحة لا على المستند كله - ابنة ما زالت تعمل عند المهلة أو الإلغاء تُنهى، ويُنتظرها حتى 5 ثوانٍ، ويُحذف دليلها الخاص في كتلة
finally - يُسقَّف
output.tsvعند 64 MiB وstderr.txtعند 1 MiB، والفحص يجري ما دامت الابنة تعمل وبعد خروجها كذلك - عدد الكلمات ووحدات كود UTF-16 تُسقَّف عن كل صفحة بميزانيات
MaxWordsPerPageوMaxTotalWordsوMaxTextCodeUnitsالمتبقية، وتجاورها تفشل الجولة بدل بتر قائمة الكلمات - يذهب الخرج القياسي إلى
NULلأن Tesseract يكتبoutput.tsv، بينما يذهب الخطأ القياسي إلى ملف حتى يبلّغ كود خروج غير صفري بما يصل إلى 4,096 محرفاً من شكوى المحرك الخاصة، وهي عادة أسرع طريق لمعرفة أن ملف.traineddataمفقود
كيف تصير الكلمات المعترف بها طبقة نص غير مرئية
تكتب HotPDF كلمات Tesseract نصاً غير مرئي بنمط عرض نص 3، نمط لا تعبئة ولا تحديد المعرف في §9.3.6 من ISO 32000-1، فتبقى الصفحة تعرض الصورة الممسوحة بينما يعمل البحث والنسخ على الكلمات المعترف بها. يفتح تدفق المحتوى BT بـ 3 Tr، وتحصل كل كلمة على مصفوفة Tm عند خط أساسها، وحجم خط مشتق من ارتفاع الصندوق بالبكسل عند DPI العرض، ومقياس أفقي Tz يمدد جري الحروف إلى عرض الصندوق المقاس، ولهذا تهبط إبرازة البحث على الكلمة في الصورة بدل أن تنجرف عبرها
ناتج TSV من Tesseract له صناديق بلا خطوط أساس، فيبلّغ المهايئ كل كلمة بلا خط أساس ويقدّر خط الأنابيب الأساس عند خُمس ارتفاع الصندوق فوق الحافة السفلية. النص نفسه يمر بخط Type0 غير مضمّن مشترك بترميز Identity-H وخريطة ToUnicode مولدة، CID واحد لكل عدد Unicode مميز عبر الجري كله، وهكذا تنجو المحارف الصينية واللاتينية المشكولة ومحارف المستوي المتمم جميعاً في النسخ والبحث. لذلك التصميم حدّان يستحقان الذكر مقدماً: الجري الواحد يحمل 65,535 عدداً مميزاً على الأكثر، والخط غير المضمّن لا يستوفي شرط تضمين الخطوط في ISO 19005، فمخرجات PDF/A تحتاج خطاً مطابقاً مضمّناً منفصلاً. وفحص النتيجة بسيط ويستحق الأتمتة: احفظ، وأعد التحميل، وشغّل مسار نص المستند المحمّل العادي من استخراج النص من PDF محمّل في Delphi؛ إن عادت الكلمات في الصفحات المتوقعة فالطبقة حقيقية
RapidOCR ومحركات أخرى على بروتوكول TSV نفسه
يعيد HotPDF استخدام مشغّل العملية ومفكك TSV نفسيهما لـ RapidOCR عبر HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds)، وهو الخيار أنفع لمسحات الصينية المبسطة. سطر الأوامر متطابق إلا أن مسار سكربت الجسر يُدرج بعد الملف التنفيذي لـ Python، واللغة مثبتة على chi_sim. تشحن HotPDF الجسر بوصفه tools/OCR/rapidocr_tsv.py؛ ويتوقع حزمتي rapidocr و onnxruntime زائد ثلاثة نماذج ONNX محلية، ويعطّل التنزيل التلقائي للنماذج، ويكتب TSV بشكل Tesseract حتى لا يحتاج جانب Delphi مفككاً ثانياً. واسم المحرك المبلَّغ في Info.EngineName هو RapidOCR (local ONNX). يوحي ذلك الشكل بالوصفة العامة: أي معرّف تستطيع تغليفه في سكربت صغير يقبل قائمة وسائط على نمط Tesseract ويطلق TSV ذا الأعمدة الاثني عشر يرث عزل المقابض والمهلة والإلغاء وميزانيات الخرج والتوثيق الكل-أو-لا-شيء مجاناً. المهايئات لـ Windows فقط، وتشغل صفحة واحدة في كل مرة تزامنياً، ولا تصحح الميل أو تعالج الصورة مسبقاً أكثر مما ينتجه العارض، فجودة الصورة الداخلة ما زالت ترسم سقف ما يخرج
مهايئا Tesseract و RapidOCR وكاتب طبقة النص غير المرئية وعارض الصفحات الذي يغذيهما واستخراج النص الذي يتحقق من النتيجة كلها تشحن في مكون VCL الأصيل نفسه لـ Delphi و C++Builder. وإن كنت تضيف OCR إلى تطبيق التقاط مستندات أو أرشفة، يمنحك مكون PDF من HotPDF لـ Delphi خط الأنابيب وبقي علىك تثبيت محرك OCR نفسه فقط