HotPDF робить відскановані сторінки PDF пошуковими за допомогою RapidOCR у процесі через HPDFCreateRapidOCRDLLOCREngine — фабрику, додану в v2.774.0, яка завантажує HotPDFRapidOCR.dll, тримає ONNX-моделі детекції, класифікації кута та розпізнавання резидентними в пам'яті й повертає IHPDFOCREngine. Той рушій ви передаєте в THotPDF.ApplyLoadedOCRTextLayer, який рендерить кожну сторінку, ганяє CPU-виведення без Python чи дочірнього процесу та комітить невидимий Unicode-текстовий шар
Мотивація — ціна за сторінку. Процесний адаптер RapidOCR, що вийшов раніше, HPDFCreateRapidOCREngine, стартує Python-робітника на кожен виклик Recognize, і той робітник імпортує свій runtime і завантажує свої ONNX-моделі, перш ніж прочитати хоч один піксель. На архіві з 500 сторінок та податок на старт повторюється 500 разів, а деплой означає везти Python-середовище поруч із Delphi-виконуваним файлом. Нативний DLL завантажує моделі один раз, при створенні рушія, а деплой стискується до DLL, його файлів моделей і словника символів. Чим ви платите натомість — це можливістю вбити завислий розпізнавач, і більша частина інженерії в цьому адаптері — про те, як жити з цим чесно
Як зробити відсканований PDF пошуковим через RapidOCR DLL?
Створення пошукового PDF із нативним RapidOCR DLL — це один виклик фабрики і той самий виклик ApplyLoadedOCRTextLayer, яким користується кожен OCR-рушій HotPDF. Фабрика живе в юніті HPDFRapidOCRRecognition і валідує завзято: DLL і папка моделей мусять існувати, кожен файл моделей і словник мусять розрішуватися, версія ABI мусить бути 1, і всі обов'язкові експорти мусять бути присутніми до ініціалізації хоч якоїсь моделі. Помилки конфігурації піднімають 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, з одним CPU-потоком, лімітом вводу 16 777 216 пікселів і дедлайном розпізнавання 60 000 мс. З v2.775.0 THPDFRapidOCRDLLOptions.ForLanguage підставляє відповідну модель розпізнавання та словник для традиційної китайської, російської, японської, арабської та інших профілів; чому модель і словник мусять змінюватися разом — описано в багатомовних моделях RapidOCR і CTC-словниках у HotPDF. Рушій звітує про себе як RapidOCR (native DLL) у Info.EngineName, що тримає журнали однозначними поруч із зовнішнім процесним адаптером Tesseract OCR і вбудованим рушієм OCR на template matching
Чому C ABI говорить лише int32_t та байтами UTF-8?
ABI HotPDFRapidOCR.dll користується лише цілими фіксованої ширини, сирими вказівниками та явними довжинами в байтах, бо Delphi, C++Builder і Free Pascal не ділять з MSVC нічого, крім C-угоди виклику. std::string, std::vector чи C++-виняток мають компонування і модель розкручування, що належать одному компілятору й одній бібліотеці виконання. Впустіть будь-що з них через межу — і відмова буде пошкоджений стек чи блок у купі, звільнений неправильним розподільником, а не чиста помилка
Версія ABI 1 отже дотримується короткого списку правил. Кожен експорт — cdecl і повертає статус int32_t, де 1 означає успіх, а 0 — невдачу. Кожна функція, що може впасти, бере буфер діагностики, яким володіє викликач, та його ємність у байтах; DLL пише NUL-терміноване UTF-8-повідомлення, обрізане під ємність, а адаптер декодує його з жорстким термінатором в останньому байті власного 4096-байтового буфера. Тіло кожного експорту загорнуте в try з обома catch (const std::exception &) і catch (...), тож помилка ONNX Runtime, припущення OpenCV чи недійсний словник стає статусом 0 плюс текст, а не винятком, що тікає в Pascal-код
| Експорт | Роль | Коли адаптер розрішує його |
|---|---|---|
HPDFRapidOCRAbiVersion | Повертає 1; будь-яке інше значення відхиляється | Першим, перед усім іншим |
HPDFRapidOCRCreate | Завантажує моделі детекції, необов'язкової класифікації, розпізнавання і словник | У фабриці |
HPDFRapidOCRRecognize | Ганяє один бітмап і видає один callback на рядок тексту | У фабриці |
HPDFRapidOCRDestroy | Звільняє екземпляр моделі | У фабриці |
HPDFRapidOCRSetReadingDirection | Необов'язковий порядок рядків справа ліворуч, додано у v2.775.0 | Лише коли встановлено RightToLeft |
Необов'язковий експорт розрізняється ліниво навмисно: DLL v2.774.0 без нього досі обслуговує запити зліва праворуч. DLL завантажується через LoadLibraryEx з прапорцями пошуку, що покривають власну папку DLL плюс усталені безпечні директорії, тож залежності ONNX Runtime чи OpenCV, покладені поруч із HotPDFRapidOCR.dll, знаходяться без доторку до PATH. Шляхи моделей і словника подорожують як UTF-8, і DLL конвертує їх через MultiByteToWideChar у строгому режимі перед відкриттям файлів через API широких символів, тож папка моделей під китайським чи кириличним ім'ям користувача працює, а не розширюється байт за байтом у нісенітницю
Одне правило живе в збірці, а не в заголовку. DLL статично лінкує ONNX Runtime і OpenCV, а усталена конфігурація CMake користується статичним release CRT (/MT). Статичні бібліотеки, зібрані проти /MD і змішані в DLL із /MT, дають у кращому разі помилки лінкування, а в гіршому — дві незалежні купи, тож постачані бібліотеки мусять збігатися з тим режимом CRT, який користує DLL
Що відбувається між TBitmap і рядком тексту?
HotPDF вручає DLL незалежний знімок відрендереної сторінки у top-down BGR, а DLL віддає назад один callback на кожен розпізнаний рядок тексту з позиченим UTF-8-текстом, який адаптер мусить скопіювати перед поверненням
У Delphi адаптер присвоює бітмап сторінки приватному TBitmap, форсує pf24bit і читає рядки через GetDIBits з від'ємним biHeight, що дає top-down рядки, підбиті до чотирьохбайтового вирівнювання; той stride передається явно. На FPC він читає через CreateIntfImage, бо записи scanline у LCL можуть оновити сите зображення без освіження GDI-хендла. Бітмап викликача ніколи не модифікується, а піксельний бюджет (MaxPixels, 16 777 216 усталено і конфігуровано до 67 108 864) та ліміт 32 767 пікселів на вимір перевіряються до виділення буфера знімка
Усередині DLL знімок підбивається 50 білими пікселями, текстові регіони детектуються з максимальним боком 1 024 пікселів, бокси впорядковуються в горизонтальні рядки, і кожен кроп опційно повертається класифікатором кута перед розпізнаванням. Кожен рядок тексту тоді проходить через callback, який отримує const char*, кількість байтів, цілочисельний бокс у пікселях оригінального зображення та середню впевненість символів. Вказівник на текст валідний лише протягом callback, тож адаптер копіює його негайно, і він строгий щодо того, що приймає:
- UTF-8 декодується з
MB_ERR_INVALID_CHARS; деформована послідовність валить сторінку замість породження замінних символів у пошуковому шарі - Керувальні символи C0 і C1 відхиляються, а рядки лише з пробілами пропускаються
- Бокс мусить лежати всередині бітмапа, а впевненість мусить бути скінченним значенням від 0 до 1
- Текст лічиться проти
MaxTextCodeUnitsзапиту з жорсткою стелею 1 048 576 UTF-16-одиниць на виклик, причому символи додаткових площин коштують дві одиниці - Будь-який Pascal-виняток усередині callback ловиться там, зберігається і перетворюється на повернення 0, що змушує DLL зупинитися і звітувати невдачу; збережене повідомлення тоді стає діагностикою
Два наслідки важать для тюнінгу. Перше: одиниця виходу — рядок, а не слово; кожен рядок споживає один слот MaxWords, Info.AcceptedWordCount і Info.DroppedWordCount лічать рядки, а підсвічування пошуку накриває бокс рядка. Друге: MinimumConfidence (0.5 усталено) порівнюється із середньою впевненістю символів рядка, тож рядок з однією нерозбірливою літерою серед двадцяти чистих зазвичай виживає. DLL не постачає базової лінії, тож конвеєр текстового шару оцінює її з боксу. Порожня сторінка — це успіх з нулем рядків, а будь-яка невдача чистить часткові результати, тож багатосторінковий коміт лишається все-або-нічого
Володіння моделями та thread safety
Кожен рушій RapidOCR DLL володіє рівно одним екземпляром моделі впродовж усього свого життя, а виклики Recognize на тому рушії серіалізуються критичною секцією. Тримання інтерфейсу 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; // прямі скани: модель класифікатора не завантажується
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 ставить і внутрішньоопераційну, і міжопераційну кількість потоків кожної ONNX-сесії, а DLL притискає його до кількості активних процесорів. Два потоки, що ділять один рушій, не ганяються паралельно; другий чекає на блокування. Те очікування — не сліпе EnterCriticalSection: адаптер викликає TryEnterCriticalSection кожні 25 мс і перевіряє токен скасування та дедлайн між спробами, тож черговий запит досі можна скасувати чи дати йому вийти за час. Якщо потрібен справжній паралелізм — створіть по одному рушію на робітника і змиріться з тим, що кожен рушій тримає власну копію моделей у пам'яті
Порядок розбору зафіксований деструктором рушія: HPDFRapidOCRDestroy спершу звільняє екземпляр моделі, потім FreeLibrary вивантажує DLL. На нативному боці ініціалізація моделей така сама обережна; коли модель розпізнавання падає після того, як сесії детектора і класифікатора вже збудовані, ті сесії звільняються до звітування помилки, а кількість класів словника перевіряється проти виходу моделі під час ініціалізації, а не на першій сторінці
Чому нативний OCR-виклик не можна вбити посеред виведення?
Нативний виклик RapidOCR не можна вбити посеред виведення, бо він ганяється на вашому потоці, всередині вашого процесу, посеред ONNX Runtime-сесії, яка не приймає переривання. Скасування в DLL-адаптері HotPDF отже кооперативне: DLL викликає abort-callback до і після детекції, після класифікації та після кожного розпізнаного рядка і зупиняється на першій контрольній точці, де callback повертає 0. Запущений ONNX Run спершу завершиться
Альтернативи гірші за очікування. TerminateThread лишив би блокування купи CRT, пул потоків ONNX Runtime і будь-який стан OpenCV в тому стані, в якому вони опинилися б, отруюючи решту процесу. FreeLibrary під час виклику, що досі виконується, вивантажує код, який лежить на стеку. Ні те, ні інше не можна зробити безпечним, тож адаптер ніколи не намагається. Дедлайн у TimeoutMilliseconds відтак кооперативний, і закінчений дедлайн виринає як помилка рушія з діагностикою таймауту, тоді як скасований токен виринає як otlsCancelled:
// Токен створює викликач і ділить його з UI-потоком,
// який викликає Token.Cancel, коли користувач тисне Stop
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 і Python RapidOCR запускають процес і завантажують моделі для кожної сторінки; DLL завантажує моделі один раз на рушій
- Зупинення: дочірній процес можна завершити цілком, причому Python-робітник ганяється всередині Job Object із kill-on-close, тож усе його процесне дерево іде слідом; DLL може зупинитися лише на межах стадій і рядків
- Утримання вад: падіння в
tesseract.exeвалить одну сторінку; access violation всередині DLL валить ваш процес - Деплой: процесним адаптерам потрібна встановлена програма чи Python-середовище; DLL потрібні сама вона, її моделі та її словник, підібрані до розрядності програми
- Пам'ять: процесні адаптери звільняють усе, коли дитина виходить; рушій DLL тримає свої моделі резидентними до звільнення останнього посилання на інтерфейс
Для інтерактивної настільної програми, що OCR-ить по сторінці за раз, чуйність DLL зазвичай перемагає. Для сервера, що поглинає недовірені скани цілодобово, процесна межа варта своєї ціни старту
Збірка та деплой HotPDFRapidOCR.dll
HotPDFRapidOCR.dll збирається з C++-джерел у Native/RapidOCR за допомогою MSVC, C++17, Windows SDK і CMake 3.20 чи новішого, використовуючи хелпер-скрипт, який бере директорії нативних мережевих джерел, ONNX Runtime і OpenCV плюс платформу Win32 чи Win64. Збирайте обидві, якщо постачаєте обидві, бо 32-бітова Delphi-програма не може завантажити 64-бітовий DLL, а статичні бібліотеки, які ви постачаєте, мусять збігатися з цільовою архітектурою так само, як і з режимом CRT
Бік моделей має власні межі сумісності. Детектор — це текстовий детектор DB; розпізнавач приймає CTC-моделі в компонуванні NCHW з фіксованою висотою вводу 32 чи 48 і користується 48 для моделей із динамічною висотою. Комплектний статичний ONNX Runtime не може завантажити моделі, збережені з новішою IR-версією, тож свіжі експорти PP-OCRv5 падають на ініціалізації з діагностикою замість часткового завантаження. Словник мусить бути UTF-8 без BOM, точно в порядку символів моделі, і його кількість класів мусить збігатися з виходом моделі; кінці рядків CRLF приймаються. Розпізнавання офлайн: DLL ніколи не завантажує відсутню модель
Шпаргалка
- Фабрика:
HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options])уHPDFRapidOCRRecognition, доступна з v2.774.0 у збірках Delphi, C++Builder і Windows FPC/Lazarus - Тримайте повернений
IHPDFOCREngineживим між сторінками і документами; його звільнення знищує моделі і вивантажує DLL - Один рушій ганяє одне розпізнавання за раз; створюйте кілька рушіїв для паралельних робітників і бюджетуйте пам'ять на кожну копію моделей
- Вихід — один запис на рядок тексту з середньою впевненістю символів, відфільтрований через
THPDFOCRTextLayerOptions.MinimumConfidence - Скасування і
TimeoutMillisecondsкооперативні; ONNX-прогін, що почався, завжди завершується - Підбирайте розрядність DLL до програми, а режим CRT статичних бібліотек ONNX Runtime і OpenCV — до DLL
- Обирайте мовний профіль на рушій через
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0); один рушій не детектить мови самотужки
Нативний адаптер RapidOCR, процесні OCR-адаптери, рендерер сторінок, який їх годує, і писар невидимого Unicode-текстового шару постачаються разом у HotPDF — нативному VCL PDF-компоненті для Delphi і C++Builder. Якщо вашій програмі захоплення чи архівування документів потрібен пошуковий вихід без Python-runtime на цільовій машині, компонент HotPDF Delphi PDF дає весь конвеєр, залишаючи в деплої лише DLL та його моделі