HotPDF הופך עמודי PDF סרוקים לניתנים לחיפוש עם RapidOCR בתוך התהליך דרך HPDFCreateRapidOCRDLLOCREngine, מפעל שנוסף ב-v2.774.0 שטוען את HotPDFRapidOCR.dll, מחזיק את מודלי ה-detection, סיווג הזווית וה-recognition של ONNX תושבי זיכרון, ומחזיר IHPDFOCREngine. את המנוע הזה מעבירים אל THotPDF.ApplyLoadedOCRTextLayer, שמרנדר כל עמוד, מריץ inferencing של CPU בלי Python או תהליך בן, וקובע שכבת טקסט Unicode בלתי נראית
המניע הוא עלות לעמוד. מתאם ה-process של RapidOCR שיצא קודם, HPDFCreateRapidOCREngine, מריץ worker של Python עבור כל קריאת Recognize, וה-worker הזה מייבא את הראנטיים שלו וטוען את מודלי ה-ONNX שלו לפני שהוא קורא פיקסל בודד. על ארכיון בן 500 עמודים מס ההפעלה הזה חוזר 500 פעמים, והפצה פירושה לשלוח סביבת Python לצד קובץ הפעלה של Delphi. ה-DLL המקורי טוען את המודלים פעם אחת, בעת יצירת המנוע, וההפצה מתכווצת ל-DLL, לקבצי המודל שלו ולמילון תווים. מה שמוותרים עליו בתמורה הוא היכולת להרוג מזהה שנתקע, ורוב ההנדסה במתאם הזה עוסקת בלחיות עם זה ביושר
איך הופכים PDF סרוק לניתן לחיפוש עם ה-DLL של RapidOCR?
יצירת PDF ניתן לחיפוש עם ה-DLL המקורי של RapidOCR דורשת קריאת מפעל אחת ואת קריאת ה-ApplyLoadedOCRTextLayer שכל מנוע OCR של HotPDF משתמש בה. המפעל יושב ביחידת ה-HPDFRapidOCRRecognition ומאמת בחפזון: ה-DLL וספריית המודלים חייבים להתקיים, כל קובץ מודל ומילון חייב להיפתר, גרסת ה-ABI חייבת להיות 1, וכל ה-exports הנדרשים חייבים להימצא לפני שמודל כלשהו מאותחל. טעויות הגדרות מעלות 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, עם thread אחד של CPU, מגבלת קלט של 16,777,216 פיקסלים ודדליין זיהוי של 60,000 ms. מאז v2.775.0, THPDFRapidOCRDLLOptions.ForLanguage מחליף במודל זיהוי ומילון תואמים עבור סינית מסורתית, רוסית, יפנית, ערבית ופרופילים נוספים; למה המודל והמילון חייבים להשתנות יחד מכוסה ב-מודלי RapidOCR רב-לשוניים ומילוני CTC ב-HotPDF. המנוע מדווח על עצמו בתור RapidOCR (native DLL) ב-Info.EngineName, מה שמשאיר את הלוגים חד-משמעיים לצד מתאם ה-process של Tesseract OCR החיצוני ו-מנוע ה-OCR מבוסס התאמת תבניות המובנה
למה ה-C ABI מדבר רק int32_t ובייטים של UTF-8?
ה-ABI של HotPDFRapidOCR.dll משתמש רק בשלמים ברוחב קבוע, במצביעים גולמיים ובאורכי בייטים מפורשים כי Delphi, C++Builder ו-Free Pascal לא חולקים עם MSVC דבר מעבר לקונבנציית הקריאה של C. ל-std::string, ל-std::vector או לחריגה של C++ יש פריסה ומודל פריקה ששייכים למהדר אחד ולספריית ראנטיים אחת. תנו לאחד מהם לחצות את הגבול והכישלון יהיה stack מושחת או בלוק heap ששוחרר על ידי ה-allocator הלא נכון, לא שגיאה נקייה
גרסת ABI 1 נאמנה אפוא לרשימה קצרה של כללים. כל export הוא cdecl ומחזיר status של int32_t, כאשר 1 פירושו הצלחה ו-0 כישלון. כל פונקציה שעלולה להיכשל מקבלת חוצץ דיאגנוסטיקה בבעלות הקורא ואת קיבולתו בבייטים; ה-DLL כותב הודעת UTF-8 המסתיימת ב-NUL וקטועה כדי להיכנס, והמתאם מפענח אותה עם מסיים קשיח בבייט האחרון של חוצץ 4,096 הבייטים שלו. גוף כל export עטוף ב-try עם גם catch (const std::exception &) וגם catch (...), כך ששגיאת ONNX Runtime, אסרט של OpenCV או מילון לא תקין הופכים ל-status 0 בתוספת טקסט, ולעולם לא לחריגה שבורחת אל קוד Pascal
| Export | תפקיד | מתי המתאם פותר אותו |
|---|---|---|
HPDFRapidOCRAbiVersion | מחזיר 1; כל ערך אחר נדחה | ראשון, לפני כל דבר אחר |
HPDFRapidOCRCreate | טוען detection, classification אופציונלי, מודלי recognition ואת המילון | במפעל |
HPDFRapidOCRRecognize | מריץ bitmap אחד ופולט callback אחד לכל שורת טקסט | במפעל |
HPDFRapidOCRDestroy | משחרר את מופע המודל | במפעל |
HPDFRapidOCRSetReadingDirection | סדר שורות מימין לשמאל אופציונלי, נוסף ב-v2.775.0 | רק כש-RightToLeft מוגדר |
ה-export האופציונלי נפתר בעצלות בכוונה: DLL של v2.774.0 שחסר אותו עדיין משרת בקשות משמאל לימין. ה-DLL נטען עם LoadLibraryEx עם דגלי חיפוש שמכסים את תיקיית ה-DLL עצמה בתוספת הספריות הבטוחות כברירת מחדל, כך שתלות ONNX Runtime או OpenCV שמוצבות לצד HotPDFRapidOCR.dll נמצאות בלי לגעת ב-PATH. נתיבי מודל ומילון נעים בתור UTF-8 וה-DLL ממיר אותם עם MultiByteToWideChar במצב מחמיר לפני פתיחת קבצים דרך APIs של תווים רחבים, כך שספריית מודלים תחת שם משתמש סיני או קירילי עובדת במקום להתרחב בייט בבייט אל גיבוב
כלל אחד חי בבנייה ולא בכותרת. ה-DLL מקשר סטטית את ONNX Runtime ו-OpenCV, והגדרת ה-CMake כברירת מחדל משתמשת ב-CRT הסטטי של release (/MT). ספריות סטטיות שהודרו מול /MD המעורבות בתוך DLL של /MT מניבות שגיאות קישור במקרה הטוב ושני heap בלתי תלויים במקרה הרע, ולכן הספריות שמסופקות חייבות להתאים לכל מצב CRT שה-DLL משתמש בו
מה קורה בין TBitmap לשורת טקסט?
HotPDF מוסר ל-DLL snapshot עצמאי top-down ב-BGR של העמוד המרונדר, וה-DLL משיב callback אחד לכל שורת טקסט שזוהתה עם טקסט UTF-8 בהשאלה שהמתאם חייב להעתיק לפני החזרה
ב-Delphi המתאם משייך את bitmap העמוד ל-TBitmap פרטי, כופה pf24bit, וקורא שורות עם GetDIBits בשימוש ב-biHeight שלילי, מה שמניב שורות top-down מרופדות ליישור של ארבעה בייטים; ה-stride הזה מועבר במפורש. ב-FPC הקריאה עוברת דרך CreateIntfImage, כי כתיבת scanline של LCL יכולה לעדכן את התמונה הגולמית בלי לרענן את ה-handle של ה-GDI. ה-bitmap של הקורא לעולם לא משתנה, ותקציב הפיקסלים (MaxPixels, 16,777,216 כברירת מחדל וניתן להגדרה עד 67,108,864) ומגבלת 32,767 הפיקסלים לממד נבדקים לפני שחוצץ ה-snapshot מוקצה
בתוך ה-DLL ה-snapshot מרופד ב-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 לעצור ולדווח כישלון; ההודעה השמורה הופכת אז לדיאגנוסטיקה
שתי השלכות משנות עבור כיוונון. ראשונה, יחידת הפלט היא שורה ולא מילה: כל שורה צורכת slot אחד של MaxWords, Info.AcceptedWordCount ו-Info.DroppedWordCount סופרים שורות, והדגשת חיפוש משתרעת על תיבת השורה. שנייה, MinimumConfidence (0.5 כברירת מחדל) מושווה מול ממוצע ביטחון התווים של השורה, כך ששורה עם תו אחד בלתי קריא בין עשרים נקיים בדרך כלל שורדת. ה-DLL לא מספק בסיס, ולכן צינור שכבת הטקסט מעריך אחד מתוך התיבה. עמוד ריק מצליח עם אפס שורות, וכל כישלון מנקה תוצאות חלקיות כך שהקביעה המרובת עמודים נשארת הכול-או-כלום
בעלות על מודלים ו-thread safety
כל מנוע DLL של RapidOCR בבעלותו מופע מודל אחד בדיוק לכל אורך חייו, וקריאות Recognize על אותו מנוע ממוספרות על ידי critical section. החזקת הממשק 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; // סריקות ישרות: לא נטען מודל classifier
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 קובע הן את מספרי ה-threads של ה-intra-op והן של ה-inter-op בכל סשן ONNX, וה-DLL לוחץ אותו אל מספר המעבדים הפעיל. שני threads שחולקים מנוע אחד לא רצים במקביל; השני ממתין לנעילה. ההמתנה הזאת אינה EnterCriticalSection עיוורת: המתאם קורא ל-TryEnterCriticalSection כל 25 ms ובודק את token הביטול ואת הדדליין בין ניסיונות, כך שבקשה בתור עדיין יכולה להתבטל או לפוג. אם צריך מקביליות אמיתית, יוצרים מנוע אחד לכל worker ומקבלים שכל מנוע מחזיק עותק משלו של המודלים בזיכרון
סדר הפירוק קבוע על ידי ה-destructor של המנוע: HPDFRapidOCRDestroy משחרר את מופע המודל קודם, ואז FreeLibrary פורק את ה-DLL. בצד המקורי, אתחול המודלים זהיר באותה מידה; כשמודל הזיהוי נכשל אחרי שסשני ה-detector וה-classifier כבר נבנו, אותם סשנים משוחררים לפני שהשגיאה מדווחת, ומניין המחלקות במילון נבדק מול הפלט של המודל במהלך האתחול ולא בעמוד הראשון
למה קריאת OCR מקורית לא יכולה ליהרג באמצע inference?
קריאת RapidOCR מקורית לא יכולה ליהרג באמצע inference כי היא רצה על ה-thread שלכם, בתוך התהליך שלכם, באמצע סשן ONNX Runtime שאינו מקבל הפרעה. הביטול במתאם ה-DLL של HotPDF הוא אפוא שיתופי: ה-DLL קורא ל-callback של abort לפני ואחרי ה-detection, אחרי ה-classification, ואחרי כל שורה שזוהתה, ועוצר בנקודת הביקורת הראשונה שבה ה-callback מחזיר 0. Run של ONNX שכבר התחיל יסיים קודם
החלופות גרועות מהמתנה. TerminateThread היה משאיר את נעילת ה-heap של ה-CRT, את מאגר ה-threads של ONNX Runtime ואת כל state של OpenCV במצב שבו הם מצאו את עצמם, ומרעיל את שאר התהליך. FreeLibrary בזמן שקריאה עדיין רצה פורק קוד שנמצא על המחסנית. אף אחד מהם לא יכול להיעשות בטוח, ולכן המתאם לעולם לא מנסה אותם. הדדליין ב-TimeoutMilliseconds הוא אפוא דדליין שיתופי, ודדליין שפג מתגלה בתור שגיאת מנוע עם דיאגנוסטיקת פקיעה, בזמן ש-token שבוטל מתגלה בתור otlsCancelled:
// ה-Token נוצר על ידי הקורא ומשותף עם ה-thread של ה-UI,
// שקורא ל-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;
זהו הפשרה המרכזית בין מתאמי ה-process של HotPDF לבין ה-DLL בתוך התהליך, ואף צד לא מנצח בכל שורה:
- עלות הפעלה: מתאמי ה-Tesseract וה-Python RapidOCR משגרים תהליך וטוענים מודלים עבור כל עמוד; ה-DLL טוען מודלים פעם אחת לכל מנוע
- עצירה: תהליך בן ניתן לחיסול מיידי, וה-worker של Python רץ בתוך Job Object של kill-on-close כך שכל עץ התהליכים שלו הולך איתו; ה-DLL יכול לעצור רק בגבולות שלב ושורה
- כליאת תקלות: קריסה ב-
tesseract.exeמכשילה עמוד אחד; access violation בתוך ה-DLL מוריד את התהליך שלכם - הפצה: מתאמי process זקוקים לתוכנית מותקנת או לסביבת Python; ה-DLL זקוק לעצמו, למודליו ולמילונו, מותאמים ל-bitness של האפליקציה
- זיכרון: מתאמי process משחררים הכול כשהבן יוצא; מנוע DLL משאיר את המודלים שלו תושבי זיכרון עד ששוחררה ההפניה האחרונה לממשק
עבור אפליקציית שולחן עבודה אינטראקטיבית שמבצעת OCR לעמוד בכל פעם, היענות ה-DLL בדרך כלל מנצחת. עבור שרת שבולע סריקות לא מהימנות סביב השעון, גבול ה-process שווה את עלות ההפעלה שלו
בנייה והפצה של HotPDFRapidOCR.dll
HotPDFRapidOCR.dll נבנה מקוד המקור של C++ ב-Native/RapidOCR עם MSVC, C++17, Windows SDK ו-CMake 3.20 ומעלה, בשימוש בסקריפט עזר שמקבל את ספריות ה-network sources המקוריות, את ONNX Runtime ואת OpenCV בתוספת פלטפורמת Win32 או Win64. בנו את שניהם אם אתם מפיצים את שניהם, כי אפליקציית Delphi בת 32 ביט לא יכולה לטעון DLL בן 64 ביט, והספריות הסטטיות שמספקים חייבות להתאים לארכיטקטורת היעד וגם למצב ה-CRT
צד המודלים מחזיק במגבלות תאימות משלו. ה-detector הוא DB text detector; המזהה מקבל מודלי CTC בפריסת NCHW עם גובה קלט קבוע של 32 או 48, ומשתמש ב-48 עבור מודלים בגובה דינמי. ONNX Runtime הסטטי המצורף אינו יכול לטעון מודלים שנשמרו עם גרסת IR חדשה יותר, ולכן exports של PP-OCRv5 אחרונים נכשלים באתחול עם דיאגנוסטיקה במקום להיטען חלקית. המילון חייב להיות UTF-8 ללא BOM, בדיוק בסדר התווים של המודל, ומניין המחלקות שלו חייב להתאים לפלט המודל; סיומות שורה של CRLF מתקבלות. הזיהוי אופליין: ה-DLL לעולם לא מוריד מודל חסר
עזר זריז
- מפעל:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])ב-HPDFRapidOCRRecognition, זמין מאז v2.774.0 ב-Delphi, C++Builder ו-builds של FPC/Lazarus ל-Windows - משאירים את ה-
IHPDFOCREngineהמוחזר חי על פני עמודים ומסמכים; שחרורו הורס את המודלים ופורק את ה-DLL - מנוע אחד מריץ זיהוי אחד בכל רגע; יוצרים כמה מנועים עבור workers מקביליים ומתקצבים זיכרון עבור כל עותק מודל
- הפלט הוא רשומה אחת לכל שורת טקסט עם ממוצע ביטחון תווים, מסונן על ידי
THPDFOCRTextLayerOptions.MinimumConfidence - הביטול ו-
TimeoutMillisecondsשיתופיים; ריצת ONNX בעיצומה תמיד מסתיימת - מתאימים את bitness של ה-DLL לאפליקציה ואת מצב ה-CRT של ספריות ONNX Runtime ו-OpenCV הסטטיות אל ה-DLL
- בוחרים פרופיל שפה לכל מנוע עם
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0); מנוע אחד אינו מאתר שפות בעצמו
מתאם ה-RapidOCR המקורי, מתאמי ה-OCR מבוססי ה-process, מנוע הרינדור לעמודים שמזין אותם וכותב שכבת הטקסט הבלתי נראית של Unicode מגיעים כולם יחד ב-HotPDF, רכיב PDF מקורי ל-VCL עבור Delphi ו-C++Builder. אם אפליקציית לכידת או ארכוב מסמכים שלכם זקוקה לפלט ניתן לחיפוש בלי ראנטיים Python על מכונת היעד, HotPDF Delphi PDF component מספק את כל הצינור, ונותר להפיץ רק את ה-DLL ואת המודלים שלו