HotPDF صفحات PDF اسکنشده را با RapidOCR درونفرایندی از طریق HPDFCreateRapidOCRDLLOCREngine قابلجستوجو میکند، یک factory که در v2.774.0 اضافه شد و HotPDFRapidOCR.dll را load میکند، مدلهای تشخیص و دستهبندی زاویه و بازشناسی ONNX را مقیم در حافظه نگه میدارد و یک IHPDFOCREngine برمیگرداند. آن موتور را به THotPDF.ApplyLoadedOCRTextLayer میدهی، که هر صفحه را رندر میکند، استنتاج CPU را بدون Python یا فرایند فرزند اجرا میکند و یک لایهٔ متنی Unicode نامرئی ثبت میکند
انگیزه هزینه بهازای هر صفحه است. آداپتور فرایندی RapidOCR که زودتر عرضه شد یعنی HPDFCreateRapidOCREngine بهازای هر فراخوانی Recognize یک worker در Python راه میاندازد و آن worker قبل از خواندن حتی یک پیکسل runtime اش را import و مدلهای ONNX اش را load میکند. روی آرشیوی 500 صفحهای آن مالیات شروع 500 بار تکرار میشود و deploy یعنی حمل یک محیط Python کنار یک executable در Delphi. DLL بومی مدلها را یک بار موقع ساخت موتور load میکند و deploy به DLL و فایلهای مدلش و یک dictionary نویسهها تنزل میکند. چیزی که در عوض قربانی میکنی توان کشتن یک recognizer گیرکرده است، و بیشتر مهندسی این آداپتور دربارهٔ صادقانه کنار آمدن با همین است
با DLL یعنی RapidOCR چطور یک PDF اسکنشده را قابلجستوجو میکنی؟
ساختن یک PDF قابلجستوجو با DLL بومی یعنی RapidOCR یک فراخوانی factory و همان فراخوانی ApplyLoadedOCRTextLayer میخواهد که هر موتور OCR در HotPDF استفاده میکند. factory در یونیت HPDFRapidOCRRecognition است و مشتاقانه اعتبارسنجی میکند: DLL و پوشهٔ مدل باید موجود باشند، هر فایل مدل و dictionary باید حل شود، نسخهٔ ABI باید 1 باشد، و همهٔ exportهای الزامی باید حاضر باشند قبل از اینکه هیچ مدلی مقداردهی شود. اشتباهات پیکربندی EArgumentException بالا میدهند؛ مدلی که load نشود EInvalidOperation با متن تشخیصی که DLL نوشته بالا میدهد
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// مدلها اینجا load میشوند، بیرون هر ضربالاجل بازشناسی.
// نامهای نسبی مدل در 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
// یک فهرست صفحهٔ خالی یعنی همهٔ صفحات؛ صفحاتی که از قبل متن دارند skip میشوند
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، با یک thread پردازنده، یک سقف ورودی 16,777,216 پیکسلی، و ضربالاجل بازشناسی 60,000 میلیثانیهای. از v2.775.0 THPDFRapidOCRDLLOptions.ForLanguage برای چینی سنتی و روسی و ژاپنی و عربی و پروفایلهای دیگر یک مدل بازشناسی و dictionary منطبق جابهجا میکند؛ اینکه چرا مدل و dictionary باید با هم عوض شوند در مدلهای چندزبانهٔ RapidOCR و dictionaryهای CTC در HotPDF پوشش داده شده. موتور خودش را در Info.EngineName بهشکل RapidOCR (native DLL) گزارش میکند، که لاگها را کنار آداپتور فرایندی بیرونی Tesseract OCR و موتور OCR تطبیق قالب داخلی بیابهام نگه میدارد
چرا ABI یعنی C فقط int32_t و بایتهای UTF-8 حرف میزند؟
ABI یعنی HotPDFRapidOCR.dll فقط از اعداد صحیح با عرض ثابت و اشارهگر خام و طول بایت صریح استفاده میکند چون Delphi و C++Builder و Free Pascal با MSVC فراتر از قرارداد فراخوانی C هیچ اشتراکی ندارند. یک std::string یا std::vector یا یک exception در C++ چیدمان و مدل unwind ای دارد که مال یک کامپایلر و یک runtime library است. بگذار هر کدامشان از مرز عبور کند و خرابی یا پشتهٔ فاسد است یا بلوک heap ای که allocator اشتباهی آزادش کرده، نه یک error تمیز
پس نسخهٔ ABI یعنی 1 یک فهرست کوتاه از قواعد را دنبال میکند. هر export cdecl است و یک وضعیت int32_t برمیگرداند که 1 یعنی موفقیت و 0 یعنی شکست. هر تابعی که میتواند شکست بخورد یک بافر تشخیصی متعلق به فراخواننده و ظرفیتش را بر حسب بایت میگیرد؛ DLL یک پیام UTF-8 با پایان NUL مینویسد که برای جا شدن بریده شده، و آداپتور آن را با یک پایانهٔ سخت در آخرین بایت بافر 4,096 بایتی خودش decode میکند. بدنهٔ هر export در try با هر دو catch (const std::exception &) و catch (...) پیچیده شده، پس یک خطای ONNX Runtime یا یک assert در OpenCV یا یک dictionary نامعتبر به وضعیت 0 بهعلاوهٔ متن تبدیل میشود، هرگز یک exception ای که به کد Pascal فرار کند نه
| Export | نقش | آداپتور کی حلش میکند |
|---|---|---|
HPDFRapidOCRAbiVersion | 1 برمیگرداند؛ هر مقدار دیگری رد میشود | اول، قبل از هر چیز دیگری |
HPDFRapidOCRCreate | مدلهای تشخیص و دستهبندی اختیاری و بازشناسی و dictionary را load میکند | داخل factory |
HPDFRapidOCRRecognize | یک بیتمپ را اجرا و بهازای هر خط متن یک callback صادر میکند | داخل factory |
HPDFRapidOCRDestroy | instance مدل را آزاد میکند | داخل factory |
HPDFRapidOCRSetReadingDirection | ترتیب ردیف راستبهچپ اختیاری، اضافهشده در v2.775.0 | فقط وقتی RightToLeft ست شده باشد |
export اختیاری عمداً تنبل حل میشود: یک DLL یعنی v2.774.0 که آن را ندارد همچنان درخواستهای چپبهراست را سرو میکند. DLL با LoadLibraryEx با فلگهای جستوجویی که پوشهٔ خود DLL بهعلاوهٔ دایرکتوریهای امن پیشفرض را پوشش میدهند load میشود، پس وابستگیهای ONNX Runtime یا OpenCV که کنار HotPDFRapidOCR.dll گذاشته شدهاند بدون دست زدن به PATH پیدا میشوند. مسیرهای مدل و dictionary بهشکل UTF-8 سفر میکنند و DLL آنها را با MultiByteToWideChar در حالت سختگیرانه قبل از باز کردن فایلها از طریق APIهای عریض تبدیل میکند، پس یک پوشهٔ مدل زیر یک نام کاربری چینی یا سیریلیک کار میکند بهجای اینکه بایتبهبایت به یک رشتهٔ بیمعنی عریض شود
یک قاعده در بیلد است نه در هدر. DLL یعنی ONNX Runtime و OpenCV را بهشکل استاتیک لینک میکند و پیکربندی پیشفرض CMake از CRT آزاد منتشر استاتیک یعنی /MT استفاده میکند. کتابخانههای استاتیکی که برعلیه /MD کامپایل شدهاند و داخل یک DLL یعنی /MT مخلوط شوند در بهترین حالت خطای لینک میدهند و در بدترین حالت دو heap مستقل، پس کتابخانههای فراهمشده باید با هر حالت CRT ای که DLL استفاده میکند بخوانند
بین یک TBitmap و یک خط متن چه میگذرد؟
HotPDF به DLL یک snapshot مستقل BGR با جهت بالا-به-پایین از صفحهٔ رندرشده میدهد و DLL بهازای هر خط متن بازشناسیشده یک callback پس میدهد با متن UTF-8 قرضی که آداپتور باید قبل از برگشتن کپیاش کند
در Delphi آداپتور بیتمپ صفحه را به یک TBitmap خصوصی منتسب میکند، pf24bit را تحمیل میکند و ردیفها را با GetDIBits با یک biHeight منفی میخواند، که ردیفهای بالا-به-پایین با padding به تراز چهاربایتی میدهد؛ آن stride صریحاً پاس میشود. در FPC از طریق CreateIntfImage میخواند، چون نوشتن scanline در LCL میتواند تصویر خام را بدون تازهسازی handle یعنی GDI بهروز کند. بیتمپ فراخواننده هرگز تغییر نمیکند و بودجهٔ پیکسل یعنی MaxPixels (پیشفرض 16,777,216 و قابل تنظیم تا 67,108,864) و سقف 32,767 پیکسلی بهازای هر بعد قبل از تخصیص بافر snapshot چک میشوند
داخل DLL snapshot با 50 پیکسل سفید padding میشود، نواحی متن با حداکثر ضلع 1,024 پیکسلی تشخیص داده میشوند، جعبهها به ردیفهای افقی مرتب میشوند، و هر برش اختیاراً قبل از بازشناسی توسط دستهبند زاویه چرخانده میشود. بعد هر خط متن از یک callback میگذرد که یک const char* و یک شمارش بایت و یک جعبهٔ عدد صحیح در پیکسلهای تصویر اصلی و میانگین اطمینان نویسهها میگیرد. اشارهگر متن فقط در طول callback معتبر است، پس آداپتور بلافاصله کپیاش میکند و در قبول کردنش سختگیر است:
- UTF-8 با
MB_ERR_INVALID_CHARSdecode میشود؛ یک توالی بدشکل صفحه را شکست میدهد بهجای تولید نویسههای جایگزین در یک لایهٔ قابلجستوجو - نویسههای کنترلی C0 و C1 رد میشوند و خطهای فقط فاصلهٔ خالی skip میشوند
- جعبه باید داخل بیتمپ باشد و اطمینان باید مقداری متناهی از 0 تا 1 باشد
- متن نسبت به
MaxTextCodeUnitsدرخواست با سقف سخت 1,048,576 واحد UTF-16 بهازای هر فراخوانی شمرده میشود و نویسههای صفحهٔ مکمل دو واحد هزینه دارند - هر exception یعنی Pascal داخل callback آنجا گرفته و ذخیره و به یک بازگشت 0 تبدیل میشود، که DLL را وادار به توقف و گزارش شکست میکند؛ پیام ذخیرهشده بعدش تشخیص میشود
دو پیامد برای تنظیم مهماند. اول، واحد خروجی یک خط است نه یک کلمه: هر خط یک خانهٔ MaxWords مصرف میکند، Info.AcceptedWordCount و Info.DroppedWordCount خط میشمارند، و هایلایت جستوجو جعبهٔ خط را میپوشاند. دوم، MinimumConfidence (پیشفرض 0.5) با میانگین اطمینان نویسههای خط مقایسه میشود، پس خطی با یک نویسهٔ ناخوانا بین بیست نویسهٔ تمیز معمولاً جان سالم به در میبرد. DLL هیچ baseline ای نمیدهد، پس خط لولهٔ لایهٔ متنی یکی را از جعبه تخمین میزند. یک صفحهٔ خالی با صفر خط موفق میشود و هر شکستی نتایج جزئی را پاک میکند تا ثبت چندصفحهای همه-یا-هیچ بماند
مالکیت مدل و thread safety
هر موتور DLL یعنی RapidOCR دقیقاً یک instance مدل را برای کل عمرش مالک است و فراخوانیهای Recognize روی آن موتور با یک critical section سریال میشوند. نگه داشتن interface یعنی 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; // اسکنهای ایستاده: هیچ مدل دستهبند load نمیشود
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 هم شمارش threadهای intra-op و هم inter-op هر نشست ONNX را ست میکند و DLL آن را به تعداد پردازندهٔ فعال میبَرد. دو thread که یک موتور را شریکاند موازی اجرا نمیشوند؛ دومی منتظر قفل میماند. آن انتظار یک EnterCriticalSection کورکورانه نیست: آداپتور هر 25 میلیثانیه TryEnterCriticalSection را صدا میزند و بین تلاشها توکن لغو و ضربالاجل را چک میکند، پس یک درخواست در صف هنوز میتواند لغو یا تایماوت شود. اگر موازیسازی واقعی لازم داری، بهازای هر worker یک موتور بساز و بپذیر که هر موتور کپی خودش از مدلها را در حافظه نگه میدارد
ترتیب برچیدن توسط destructor موتور ثابت شده: HPDFRapidOCRDestroy اول instance مدل را آزاد میکند، بعد FreeLibrary DLL را بارگیری میکند. در سمت بومی مقداردهی مدل به همان اندازه مواظب است؛ وقتی مدل بازشناسی بعد از اینکه نشستهای detector و دستهبند از قبل ساخته شده بودند شکست میخورد، آن نشستها قبل از گزارش خطا آزاد میشوند و شمارش کلاسهای dictionary موقع مقداردهی نسبت به خروجی مدل چک میشود نه روی اولین صفحه
چرا یک فراخوانی OCR بومی وسط استنتاج کشته نمیشود؟
یک فراخوانی بومی RapidOCR وسط استنتاج کشته نمیشود چون روی thread تو اجرا میشود، داخل فرایند تو، وسط یک نشست ONNX Runtime که وقفه را نمیپذیرد. پس لغو در آداپتور DLL یعنی HotPDF مشارکتی است: DLL قبل و بعد از تشخیص، بعد از دستهبندی و بعد از هر خط بازشناسیشده یک callback لغو را صدا میزند و در اولین نقطهٔ بازرسی که callback مقدار 0 برگرداند متوقف میشود. یک Run تکی ONNX که شروع شده اول تمام میشود
جایگزینها بدتر از انتظارند. TerminateThread قفل heap یعنی CRT و thread pool یعنی ONNX Runtime و هر state یعنی OpenCV را در هر وضعیتی که اتفاقاً بوده رها میکرد و بقیهٔ فرایند را مسموم میکرد. FreeLibrary وقتی یک فراخوانی هنوز در حال اجراست کدی را که روی پشته است بارگیری میکند. هیچکدام امن نمیشوند، پس آداپتور هرگز سراغشان نمیرود. ضربالاجل داخل TimeoutMilliseconds در نتیجه یک ضربالاجل مشارکتی است و ضربالاجل منقضیشده بهشکل یک خطای موتور با تشخیص تایماوت خودش را نشان میدهد، در حالی که توکن لغوشده بهشکل otlsCancelled:
// توکن توسط فراخواننده ساخته و با UI thread شریک میشود،
// که وقتی کاربر Stop را میزند Token.Cancel را صدا میزند
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 و RapidOCR در Python بهازای هر صفحه یک فرایند راه میاندازند و مدلها را load میکنند؛ DLL مدلها را بهازای هر موتور یک بار load میکند
- متوقف کردن: یک فرایند فرزند را میشود مستقیم خاتمه داد و worker در Python داخل یک Job Object یعنی kill-on-close اجرا میشود که کل درخت فرایندش را با خودش میبرد؛ DLL فقط در مرزهای مرحله و خط میتواند بایستد
- محصورسازی خرابی: یک crash در
tesseract.exeیک صفحه را شکست میدهد؛ یک access violation داخل DLL فرایند تو را زمین میزند - deploy: آداپتورهای فرایندی یک برنامهٔ نصبشده یا محیط Python میخواهند؛ DLL خودش و مدلهایش و dictionary اش را میخواهد، همخوان با bitness برنامه
- حافظه: آداپتورهای فرایندی وقتی فرزند خارج میشود همهچیز را آزاد میکنند؛ یک موتور DLL مدلهایش را مقیم نگه میدارد تا آخرین ارجاع interface آزاد شود
برای یک اپ دسکتاپ تعاملی که بهازای هر بار یک صفحه را OCR میکند، پاسخدهی DLL معمولاً برنده است. برای یک سرور که شبانهروز اسکنهای غیرقابلاعتماد میبلعد، مرز فرایند لایق هزینهٔ شروعش است
بیلد کردن و deploy کردن HotPDFRapidOCR.dll
HotPDFRapidOCR.dll از سورسهای C++ داخل Native/RapidOCR با MSVC و C++17 و یک Windows SDK و CMake نسخهٔ 3.20 به بعد بیلد میشود، با یک اسکریپت هلپر که سورسهای شبکه بومی و پوشههای ONNX Runtime و OpenCV بهعلاوهٔ یک پلتفرم Win32 یا Win64 میگیرد. اگر هر دو را عرضه میکنی هر دو را بیلد کن، چون یک اپ 32 بیتی Delphi نمیتواند یک DLL یعنی 64 بیتی load کند، و کتابخانههای استاتیکی که فراهم میکنی باید هم با معماری هدف و هم با حالت CRT بخوانند
سمت مدل محدودیتهای سازگاری خودش را دارد. detector یک detector متن یعنی DB است؛ بازشناس مدلهای CTC در چیدمان NCHW با ارتفاع ورودی ثابت 32 یا 48 را میپذیرد و برای مدلهای با ارتفاع پویا از 48 استفاده میکند. ONNX Runtime استاتیک بستهبندیشده نمیتواند مدلهایی را که با نسخهٔ IR جدیدتری ذخیره شدهاند load کند، پس exportهای اخیر PP-OCRv5 مقداردهی را با یک تشخیص شکست میدهند بهجای load ناقص. dictionary باید UTF-8 بدون BOM باشد، دقیقاً با ترتیب نویسههای مدل، و شمارش کلاسهایش باید با خروجی مدل بخواند؛ پایان خطهای CRLF پذیرفته میشوند. بازشناسی آفلاین است: DLL هرگز مدلی را که نیست دانلود نمیکند
مرجع سریع
- factory:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])درHPDFRapidOCRRecognition، در دسترس از v2.774.0 در بیلدهای Delphi و C++Builder و Windows FPC/Lazarus -
IHPDFOCREngineبرگشتی را در طول صفحات و اسناد زنده نگه دار؛ آزاد کردنش مدلها را نابود و DLL را بارگیری میکند - یک موتور در هر لحظه یک بازشناسی اجرا میکند؛ برای workerهای موازی چند موتور بساز و حافظه را برای هر کپی مدل بودجه بده
- خروجی یک مدخل بهازای هر خط متن با میانگین اطمینان نویسهها است، فیلترشده توسط
THPDFOCRTextLayerOptions.MinimumConfidence - لغو و
TimeoutMillisecondsمشارکتیاند؛ یک run یعنی ONNX در حال اجرا همیشه تمام میشود - bitness یعنی DLL را با برنامه و حالت CRT کتابخانههای استاتیکی ONNX Runtime و OpenCV را با DLL بخوان
- پروفایل زبان را بهازای هر موتور با
THPDFRapidOCRDLLOptions.ForLanguageانتخاب کن (v2.775.0)؛ یک موتور خودش زبانها را تشخیص نمیدهد
آداپتور بومی RapidOCR و آداپتورهای OCR فرایندی و renderer صفحهای که به آنها تغذیه میدهد و نویسندهٔ لایهٔ متنی Unicode نامرئی همه با هم در HotPDF عرضه میشوند، یک کامپوننت PDF بومی VCL برای Delphi و C++Builder. اگر اپ capture یا آرشیو سندت خروجی قابلجستوجو بدون runtime یعنی Python روی ماشین مقصد میخواهد، کامپوننت HotPDF Delphi PDF کل خط لوله را میدهد و فقط DLL و مدلهایش برای deploy باقی میمانند