HotPDF запускает Tesseract внутри вашего Delphi-процесса через HPDFCreateTesseractDLLOCREngine — фабрику, добавленную в v2.772.0: она динамически грузит совместимую с Tesseract 5 DLL, рулит её C API (TessBaseAPIInit2, TessBaseAPIRecognize, итератор результатов) и возвращает IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer пользуется этим движком, чтобы добавить невидимый searchable Unicode-текстовый слой на отсканированные страницы PDF
Тот же распознаватель и раньше был достижим через внешний адаптер tesseract.exe, который пишет BMP и парсит TSV. Тот путь работает, но каждая страница платит за запуск процесса, временный битмап-файл и текстовый формат без базовых линий и без контроля сегментации страницы. Вызов DLL снимает все три. Заодно он снимает процессную стену, а это значит, что Pascal-биндинг лежит прямо поверх C-структур, C-булев и C-выделенных строк. Большая часть того, что стоит знать об этом адаптере, — где такой биндинг может тихо пойти не так
Как запустить Tesseract in-process из Delphi с HotPDF?
Запуск Tesseract in-process с HotPDF занимает один вызов фабрики в юните HPDFTesseractRecognition и тот же вызов ApplyLoadedOCRTextLayer, которым пользуется каждый OCR-движок HotPDF. Фабрика валидирует заранее. Файл DLL и каталог tessdata обязаны существовать, идентификатор языка может содержать только ASCII-буквы, цифры, _ и +, каждая модель в комбинации вроде chi_sim+eng обязана иметь подходящий файл .traineddata, и все 21 нужный экспорт обязаны разрешиться до возврата движка. Ошибки конфигурации поднимают EArgumentException; DLL, не сумевшая загрузиться, поднимает EOSError с кодом ошибки Windows и подсказкой проверить архитектуру и зависимости
uses
SysUtils, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// Win64-приложению нужна 64-битная DLL; DLL-зависимости кладутся рядом
Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
'C:\OCR\tessdata', 'chi_sim+eng'); // THPDFTesseractOptions.Default
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,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
THPDFTesseractOptions.Default ставит PageSegMode в tpsAuto, EngineMode в temDefault, TimeoutMilliseconds в 60 000 и MaxPixels в 16 777 216. Пиксельный бюджет важнее, чем кажется. Страница US Letter при дефолтных 300 DPI рендерится в 2 550 × 3 300 пикселей, около 8,4 миллиона, — влезает. Та же страница при 600 DPI — 5 100 × 6 600, около 33,7 миллиона, и адаптер отвергнет её раньше, чем Tesseract увидит хоть пиксель. Поднимайте MaxPixels (потолок — 67 108 864) или держите DPI на месте; каждая сторона к тому же зажата в 32 767 пикселей
DLL грузится через LoadLibraryEx с поисковыми флагами на собственную папку DLL плюс дефолтные безопасные каталоги, так что библиотеки образов, от которых зависит Tesseract, могут лежать рядом без трогания PATH и текущего каталога. HotPDF не бандлит и не скачивает никакого OCR-рантайма или моделей; то и другое проставляете вы
Что меняется по сравнению с адаптером tesseract.exe?
DLL-адаптер меняет изоляцию процессов на более богатый вывод и меньший оверхед на страницу. Оба адаптера встают в один конвейер текстового слоя, так что координатный маппинг, фильтрация по уверенности и фиксация всё-или-ничего идентичны; различается то, как пиксели въезжают и слова выезжают
| Аспект | Адаптер tesseract.exe | Адаптер Tesseract DLL |
|---|---|---|
| Фабрика | HPDFCreateTesseractOCREngine | HPDFCreateTesseractDLLOCREngine |
| Пиксели на вход | BMP-файл в приватном временном каталоге | 8-битный grayscale-буфер в памяти |
| Слова на выход | Word-уровневый TSV, потолок 64 МиБ | Итератор результатов, UTF-8 на слово |
| Базовые линии | Недоступны | Прокидываются из TessPageIteratorBaseline |
| Сегментация страницы и режим движка | Только автоматическая сегментация | THPDFTesseractPageSegMode, THPDFTesseractEngineMode |
| Таймаут | Жёсткий: дочерний процесс терминируется | Кооперативный: Tesseract обязан заметить |
| Изоляция падений и памяти | Отдельный процесс | Нет, делит ваше адресное пространство |
Одна цена не исчезает. Каждый вызов Recognize создаёт собственный инстанс API и зовёт TessBaseAPIInit2, так что языковые модели инициализируются на каждую страницу, а не один раз на движок. Файловый кэш операционной системы смягчает перегрузку, но на крупных многоязычных наборах моделей это всё равно доминирующая фиксированная цена за страницу, и она съедает дедлайн распознавания. In-process движок RapidOCR DLL берёт противоположный дизайн и держит ONNX-модели резидентно всю жизнь движка; граничные проблемы (C ABI, заёмные буферы, непрерываемая нативная работа) — из одной семьи
Почему Delphi не может скопировать структуру monitor Tesseract?
Delphi не может безопасно отзеркалить progress monitor Tesseract, потому что ETEXT_DESC содержит зависящие от версии внутренние поля, и рукокопированная запись кладёт cancel-callback и дедлайн на неверные смещения на некоторых сборках. Ничто не падает громко, когда это случается. Tesseract просто читает ваш указатель на callback из поля, которое теперь держит нечто другое, — или вообще не видит дедлайна
Поэтому HotPDF трактует monitor как непрозрачный указатель и трогает его только через экспортируемые функции: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs и TessMonitorDelete. Если вы сами биндите C API для другой цели, применим тот же паттерн. Набросок ниже — ваш собственный биндинг-код, не API HotPDF, и зеркалит декларации, которые HotPDF использует внутри
type
// C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
TTessMonitorCreate = function: Pointer; cdecl; // ETEXT_DESC*, никогда не разыменовывается
TTessMonitorDelete = procedure(Monitor: Pointer); cdecl;
TTessMonitorSetCancelFunc = procedure(Monitor: Pointer; Func: TTessCancelFunc); cdecl;
TTessMonitorSetCancelThis = procedure(Monitor, CancelThis: Pointer); cdecl;
TTessMonitorSetDeadlineMSecs = procedure(Monitor: Pointer; MSecs: Integer); cdecl;
TTessBaseAPIRecognize = function(Handle, Monitor: Pointer): Integer; cdecl;
TOCRJob = record
CancelRequested: Boolean;
DeadlineTick: UInt64;
end;
POCRJob = ^TOCRJob;
function ShouldCancel(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
begin
// Бежит на стеке Tesseract: читаем флаги и часы, никогда не поднимаем
Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
(GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;
// Использование: указатели функций разрешены через GetProcAddress:
// Monitor := MonitorCreate();
// try
// MonitorSetCancelThis(Monitor, @Job);
// MonitorSetCancelFunc(Monitor, ShouldCancel);
// MonitorSetDeadlineMSecs(Monitor, RemainingMs);
// RC := BaseAPIRecognize(API, Monitor);
// finally
// MonitorDelete(Monitor);
// end;
Две детали в том наброске нарочиты. Callback возвращает Boolean — один байт и в Delphi, и в Free Pascal, что совпадает с C bool в TessCancelFunc. Четырёхбайтовый Windows BOOL или Delphi LongBool выглядит взаимозаменяемым и не является: когда одна сторона пишет один байт, а другая читает четыре, старшие байты регистра возврата — что там осталось, и false может приехать как true. Тот же заголовок усложняет дальше: функции вроде TessPageIteratorBoundingBox возвращают int, который HotPDF декларирует как Integer. Читайте C-тип каждого возвращаемого значения вместо допущения одной конвенции на весь API
Вторая деталь — callback никогда не поднимает. Исключение Delphi, разматывающееся сквозь C++-фреймы Tesseract, — неопределённое поведение, поэтому callback HotPDF читает только токен отмены и монотонное значение GetTickCount64. Адаптер превращает результат в диагностику отмены или таймаута после возврата TessBaseAPIRecognize, и выполняет ту проверку независимо от нативного кода возврата
Какими нативными указателями владеет сторона Delphi?
Tesseract DLL-адаптер HotPDF владеет тремя нативными объектами на запрос — инстансом API, monitor и итератором результатов — и всё остальное берёт внаём. Каждый вызов Recognize создаёт собственный набор и освобождает его в блоке finally: TessResultIteratorDelete, затем TessMonitorDelete, затем TessBaseAPIDelete. Освобождение интерфейса движка выгружает библиотеку
TessResultIteratorGetPageIteratorвозвращает заёмный вид внутрь итератора результатов, а не новый объект. HotPDF пользуется им дляTessPageIteratorBoundingBoxиTessPageIteratorBaselineи никогда не освобождает; отдельное удаление освободило бы ту же память дваждыTessResultIteratorGetUTF8Textвозвращает строку, выделенную собственным рантаймом DLL. HotPDF копирует её и отдаёт обратно черезTessDeleteTextв блокеfinally; Pascal-овскийFreeMemосвободил бы её на чужой куче- Текст слова декодируется строгой валидацией UTF-8 и проверкой длины до конверсии. Слова с управляющими символами, битым UTF-8, боксами вне образа, перевёрнутыми прямоугольниками или уверенностью вне 0–100 топят запрос вместо тихой заплатки
- Суммарный текст на запрос зажат в 1 048 576 UTF-16-юнитов, а число слов обязано влезть в бюджет запроса, спущенный сверху
ApplyLoadedOCRTextLayer
Уверенность приезжает как 0–100 и масштабируется в 0–1, так что THPDFOCRTextLayerOptions.MinimumConfidence значит одно и то же для каждого движка. Когда Tesseract отчитывает базовую линию, прокидываются обе конечные точки; иначе конвейер текстового слоя откатывается к геометрической оценке — ровно как для TSV-ввода
Зачем валидировать enum до того, как он доедет до DLL?
HotPDF копирует сырой ordinal из PageSegMode и EngineMode в Integer до проверки диапазона, потому что компилятор может предположить, что переменная enum всегда держит объявленное значение, и свернуть Ord(X) > Ord(High(T)) в константный false. Ordinal не украшение: THPDFTesseractPageSegMode следует нумерации сегментации страниц Tesseract от 0 до 13, THPDFTesseractEngineMode — нумерации режимов движка от 0 до 3, и оба едут в DLL как простые целые. Запись опций, собранная через FillChar, заполненная из потока или переданная из C++Builder с приведённым целым, может нести байт вроде 200. Валидация скопированного ordinal превращает это в EArgumentException на фабрике вместо неопределённого режима внутри нативного кода. Фабрика к тому же отвергает tpsOSDOnly и tpsAutoOnly, не дающие слов, и требует osd.traineddata для tpsAutoOSD и tpsSparseTextOSD
Что на самом деле гарантирует таймаут распознавания?
Таймаут Tesseract DLL кооперативен: HotPDF может остановить собственную работу и попросить Tesseract остановиться, но не может заставить нативный код вернуться. Часы стартуют с началом Recognize, так что конверсия битмапа и инициализация моделей съедают тот же бюджет, что и распознавание. HotPDF проверяет истёкшее время и токен отмены при конверсии в grayscale и между словами при итерации результатов, и передаёт оставшиеся миллисекунды в TessMonitorSetDeadlineMSecs перед вызовом TessBaseAPIRecognize
Зазор — внутри нативного вызова. Monitor Tesseract опрашивается во время распознавания слов, а не во время TessBaseAPIInit2 или анализа вёрстки страницы, так что медленная загрузка модели или патологическая вёрстка могут пробежать дедлайн раньше, чем таймаут будет отчитан. Пиксельный и выводной бюджеты к тому же не зажимают собственное потребление памяти нативной библиотеки. Если нужен воркер, которого можно убить, — берите процессный адаптер; это честный компромисс, а не отсутствующая фича
Сегментация страницы — то, на чём DLL-адаптер зарабатывает на трудном вводе. Формы, этикетки и отсканированные таблицы с разбросанными полями часто распознаются лучше с tpsSparseText, чем с автоматической сегментацией, которая пытается собрать колонки и абзацы, которых нет
procedure OCRFormPages(Doc: THotPDF; const Pages: array of Integer);
var
Engine: IHPDFOCREngine;
TessOptions: THPDFTesseractOptions;
LayerOptions: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
TessOptions := THPDFTesseractOptions.Default;
TessOptions.PageSegMode := tpsSparseText; // разбросанные поля, без сборки колонок
TessOptions.EngineMode := temLSTMOnly; // нужны LSTM-модели в tessdata
TessOptions.TimeoutMilliseconds := 20000; // включает инициализацию моделей
Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
'C:\OCR\tessdata', 'eng+deu', TessOptions);
LayerOptions := THPDFOCRTextLayerOptions.Default;
LayerOptions.MinimumConfidence := 0.6;
if not Doc.ApplyLoadedOCRTextLayer(Pages, Engine, LayerOptions, Info) then
case Info.Status of
otlsCancelled:
Writeln('OCR cancelled, document unchanged');
otlsEngineError:
Writeln('Tesseract failed or timed out: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
end;
Таймаут всплывает как otlsEngineError с диагностикой Tesseract DLL OCR timed out, а отменённый токен — как otlsCancelled. В обоих случаях ApplyLoadedOCRTextLayer распознал каждую выбранную страницу прежде, чем запустить транзакцию фиксации, так что неудача на странице 40 из 50 оставляет загруженный документ ровно как был. Заметьте: tpsSingleLine, tpsSingleBlock и tpsSparseText меняют только сегментацию; ни один не выпрямляет перекошенный скан
Free Pascal и Lazarus: протухшие пиксели и потерянный китайский
Обе фабрики Tesseract работают в Windows Free Pascal и сборках Lazarus Win32 и Win64 с v2.772.1 — после двух специфичных для FPC правок. Сперва пересоберите пакет Lazarus под целевую архитектуру; общий порт разобран в статье о HotPDF на Free Pascal и Lazarus Win64
Первая правка про пиксели. LCL-овский TBitmap, записанный через scanlines, может обновить сырой образ, не обновив Windows-хэндл битмапа, так что GetDIBits на том хэндле вернёт старые пиксели. Симптом ставил в тупик: текст, нарисованный прямо на битмап, распознавался, а страница, отрендеренная PDF-рендерером HotPDF, давала пустой список слов. На FPC адаптер теперь читает формат-осведомлённый снимок через CreateIntfImage, уважающий пиксельный формат и порядок строк сырого образа. Сборка Delphi держит путь GetDIBits на приватной 24-битной копии. Ни одна сборка не модифицирует битмап вызывающего
Вторая правка принадлежит адаптеру tesseract.exe. TStringList в FPC хранит ANSI-строки, так что присваивание декодированного UTF-8 TSV-текста в Lines.Text молча выбрасывало каждый китайский или supplementary-plane символ, который системная ANSI-кодовая страница не могла представить. FPC-путь теперь держит TSV как байты UTF-8, снимает BOM на байтовом уровне и декодирует каждое слово в UnicodeString по отдельности. DLL-адаптер этой проблемы никогда не имел, потому что декодирует каждое слово прямо из итератора
Шпаргалка
- Фабрика:
HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options])вHPDFTesseractRecognition, добавлена в v2.772.0, поддержка FPC в v2.772.1 - Дефолты:
tpsAuto,temDefault, 60 000 мс, 16 777 216 пикселей; диапазон таймаута 1–3 600 000 мс, пиксельный потолок 67 108 864 - Подгоняйте разрядность DLL под приложение и кладите DLL-зависимости рядом с Tesseract DLL
- Трактуйте monitor как непрозрачный; никогда не копируйте
ETEXT_DESCв Pascal-запись - Декларируйте cancel-callback как
cdeclс однобайтовымBoolean-результатом и никогда не позволяйте исключению из него вырваться - Освобождайте текст итератора через
TessDeleteText; никогда не освобождайте страничный итератор, полученный из итератора результатов - Ждите от дедлайна кооперативности: инициализация моделей и анализ вёрстки могут его перебежать
- Берите адаптер tesseract.exe, когда нужна жёсткая остановка или изоляция падений
Tesseract DLL-адаптер, процессные адаптеры и встроенный OCR-движок поставляются с компонентом HotPDF Delphi PDF component для Delphi, C++Builder и Free Pascal; издания и загрузки — на странице продукта HotPDF