Техническа статия

HotPDF Tesseract DLL OCR: викане на C API от Delphi

HotPDF пуска Tesseract вътре в вашия Delphi процес чрез HPDFCreateTesseractDLLOCREngine — фабрика, добавена във v2.772.0, която динамично зарича DLL, съвместим с Tesseract 5, задвижва C API-я му (TessBaseAPIInit2, TessBaseAPIRecognize, резултатният итератор) и връща IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer ползва този engine, за да добави невидим, търсим Unicode текстов слой към сканираните PDF страници

Същият разпознавач вече беше достижим през външния tesseract.exe adapter, който пише BMP и парсва TSV. Този път работи, но всяка страница плаща за стартиране на процес, временен bitmap файл и текстов формат без базови линии и без контрол върху страничната сегментация. Викането на DLL-а маха и трите. Маха и стената на процеса, което значи, че Pascal свързване седи директно върху C структури, C булеви стойности и C-заделени низове. По-голямата част от това, което си заслужава да знаете за този adapter, е къде това свързване може тихо да се обърка

Как пускате Tesseract in-process от Delphi с HotPDF?

Пускането на Tesseract in-process с HotPDF струва едно фабрично извикване в unit-а HPDFTesseractRecognition и същия разговор ApplyLoadedOCRTextLayer, който всеки HotPDF OCR engine ползва. Фабриката валидира предварително. Файлът DLL и директорията tessdata трябва да съществуват, езиковият идентификатор може да съдържа само ASCII букви, цифри, _ и +, всеки модел в комбинация като chi_sim+eng трябва да има съответстващ файл .traineddata, а всички 21 задължителни export-и трябва да се намерят, преди engine-ът да бъде върнат. Грешки в конфигурацията вдигат 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 милиона, и adapter-ът я отказва, преди Tesseract да е видял и пиксел. Вдигнете MaxPixels (таванът е 67,108,864) или дръжте DPI-я където е; всяка страна също е ограничена до 32,767 пиксела

DLL-ът се зарича с LoadLibraryEx със search флаговете за собствената му папка плюс директориите по подразбиране за безопасно търсене, така че image библиотеките, от които Tesseract зависи, могат да живеят до него без пипане по PATH или текущата директория. HotPDF не опакова и не тегли никаква OCR среда или модел; и двете ги осигурявате вие

Какво се мени в сравнение с tesseract.exe adapter-а?

DLL adapter-ът разменя процесна изолация за по-богат изход и по-нисък разход на страница. И двата adapter-а се включват в същия тръбопровод на текстовия слой, така че картирането на координати, филтрирането по увереност и записът всичко-или-нищо са идентични; различното е как пикселите влизат и думите излизат

Aspecttesseract.exe adapterTesseract DLL adapter
ФабрикаHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Пиксели входBMP файл в лична временна директория8-битов grayscale буфер в паметта
Думи изходTSV на ниво дума, ограничен до 64 MiBРезултатен итератор, UTF-8 на дума
Базови линииНяма наличниПредавани от TessPageIteratorBaseline
Сегментация на страница и режим на engineСамо автоматична сегментацияTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutТвърд: дочерният процес се прекратяваКооперативен: Tesseract трябва да забележи
Изолация при срив и паметОтделен процесНяма, споделя вашето адресно пространство

Една цена не изчезва. Всеки разговор Recognize създава собствена инстанция на API-я и вика TessBaseAPIInit2, така че езиковите модели се инициализират на страница, а не веднъж на engine. Файловият кеш на операционната система омекотява презареждането, но при големи многомезични набори от модели това остава доминиращата фиксирана цена на страница и брои срещу крайния срок за разпознаване. In-process RapidOCR DLL engine-ът взима противоположния дизайн и държи ONNX моделите си резидентни през живота на engine-а; граничните проблеми (C ABI, заети буфери, непрекъсваема native работа) са от същото семейство

Защо Delphi не може да копира monitor структурата на Tesseract?

Delphi не може безопасно да огледали progress monitor-а на Tesseract, защото ETEXT_DESC съдържа зависещи от версията вътрешни полета, така че ръчно копиран запис поставя cancel callback-а и крайния срок на грешните отмествания на някои билдове. Нищо не се проваля гръмко, когато това стане. Tesseract просто прочита указателя към вашия callback от поле, което вече държи нещо друго, или изобщо никога не вижда крайния срок

HotPDF затова третира monitor-а като непрозрачен указател и го пипа само през експортирани функции: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs и TessMonitorDelete. Ако сами свържете C API-я за друга цел, същият образец важи. Скицата по-долу е ваш собствен свързващ код, не HotPDF API, и огледалява декларациите, които HotPDF ползва вътрешно

Обработка на monitor-а в HotPDF Tesseract DLL: копирането на записа ETEXT_DESC, зависещ от версията, поставя cancel callback-а и крайния срок на грешни отмествания и проваля тихо, докато HotPDF третира monitor-а като непрозрачен, задвижва TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc и TessMonitorSetDeadlineMSecs и пази cdecl callback-а без изключения
непрозрачен указател плюс пет export-а е целият договор; callback-ът остава еднобайтов Boolean, който чете само флаг и часовник
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. Същият header още усложнява, защото функции като TessPageIteratorBoundingBox връщат int, който HotPDF декларира като Integer. Четете C типа на всяка връщана стойност, вместо да предполагате една конвенция за целия API

Втората подробност е, че callback-ът никога не вдига. Delphi изключение, размотаващо се през C++ рамките на Tesseract, е недефинирано поведение, така че callback-ът на HotPDF чете само токена за отмяна и монотонна стойност GetTickCount64. Adapter-ът превръща резултата в диагностика за отмяна или timeout, след като TessBaseAPIRecognize се върне, и извършва тази проверка независимо от native кода за връщане

Кои native указатели притежава страната на Delphi?

HotPDF Tesseract DLL adapter-ът притежава три native обекта на заявка — инстанцията на API-я, monitor-ът и резултатният итератор — и заема всичко останало. Всеки разговор Recognize създава собствен набор и го освобождава в блок finally: TessResultIteratorDelete, после TessMonitorDelete, после TessBaseAPIDelete. Освобождаването на интерфейса на engine-а разтоварва библиотеката

Собственост на обектите в HotPDF Tesseract DLL на всеки разговор Recognize: резултатният итератор, monitor-ът и инстанцията на API-я са притежавани и освобождавани в този ред вътре в finally, страницният итератор от TessResultIteratorGetPageIterator е зает изглед, който никога не бива да се освобождава, а низовете от GetUTF8Text се копират и връщат през TessDeleteText
три обекта притежавани, всичко останало заето: освобождавайте в фиксирания ред, никога двукратно страницния итератор и никога не смесвайте алокатори
  • TessResultIteratorGetPageIterator връща зает изглед в резултатния итератор, не нов обект. HotPDF го ползва за TessPageIteratorBoundingBox и TessPageIteratorBaseline и никога не го освобождава; отделното му изтриване би освободило същата памет два пъти
  • TessResultIteratorGetUTF8Text връща низ, заделен от собствения runtime на DLL-а. HotPDF го копира и го връща през TessDeleteText в блок finally; Pascal FreeMem би го освободил на грешния heap
  • Текстът на думите се декодира със строга UTF-8 валидация и проверена дължина преди конверсията. Думи с контролни символи, развален UTF-8, кутии извън изображението, обърнати правоъгълници или увереност извън 0–100 провалят заявката вместо мълчаливо да бъдат закърпени
  • Общият текст на заявка е ограничен до 1,048,576 UTF-16 кодови единици, а броят думи трябва да се събере в бюджета на заявката, подаден надолу от ApplyLoadedOCRTextLayer

Увереността пристига като 0–100 и се мащабира до 0–1, така че THPDFOCRTextLayerOptions.MinimumConfidence значи едно и също за всеки engine. Когато Tesseract докладва базова линия, двата края се предават; иначе тръбопроводът на текстовия слой пада обратно на геометричната си оценка, точно както прави за TSV вход

Защо да валидираме enum, преди да стигне до DLL-а?

HotPDF копира суровия ordinal на PageSegMode и EngineMode в Integer преди проверката на диапазона, защото компилатор може да предположи, че променлива от enum винаги държи обявена стойност и да сгъне Ord(X) > Ord(High(T)) до константа false. Ordinal-ите не са украса: THPDFTesseractPageSegMode следва нумерацията на страничната сегментация на Tesseract от 0 до 13, THPDFTesseractEngineMode следва нумерацията на режимите на engine от 0 до 3, и двете отиват при DLL-а като обикновени цели числа. Запис с опции, построен с FillChar, попълнен от stream или подаден от C++Builder с кастнато цяло число, може да носи байт като 200. Валидирането на копирания ordinal превръща това в EArgumentException по време на фабриката вместо в недефиниран режим вътре в native кода. Фабриката също отхвърля tpsOSDOnly и tpsAutoOnly, които не произвеждат думи, и изисква osd.traineddata за tpsAutoOSD и tpsSparseTextOSD

Какво всъщност гарантира timeout-ът за разпознаване?

Timeout-ът на Tesseract DLL-а е кооперативен: HotPDF може да спре собствената си работа и да помоли Tesseract да спре, но не може да принуди native кода да се върне. Часовникът тръгва, когато Recognize започне, така че конверсията в bitmap и инициализацията на модела изразходват същия бюджет като разпознаването. HotPDF проверява изминалото време и токена за отмяна по време на grayscale конверсията и между думите, докато обхожда резултатите, и подава оставащите милисекунди на TessMonitorSetDeadlineMSecs, преди да извика TessBaseAPIRecognize

Пролуката е вътре в native разговора. Monitor-ът на Tesseract се пита по време на разпознаване на думи, не по време на TessBaseAPIInit2 или анализа на страничното оформление, така че бавно зареждане на модел или патологично оформление може да мине крайния срок, преди timeout-ът да бъде докладван. Пикселният и изходният бюджет също не ограничават собствения разход на памет на native библиотеката. Ако ви трябва работник, който можете да убиете, ползвайте process adapter-а; това е честният компромис, не липсваща възможност

Анатомия на кооперативния timeout в HotPDF Tesseract DLL: часовникът тръгва, когато Recognize започне, и покрива grayscale конверсията, TessBaseAPIInit2 и анализа на оформлението, но monitor-ът се пита само по време на разпознаване на думи, така че зареждания на модели и оформление могат да надхвърлят, преди HotPDF да докладва otlsEngineError или otlsCancelled
краен срок тук е молба, не гаранция: init и анализ на оформлението могат да вървят дълго, а работник, който истински можете да убиете, изисква process adapter-а

Страничната сегментация е мястото, където DLL adapter-ът си заслужава на труден вход. Формуляри, етикети и сканирани таблици с разпръснати полета често се разпознават по-добре с 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;

Timeout излиза като 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 bitmap манипулатора, така че GetDIBits върху този манипулатор връща старите пиксели. Симптомът беше озадачаващ: текст, начертан директно върху bitmap, се разпознаваше, докато страница, рендирана от PDF рендерера на HotPDF, произвеждаше празен списък с думи. На FPC adapter-ът вече чете формат-съзнателен snapshot през CreateIntfImage, който зачита пикселния формат и реда на редовете на суровото изображение. Delphi билдът пази пътя GetDIBits върху частно 24-битово копие. Нито един билд не модифицира bitmap-а на извикващия

Втората поправка принадлежи на tesseract.exe adapter-а. TStringList на FPC съхранява ANSI низове, така че задаването на декодиран UTF-8 TSV текст на Lines.Text мълчаливо изпускаше всеки китайски или допълнително-равнинен символ, който системната ANSI кодова страница не можеше да представи. Пътят на FPC вече пази TSV като UTF-8 байтове, маха BOM на байтово ниво и декодира всяка дума поотделно в UnicodeString. DLL adapter-ът никога не е имал този проблем, защото декодира всяка дума директно от итератора

Бърза справка

  • Фабрика: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) в HPDFTesseractRecognition, добавена във v2.772.0, FPC поддръжка във v2.772.1
  • Дефолти: tpsAuto, temDefault, 60,000 ms, 16,777,216 пиксела; диапазон на timeout 1–3,600,000 ms, пикселен таван 67,108,864
  • Съобразете битовостта на DLL-а с приложението и сложете зависимите DLL-и до Tesseract DLL-а
  • Третирайте monitor-а като непрозрачен; никога не копирайте ETEXT_DESC в Pascal запис
  • Обявете cancel callback-а cdecl с еднобайтов резултат Boolean и никога не пускайте изключение да избяга от него
  • Освобождавайте текста на итератора с TessDeleteText; никога не освобождавайте страницния итератор, получен от резултатния
  • Очаквайте крайният срок да е кооперативен: инициализацията на модела и анализът на оформлението могат да го надхвърлят
  • Ползвайте tesseract.exe adapter-а, когато имате нужда от твърдо спиране или изолация при срив

Tesseract DLL adapter-ът, process adapter-ите и вграденият OCR engine идват всички с HotPDF Delphi PDF компонента за Delphi, C++Builder и Free Pascal; вижте продуктовата страница на HotPDF за издания и изтегляния