تجري HotPDF الـ OCR للصينية ولغات متعددة في Delphi عبر مُكيِّفها الأصلي لـ DLL الـ RapidOCR: THPDFRapidOCRDLLOptions.ForLanguage تربط وسم لغة مثل 'zh-CN' أو 'zh-TW' أو 'ru' أو 'ar' بنموذج تعرف وقاموس محارف متطابقَين، وتحوّل THotPDF.ApplyLoadedOCRTextLayer الأسطر المعروفة إلى طبقة نص Unicode غير مرئية قابلة للبحث على صفحات PDF الممسوحة
جعل عرض توضيحي بحروف لاتينية يعمل هو الجزء السهل. الإخفاقات المثيرة تبدأ حين تنتقل إلى الصينية التقليدية أو الروسية فيتحول المخرج إلى هراء واثق مُحكم التكوين، أو حين يفقد كل سطر محرفه الأخير بصمت، أو حين تعود صفحة عربية بصناديق نصها بترتيب خاطئ. لا شيء من ذلك يرفع استثناءً بذاته. أتت الملفات التعريفية اللغوية المضافة في HotPDF v2.775.0 أساساً لإغلاق تلك الفجوات، والأربعة فخوص أدناه تستحق الفهم حتى لو لم تلمس الكود الأصلي قط، لأن كل واحد منها يفسر عَرَضاً كان في غير ذلك سيطاردك يوماً كاملاً
كيف تختار ForLanguage نموذجاً وقاموساً؟
تفاضل THPDFRapidOCRDLLOptions.ForLanguage وسمَ إلى واحد من تسعة ملفات تعريفية وتعيد خيارات تشير إلى <profile>/recognition.onnx و <profile>/dictionary.txt تحت مجلد نماذجك، مع إبقاء الكاشف المشترك والمصنِّف الاختياري للزاوية وافتراضات الخيوط والبكسل والمهلة من THPDFRapidOCRDLLOptions.Default. يصغّر التابع الوسم حروفه ويحول الشرطات السفلية إلى شرطات ويجز محارف الفراغ المحيطة، فتقبع 'zh_TW' و 'ZH-tw' و ' zh-tw ' جميعها على الملف التعريفي نفسه. والأسماء البديلة قائمة صريحة لا مطابقة بادئة: 'zh-Hant-TW' مقبولة لأنها مسردة، بينما صيغة إقليمية اعتباطية غير مسردة ترفع EArgumentException قبل تحميل أي نموذج
| الملف التعريفي | اللغات | أوسمة مثال | النموذج المثبت |
|---|---|---|---|
ch | الصينية المبسطة والإنجليزية | zh، zh-CN، zh-Hans، chi_sim | PP-OCRv4 |
chinese_cht | الصينية التقليدية | zh-TW، zh-HK، zh-Hant، chi_tra | PP-OCRv3 |
en | الإنجليزية | en، en-US، en-GB، eng | PP-OCRv4 |
latin | الفرنسية والألمانية والإسبانية والبرتغالية والإيطالية والهولندية والتركية | fr، de، es-419، pt-BR، tr | PP-OCRv3 |
japan | اليابانية | ja، ja-JP، jpn | PP-OCRv4 |
korean | الكورية | ko، ko-KR، kor | PP-OCRv4 |
cyrillic | الروسية والأوكرانية والبلغارية والبيلاروسية | ru، ru-RU، uk، bg | PP-OCRv3 |
arabic | العربية والفارسية والأردية | ar، ar-SA، fa، ur | PP-OCRv4 |
devanagari | الهندية والماراثية والنيبالية | hi، mr، ne | PP-OCRv4 |
والمُكيِّف نفسه لا ينزّل شيئاً قط. تزوَّد الملفات مرة واحدة بالمساعد المرفق، مثل tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (أو -Language All للملفات التعريفية التسعة كلها)، ويلحق المساعد كاشفاً ومصنِّفاً مشتركين عند أسماء الملفات الجذرية التي تتوقعها Default. بعدها يصير مسح صيني مبسط قابلاً للبحث في بضعة أسطر. وسباكة المحرك هي مفصل IHPDFOCREngine نفسه الموصوف في مقالة DLL الـ RapidOCR داخل العملية وحدّ ABI عندها، فتبقى هذه المقالة مركزة على اللغات
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeChineseScanSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Models: THPDFRapidOCRDLLOptions;
Layer: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// ch/recognition.onnx + ch/dictionary.txt، كاشف ومصنِّف مشتركان
Models := THPDFRapidOCRDLLOptions.ForLanguage('zh-CN');
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Layer := THPDFOCRTextLayerOptions.Default; // 300 DPI، MinimumConfidence 0.5
// قائمة صفحات فارغة تعني كل الصفحات؛ والصفحات ذات نص مسبق تُتخطى
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' lines, ', Info.UniqueScalarCount, ' distinct characters');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
تفصيلان في ذلك المخرج يستحقان ملاحظة. يعيد المسار الأصلي نتيجة واحدة لكل سطر نص مكتشف لا لكل كلمة، فتعدّ AcceptedWordCount أسطراً هنا، وتُقارن MinimumConfidence بمتوسط ثقة محارف السطر كله: سطر متوسطه 0.45 يُسقط وحدةً واحدة. وتبلّغ UniqueScalarCount كم محرف Unicode مميز اضطرت طبقة النص إلى تعيينه في خطها وجدول ToUnicode عندها، وهو فحص عقلي مفيد بأن نص CJK وصل فعلاً بدل حَفنة من الاحتياطات اللاتينية. وأبقِ واجهة المحرك حية عبر المستندات، لأن تهيئة النموذج تجري في المصنع وهي الخطوة المكلفة
لماذا ينتج تبديل نموذج التعرف وحده هراءً؟
نموذج تعرف CTC لا يُخرج محارف قط، بل فهارس أصناف فقط، والقاموس هو الشيء الوحيد الذي يحول الفهرس 1,204 إلى رمز. استبدل ch/recognition.onnx بـ cyrillic/recognition.onnx مع إبقاء القاموس الصيني، وسيصدر النموذج بكل سرور فهارس سيريلية صالحة يترجمها القاموس القديم إلى محارف هان عشوائية. والنتيجة تبدو نصاً، وتجتاز تحقق UTF-8، وقابلة للبحث عن لا شيء تماماً. ولهذا تضبط ForLanguage دائماً RecognitionModel و CharacterDictionary معاً، ولهذا لا ينبغي أبداً للخيارات المبنية يدوياً أن تغيّر أحدهما دون الآخر
وفحص السلامة البديهي، مقارنة حجم القاموس بعرض مخرج النموذج، ضروري لكنه غير كافٍ. قاموسان قد يملكان العدد نفسه من المدخلات بترتيب مختلف، وانزياح بمقدار واحد في الترتيب يزيح كل محرف نقطةَ كود واحدة. لذلك تفحص HotPDF على مرحلتين حين يهيئ المصنع النموذج. أولاً يجب أن يساوي عدد أصناف المخرج مدخلات القاموس زائد اثنين. وثانياً حين يضمّ ملف ONNX قائمة بيانات وصفية باسم character تُقارن كل مدخل قاموس معها بالترتيب، ويُفشل عدمُ تطابق التهيئةَ بـ EInvalidOperation وتشخيص أصلي بدل إنتاج هراء مقنع لاحقاً
و "زائد اثنين" يأتيان من تخطيط الأصناف. الصنف 0 هو الفراغ CTC، والأصناف من 1 إلى N هي أسطر القاموس بترتيب الملف، والصنف الأخير مسافة. وبعض القواميس تحمل مدخل مسافة خاصاً بها أيضاً، ويجب إبقاء ذلك السطر كما هو تماماً. هنا يُحدث Trim حسن النية ضرراً حقيقياً: إنه يحول مدخل مسافة مفردة إلى سلسلة فارغة ويزيح الجدول أو يكسره. والتطبيع الوحيد الآمن هو إزالة إرجاع سطر لاحق، فيُحمَّل قاموس محفوظ بنهايات أسطر CRLF صحيحاً، بينما تُرفض علامة ترتيب بايتات UTF-8 أو سطر فارغ أو مدخل يحوي جدولة. يُظهر الهيكل أدناه التخطيط بلغة Pascal؛ إنه كود توضيحي لا API من HotPDF
// للتوضيح فقط: جدول الأصناف الذي يتوقعه مُعَرِّف CTC
uses
SysUtils, IOUtils;
function BuildCTCClassTable(const FileName: string): TArray<string>;
var
Text, Entry: string;
Lines: TArray<string>;
I, Last: Integer;
begin
Text := TEncoding.UTF8.GetString(TFile.ReadAllBytes(FileName));
if (Text <> '') and (Text[1] = #$FEFF) then
raise EArgumentException.Create('Dictionary must be UTF-8 without a BOM');
Lines := Text.Split([#10]);
Last := High(Lines);
if (Last >= 0) and (Lines[Last] = '') then
Dec(Last); // سطر جديد عند نهاية الملف
SetLength(Result, Last + 3);
Result[0] := ''; // الصنف 0: الفراغ CTC
for I := 0 to Last do
begin
Entry := Lines[I];
if (Entry <> '') and (Entry[Length(Entry)] = #13) then
SetLength(Entry, Length(Entry) - 1); // CRLF: أسقط الـ CR فقط
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // لا Trim أبدا: الفراغ ' ' صنف
end;
Result[Last + 2] := ' '; // الصنف الأخير: مسافة
// يجب أن يساوي Length(Result) عدد أصناف مخرج النموذج
end;
ماذا يفعل فك CTC الجشع فعلاً؟
يفك فك CTC الجشع الصنفَ الأعلى نقاطاً عند كل خطوة زمنية، ويطوي التكرارات المتتالية في محرف واحد، ويسقط صنف الفراغ؛ والفراغ هو ما يسمح لحروف مضاعفة حقاً بأن تنجو. ينظر نموذج التعرف إلى سطر نص بوصفه سلسلة شرائح رأسية ضيقة، ولكل شريحة، أي خطوة زمنية، يخرج احتمالاً لكل صنف. سطر يحوي AA中 قد ينتج تسلسل argmax A A blank A 中 space. فطي خطوتَي A الأولتين يعطي A واحدة، والفراغ يفصلها عن A التالية، والنتيجة AA中 والمسافة اللاحقة سليمة. وبلا قاعدة الفراغ لتكن book و bok غير قابلتين للتمييز
ولأن المفكك بضعة عشر سطراً فقط، من السهل أن تخطئ الحدود، والإخفاقات صامتة. إن توقف حلقة argmax الداخلية صنفاً قبيل النهاية فلن يفوز صنف المسافة أبداً ويعود كل سطر بلا تباعد كلمات، وهو ما يخرب بحث العبارات على صفحات الإنجليزية واللاتينية. وإن توقفت الحلقة الخارجية خطوة زمنية قبيل النهاية اختفى المحرف الأخير من كل سطر، وهو في سطر قصير قد يكون ثلث النص. وإن لم يُصفِّر الفراغُ حارسَ التكرار فطوت محارف مضاعفة مثل ll أو مضاعفات صينية مثل 谢谢 إلى واحد. يُدرِج مفكك HotPDF الصنف الأخير والخطوة الزمنية الأخيرة، ويبقي التكرارات المفصولة بفراغ، ويرفض إضافةً النقاط غير المحدودة أو الخارجة عن 0 إلى 1، وأي عدد أصناف لا يطابق القاموس. وهذا هو المنطق نفسه بوصفه توضيحاً بلغة Pascal
// للتوضيح فقط: فك CTC جشع بحدود صحيحة.
// تحمل Scores احتمالات Steps * Classes، صفاً واحداً لكل خطوة زمنية
function GreedyCTCDecode(const Scores: array of Single;
Steps, Classes: Integer; const Characters: array of string): string;
var
Step, C, Best, Previous: Integer;
BestScore: Single;
begin
if (Classes < 3) or (Length(Characters) <> Classes) or
(Length(Scores) <> Steps * Classes) then
raise EArgumentException.Create('Model output does not match the dictionary');
Result := '';
Previous := 0; // الصنف 0 هو الفراغ CTC
for Step := 0 to Steps - 1 do // أدرِج الخطوة الزمنية الأخيرة
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // أدرِج الصنف الأخير (المسافة)
if Scores[Step * Classes + C] > BestScore then
begin
Best := C;
BestScore := Scores[Step * Classes + C];
end;
if (Best <> 0) and (Best <> Previous) then
Result := Result + Characters[Best];
Previous := Best; // الفراغ يصفّر حارس التكرار
end;
end;
والفك الجشع ليس أدق استراتيجيات CTC المتاحة؛ فبحث beam مع نموذج لغة يصلح بعض الشرائح الملتبسة. أما للمستندات المطبوعة عند 300 DPI فنتيجة الجشع عادة هي ما لدى النموذج أن يعطيه، والمفكك ليس المكان لتعويض ضعف النموذج. مثلاً يستطيع نموذج Latin PP-OCRv3 أن يقرأ ñ بوصفها n حتى على دخل نظيف. لا تخفي HotPDF ذلك باستبدالات محارف في المعالجة اللاحقة، لأن جدول استبدال يصلح الإسبانية يكسر شيئاً آخر، ومحرف خاطئ في طبقة قابلة للبحث أسوأ من إخفاق صريح
كيف ترتب HotPDF أسطر النص، بما فيها العربية من اليمين إلى اليسار؟
ترتب HotPDF صناديق النص المكتشفة من أعلى إلى أسفل، وتجمع الصناديق في صف حين تتداخل رأسياً بنصف ارتفاع الصندوق الأصغر على الأقل، وترتب كل صف من اليسار إلى اليمين، أو من اليمين إلى اليسار حين تُفعَّل RightToLeft؛ ولا تُعكس محارف كل سطر معروف قط. والتجميع مهم لأن الكاشف كثيراً ما يقسم سطراً بصرياً واحداً إلى عدة صناديق، تسمية وقيمة تفصلهما فجوة عريضة مثلاً، وفرز صافٍ بالإحداثي العلوي كان سيشابكها مع السطر المجاور كلما اختلفت قممهما ببكسل أو بكسلين
يضبط الملف التعريفي العربي RightToLeft := True، وهو يخبر الـ DLL أن يرتب صناديق كل صف بحافته اليمنى، من الهامش الأيمن إلى الداخل. وهذا هو الأثر كله. فالنص الذي يعيده النموذج لسطر هو أصلاً بترتيب Unicode المنطقي، الترتيب الذي يقرؤه كاتب عربي ويكتبه، وهو أيضاً الترتيب الذي تتوقعه استخراج نص PDF والبحث. وقلب السلسلة ميكانيكياً لتجعلها "تبدو صحيحة" في مصحح أخطاء يكسر البحث والنسخ واللصق وقارئات الشاشة. أما العرض ثنائي الاتجاه وتشكيل الرموز فمن شأن العارض
محرك واحد يخدم ملفاً تعريفياً لغوياً واحداً. لا كشف تلقائي للكتابة، فالمستند الذي يخلط أنظمة كتابة يحتاج محركاً لكل ملف تعريفي، مطبقاً على الصفحات التي تستخدمه. ولأن ApplyLoadedOCRTextLayer تأخذ قائمة صفحات صريحة وتثبت كل نداء معاملةً مستقلة من نوع الكل أو لا شيء، فالأمر بسيط
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// ترفع EArgumentException لوسم مجهول قبل تحميل أي نموذج
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // سعة لصفحات A3 عند 300 DPI
Result := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
end;
procedure OCRMixedArchive(Doc: THotPDF);
var
Chinese, Arabic: IHPDFOCREngine;
Layer: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
Chinese := CreateRapidEngine('zh-TW'); // الملف التعريفي chinese_cht
Arabic := CreateRapidEngine('ar-SA'); // الملف التعريفي arabic، RightToLeft = True
Layer := THPDFOCRTextLayerOptions.Default;
if not Doc.ApplyLoadedOCRTextLayer([0, 1, 2], Chinese, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
if not Doc.ApplyLoadedOCRTextLayer([3], Arabic, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
end;
سطر MaxPixels موجود لسبب. تفترض خيارات الـ DLL افتراضياً 16,777,216 بكسل لكل طلب، وهو يغطي A4 و US Letter عند 300 DPI باتساع، لكن صفحة A3 عند 300 DPI هي نحو 3508 في 4961 بكسل، أي قرابة 17.4 مليوناً، فيُرفض الطلب بوصفه فوق الميزانية. ارفع MaxPixels (السقف 67,108,864) أو اخفض THPDFOCRTextLayerOptions.DPI للمقاسات الكبيرة. وترتيب اليمين إلى اليسار يستخدم تصدير HPDFRapidOCRSetReadingDirection الاختياري لنسخة ABI رقم 1؛ ولا يتطلبه المُكيِّف إلا عند ضبط RightToLeft، فيواصل DLL أقدم خدمةَ اللغات من اليسار إلى اليمين ويفشل عند إنشاء المحرك بـ EArgumentException تسمي التصدير المفقود للعربية
لماذا تفشل نماذج OCR الأحدث في التحميل؟
تربط DLL الـ RapidOCR لدى HotPDF نسخة ONNX Runtime 1.14 ثابتة، لا تستطيع قراءة نماذج حُفظت بنسخة ONNX IR رقم 10، وقد تتطلب الصادرات الأحدث مثل نماذج PP-OCRv5 بيئة تشغيل أحدث من ذلك؛ فيفشل ذلك النموذج عند إنشاء المحرك بتشخيص أصلي. وقيودُها هي سبب تثبيت حزم اللغات على أزواج مُعَرِّف وقاموس محددة من PP-OCRv3 و PP-OCRv4 بدل "الأحدث"، ولماذا يخلط الجدول أعلاه الجيلين: كل زوج مثبت هو زوج يُحمَّل ويُتحقق منه تحت تلك البيئة
ويفرض المثبِّت الاقتران. كل ملف في بيانه يحمل hash من نوع SHA256، وملف موجود به مختلف يوقف التثبيت بدل الكتابة فوقه، وكل تنزيل يهبط باسم مؤقت ولا ينتقل إلى مكانه إلا بعد مطابقة hash عنده. يحمي ذلك من النسخة الهادئة من مشكلة القاموس: أحدهم يُسقط recognition.onnx أحدث في مجلد ملف تعريفي يدوياً، فيصادف أن عدد الأصناف يطابق، ولا يفشل شيء حتى يبلّغ عميل أن البحث لا يجد كلمات يراها بأم عينه. وفي وقت التشغيل يبقى المُكيِّف دون اتصال ولا يجلب نموذجاً مفقوداً أبداً. ويتحقق المُعَرِّف أيضاً من شكل النموذج عند التحميل، مقبلاً دخل NCHW بارتفاع مثبت 32 أو 48 بكسل أو بارتفاع ديناميكي يجريه عند 48
وإن احتجت نظام كتابة لا تغطيه التسعة ملفات تعريفية فلا يزال بوسك أن تشير بـ RecognitionModel و CharacterDictionary إلى ملفاتك الخاصة. تسري الفحوص نفسها، وهذا هو الهدف: الزوج غير المتطابق يفشل عند التهيئة، لا في أرشيف عميلك. وللصفحات التي لا يناسبها أي ملف تعريفي من RapidOCR، يندمج مُكيِّف Tesseract لـ PDF قابل للبحث في نداء ApplyLoadedOCRTextLayer نفسه، وللنماذج ASCII المطبوعة آلياً لا يحتاج محرك OCR المدمج بمطابقة القوالب نماذج إطلاقاً
مرجع سريع: قائمة تحقق RapidOCR متعدد اللغات
- أنشئ الخيارات بـ
THPDFRapidOCRDLLOptions.ForLanguageوعاملEArgumentExceptionوسمَاً غير مدعوم لا عطلاً في وقت التشغيل - غيّر
RecognitionModelوCharacterDictionaryمعاً لا أحدهما أبداً؛ فتساوي أعداد الأصناف لا يثبت تساوي ترتيب المحارف - أبقِ القواميس UTF-8 بلا BOM، ولا تُجز مدخلات أبداً، وتوقع أن يملك النموذج N + 2 أصناف: فراغ، N مدخلات، مسافة
- على مفكك CTC مخصص أن يغطي الصنف الأخير والخطوة الزمنية الأخيرة وأن يبقي التكرارات مفصولة بفراغ
- استخدم محركاً واحداً لكل ملف تعريفي لغوي ومرّر قوائم صفحات صريحة للمستندات مختلطة الأنظمة
- تغيّر
RightToLeftترتيب الصناديق فقط؛ ويبقى النص المعروف بترتيب Unicode المنطقي - ثبّت النماذج بـ
Install-RapidOCRModels.ps1لتحفظ ثوابث SHA256 اقترانَ النموذج والقاموس؛ واضبطUseAngleClassifier := Falseإن كنت ثبّتّ بـ-SkipClassifier - ارفع
MaxPixelsفوق الافتراضي 16,777,216 قبل تشغيل صفحات A3 أو أكبر عند 300 DPI
الملفات التعريفية اللغوية لـ RapidOCR والمُكيِّف الأصلي للـ DLL ومسار طبقة نص الـ OCR جزء من مكوّن HotPDF Delphi PDF Component لـ Delphi و C++Builder و FPC/Lazarus على Windows، بدءاً من v2.775.0 للملفات التعريفية متعددة اللغات