Technický článek

HotPDF Tesseract DLL OCR: volání C API z Delphi

HotPDF provozuje Tesseract uvnitř vašeho Delphi procesu přes HPDFCreateTesseractDLLOCREngine, továrnu přidanou ve v2.772.0, která dynamicky nahraje DLL kompatibilní s Tesseract 5, ovládá jeho C API (TessBaseAPIInit2, TessBaseAPIRecognize, result iterator) a vrací IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer použije tenhle engine k přidání neviditelné, prohledávatelné Unicode textové vrstvy na naskenované PDF stránky

Tentýž recognizer už byl dostupný přes externí adapter tesseract.exe, který píše BMP a parsuje TSV. Ta cesta funguje, ale každá stránka platí za start procesu, dočasný soubor bitmapy a textový formát bez baseline a bez kontroly nad page segmentation. Volání DLL odstraňuje všechny tři. Odstraňuje ale i process zeď, což znamená, že Pascal binding sedí přímo na C strukturách, C booleanech a stringech alokovaných v C. Většina toho, co stojí za vědět o tomhle adapteru, je kde může tenhle binding potichu selhat

Jak provozujete Tesseract in-process z Delphi s HotPDF?

Provoz Tesseractu in-process s HotPDF stojí jedno volání továrny v unitu HPDFTesseractRecognition a totéž volání ApplyLoadedOCRTextLayer, které používá každý HotPDF OCR engine. Továrna validuje hned. Soubor DLL i adresář tessdata musí existovat, jazykový identifikátor smí obsahovat jen písmena ASCII, číslice, _ a +, každý model v kombinaci jako chi_sim+eng musí mít odpovídající soubor .traineddata a všech 21 povinných exportů se musí resolvovat, než se engine vrátí. Chyby konfigurace vyhodí EArgumentException; DLL, který se nepodaří nahrát, vyhodí EOSError s Windows kódem chyby a tipem zkontrolovat architekturu a závislosti

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Win64 aplikace potřebuje 64bitovou DLL; závislé DLL se pokládají vedle ní
  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
    // prázdný seznam stránek znamená všechny stránky; stránky, které už text mají, se přeskakují
    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 nastaví PageSegMode na tpsAuto, EngineMode na temDefault, TimeoutMilliseconds na 60 000 a MaxPixels na 16 777 216. Pixelový rozpočet je důležitější, než vypadá. Stránka US Letter v defaultních 300 DPI se vyrenderuje na 2 550 × 3 300 pixelů, zhruba 8,4 milionu, což se vejde. Táž stránka v 600 DPI je 5 100 × 6 600, zhruba 33,7 milionu, a adapter ji odmítne, dřív než Tesseract uvidí jediný pixel. Zvyšte MaxPixels (strop je 67 108 864) nebo nechte DPI, kde je; každá strana je navíc omezena na 32 767 pixelů

DLL se nahraje přes LoadLibraryEx s vyhledávacími příznaky pro vlastní složku DLL plus defaultní bezpečné adresáře, takže image knihovny, na kterých Tesseract závisí, můžou bydlet vedle něj bez sahání na PATH nebo aktuální adresář. HotPDF nepřibaluje ani nestahuje žádný OCR runtime ani model; obojí zajišťujete sami

Co se mění proti adapteru tesseract.exe?

DLL adapter vyměňuje process izolaci za bohatší výstup a nižší režii na stránku. Oba adaptery se zapojují do téže textové pipeline, takže mapování souřadnic, filtrování podle confidence i all-or-nothing commit jsou identické; liší se jen to, jak pixely vstupují a slova vystupují

AspektAdapter tesseract.exeAdapter Tesseract DLL
TovárnaHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Vstup pixelůSoubor BMP v privátním dočasném adresáři8bitový grayscale buffer v paměti
Výstup slovTSV na úrovni slov, strop 64 MiBResult iterator, UTF-8 na slovo
BaselinesNedostupnéPropustí se z TessPageIteratorBaseline
Page segmentation a engine modeJen automatická segmentaceTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutHard: dceřiný proces se ukončíKooperativní: Tesseract si toho musí všimnout
Izolace pádů a pamětiSamostatný procesŽádná, sdílí váš adresní prostor

Jedna cena nezmizí. Každé volání Recognize vytvoří vlastní instanci API a volá TessBaseAPIInit2, takže jazykové modely se inicializují na stránku, ne jednou za engine. Souborový cache operačního systému reload zmírní, ale u velkých vícejazykových sad modelů je to pořád dominantní fixní cena stránky a počítá se do rozpoznávacího deadline. In-process RapidOCR DLL engine jde opačnou cestou a drží své ONNX modely rezidentní po celý život engine; hraniční problémy (C ABI, vypůjčené buffery, nepřerušitelná nativní práce) jsou tatáž rodina

Proč Delphi nesmí kopírovat strukturu monitoru Tesseractu?

Delphi nemůže bezpečně zrcadlit progress monitor Tesseractu, protože ETEXT_DESC obsahuje interní pole závislá na verzi, takže ručně okopírovaný record dá cancel callback a deadline na špatné offsety na některých buildech. Nic hlasitě neselže, když se to stane. Tesseract si prostě přečte váš callback ukazatel z pole, které teď drží něco jiného, nebo deadline nikdy neuzří

HotPDF proto bere monitor jako neprůhledný ukazatel a sahá na něj jen přes exportované funkce: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs a TessMonitorDelete. Pokud si C API bindujete sami pro jiný účel, platí týž vzor. Náčrt dole je váš vlastní binding kód, ne API HotPDF, a zrcadlí deklarace, které HotPDF používá interně

Práce s monitorem v HotPDF Tesseract DLL: okopírování recordu ETEXT_DESC závislého na verzi dá cancel callback a deadline na špatné offsety a selhává potichu, zatímco HotPDF bere monitor jako neprůhledný, ovládá ho přes TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc a TessMonitorSetDeadlineMSecs a drží cdecl callback bez výjimek
neprůhledný ukazatel plus pět exportů je celá smlouva; callback zůstává jednobajtové Boolean, které čte jen příznak a hodiny
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, nikdy se nedereferencuje
  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
  // Běží na stacku Tesseractu: čti příznaky a hodiny, nikdy nevyhazuj výjimku
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Použití, s ukazateli na funkce resolvovanými přes GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Dva detaily v tomhle náčrtu jsou záměrné. Callback vrací Boolean, který je jeden bajt v Delphi i Free Pascalu, což odpovídá C bool v TessCancelFunc. Čtyřbajtové Windows BOOL nebo Delphi LongBool vypadá zaměnitelně a není: když jedna strana zapíše jeden bajt a druhá čte čtyři, horní bajty návratového registru jsou to, co tam zbylo, a false může dorazit jako true. Týž header to komplikuje dál, protože funkce jako TessPageIteratorBoundingBox vrací int, který HotPDF deklaruje jako Integer. Čtěte C typ každé návratové hodnoty místo předpokládání jedné konvence pro celé API

Druhý detail je, že callback nikdy nevyhazuje výjimku. Delphi výjimka unwindující přes C++ snímky Tesseractu je undefined behavior, takže callback HotPDF čte jen cancellation token a monotonní hodnotu GetTickCount64. Adapter promění výsledek v diagnostiku zrušení nebo timeoutu poté, co se TessBaseAPIRecognize vrátí, a tu kontrolu provádí nezávisle na nativním návratovém kódu

Které nativní ukazatele vlastní strana Delphi?

HotPDF Tesseract DLL adapter vlastní na požadavek tři nativní objekty, instanci API, monitor a result iterator, a všechno ostatní si jen půjčuje. Každé volání Recognize si vytvoří vlastní sadu a uvolní ji v bloku finally: TessResultIteratorDelete, pak TessMonitorDelete, pak TessBaseAPIDelete. Uvolnění interface engine odloaduje knihovnu

Vlastnictví nativních objektů HotPDF Tesseract DLL na jedno volání Recognize: result iterator, monitor a instance API se vlastní a uvolňují v tomto pořadí uvnitř finally, page iterator z TessResultIteratorGetPageIterator je vypůjčený pohled, který se nikdy nesmí uvolnit, a stringy z GetUTF8Text se zkopírují a vrátí přes TessDeleteText
tři vlastněné objekty, všechno ostatní vypůjčené: uvolňujte v pevném pořadí, nikdy nedělejte double-free na page iteratoru a nikdy nemíchejte alokátory
  • TessResultIteratorGetPageIterator vrací vypůjčený pohled do result iteratoru, ne nový objekt. HotPDF ho používá pro TessPageIteratorBoundingBox a TessPageIteratorBaseline a nikdy ho neuvolňuje; samostatné smazání by uvolnilo tutéž paměť dvakrát
  • TessResultIteratorGetUTF8Text vrací string alokovaný vlastním runtimem DLL. HotPDF ho zkopíruje a vrátí přes TessDeleteText v bloku finally; Pascalovské FreeMem by ho uvolnilo na špatném heapu
  • Text slova se dekóduje s přísnou validací UTF-8 a kontrolou délky před konverzí. Slova s řídicími znaky, poškozeným UTF-8, boxy mimo obrázek, inverted obdélníky nebo confidence mimo 0–100 failnou požadavek místo toho, aby se potichu opravila
  • Celkový text na požadavek je stropovaný na 1 048 576 UTF-16 code units a počet slov se musí vejít do budgetu požadavku předaného z ApplyLoadedOCRTextLayer

Confidence přijde jako 0–100 a škáluje se na 0–1, takže THPDFOCRTextLayerOptions.MinimumConfidence znamená totéž pro každý engine. Když Tesseract nahlásí baseline, propustí se oba koncové body; jinak pipeline textové vrstvy spadne na svůj geometrický odhad, úplně stejně jako u TSV vstupu

Proč validovat enum, dřív než dorazí do DLL?

HotPDF kopíruje surový ordinal PageSegMode a EngineMode do Integer, než provede kontrolu rozsahu, protože kompilátor může předpokládat, že proměnná enum vždy drží deklarovanou hodnotu, a složí Ord(X) > Ord(High(T)) na konstantní false. Ordinály nejsou ozdoba: THPDFTesseractPageSegMode sleduje číslování page segmentation Tesseractu od 0 do 13, THPDFTesseractEngineMode sleduje číslování engine mode od 0 do 3 a obojí putuje do DLL jako prosté celé číslo. Options record postavený přes FillChar, naplněný ze streamu nebo předaný z C++Builder s přetypovaným celým číslem může nést bajt jako 200. Validace okopírovaného ordinalu z toho udělá EArgumentException už v továrně místo nedefinovaného režimu uvnitř nativního kódu. Továrna navíc odmítá tpsOSDOnly a tpsAutoOnly, které neprodukují žádná slova, a vyžaduje osd.traineddata pro tpsAutoOSD a tpsSparseTextOSD

Co vlastně garantuje rozpoznávací timeout?

Timeout Tesseract DLL je kooperativní: HotPDF dokáže zastavit svou práci a Tesseract požádat o zastavení, ale nemůže nativní kód donutit se vrátit. Hodiny startují, když Recognize začíná, takže konverze bitmapy i inicializace modelů spotřebují tentýž rozpočet jako rozpoznávání. HotPDF kontroluje uplynulý čas i cancellation token během grayscale konverze a mezi slovy při iterování výsledků a zbývající milisekundy předá TessMonitorSetDeadlineMSecs, než zavolá TessBaseAPIRecognize

Ta mezera je uvnitř nativního volání. Monitor Tesseractu se konzultuje během rozpoznávání slov, ne během TessBaseAPIInit2 nebo analýzy page layout, takže pomalé nahrání modelu nebo patologický layout může běžet přes deadline, dřív než se timeout nahlásí. Pixelový i výstupní rozpočet navíc nestropují vlastní paměť nativní knihovny. Pokud potřebujete workera, kterého zabijete, použijte process adapter; to je čestný kompromis, ne chybějící funkce

Anatomie kooperativního timeoutu HotPDF Tesseract DLL: hodiny startují, když začíná Recognize, a pokrývají grayscale konverzi, TessBaseAPIInit2 i analýzu layoutu, ale monitor se konzultuje jen během rozpoznávání slov, takže nahrání modelu a layout můžou přeběhnout, dřív než HotPDF nahlásí otlsEngineError nebo otlsCancelled
deadline je tady žádost, ne garance: init a analýza layoutu můžou běžet dlouho a workera, kterého skutečně zabijete, potřebuje process adapter

Page segmentation je místo, kde si DLL adapter na obtížném vstupu vydělává. Formuláře, štítky a naskenované tabulky s roztroušenými poli se často rozpoznají lépe s tpsSparseText než s automatickou segmentací, která se snaží skládat sloupce a odstavce, které tam nejsou

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;  // roztroušená pole, žádné skládání sloupců
  TessOptions.EngineMode := temLSTMOnly;     // vyžaduje LSTM modely v tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // zahrnuje inicializaci modelů
  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 se projeví jako otlsEngineError s diagnostikou Tesseract DLL OCR timed out a zrušený token jako otlsCancelled. V obou případech má ApplyLoadedOCRTextLayer rozpoznány všechny vybrané stránky, než spustí commit transakci, takže selhání na stránce 40 z 50 nechá nahraný dokument přesně takový, jaký byl. Pozor, tpsSingleLine, tpsSingleBlock a tpsSparseText mění jen segmentaci; žádný z nich nenarovná nakřivo naskenovanou stránku

Free Pascal a Lazarus: zastaralé pixely a ztracená čínština

Obě Tesseract továrny fungují ve Windows buildech Free Pascal a Lazarus Win32 i Win64 od v2.772.1, po dvou FPC-specifických opravách. Lazarus balíček nejdřív přebudujte pro cílovou architekturu; obecný port popisuje HotPDF na Free Pascal a Lazarus Win64

První oprava se týká pixelů. LCL TBitmap zapisovaný přes scanlines může aktualizovat svůj syrový obraz, aniž by osvěžil Windows bitmap handle, takže GetDIBits na tom handle vrací staré pixely. Příznak byl matoucí: text nakreslený přímo na bitmap se rozpoznal, zatímco stránka vyrenderovaná PDF rendererem HotPDF produkovala prázdný seznam slov. Na FPC čte adapter teď formátově uvědomělý snapshot přes CreateIntfImage, který respektuje pixelový formát a pořadí řádků syrového obrazu. Delphi build drží cestu GetDIBits na privátní 24bitové kopii. Ani jeden build nemění bitmap volajícího

Druhá oprava patří adapteru tesseract.exe. TStringList ve FPC ukládá ANSI stringy, takže přiřazení dekódovaného UTF-8 TSV textu do Lines.Text potichu zahodilo každý čínský znak nebo znak ze supplementary plane, který systémová ANSI code page nedokázala reprezentovat. Cesta FPC teď drží TSV jako UTF-8 bajty, strhne BOM na úrovni bajtů a dekóduje každé slovo zvlášť do UnicodeString. DLL adapter tenhle problém nikdy neměl, protože dekóduje každé slovo přímo z iteratoru

Rychlá reference

  • Továrna: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) v unitu HPDFTesseractRecognition, přidaná ve v2.772.0, FPC podpora ve v2.772.1
  • Defaulty: tpsAuto, temDefault, 60 000 ms, 16 777 216 pixelů; rozsah timeoutu 1–3 600 000 ms, pixelový strop 67 108 864
  • Přiřaďte bitnost DLL k aplikaci a pokládejte závislé DLL vedle Tesseract DLL
  • Berte monitor jako neprůhledný; nikdy nekopírujte ETEXT_DESC do Pascal recordu
  • Deklarujte cancel callback jako cdecl s jednobajtovým výsledkem Boolean a nikdy nenechte z něj utéct výjimku
  • Uvolňujte text iteratoru přes TessDeleteText; nikdy neuvolňujte page iterator získaný z result iteratoru
  • Počítejte s kooperativním deadlinem: inicializace modelů a analýza layoutu ho můžou přeběhnout
  • Použijte adapter tesseract.exe, když potřebujete tvrdé ukončení nebo izolaci pádů

Tesseract DLL adapter, process adaptery i vestavěný OCR engine lodí společně s HotPDF Delphi PDF komponentou pro Delphi, C++Builder a Free Pascal; edice a stažení najdete na produktové stránce HotPDF