Teknisk artikel

HotPDF Tesseract DLL OCR: C API'et kaldt fra Delphi

HotPDF kører Tesseract inde i din Delphi-proces gennem HPDFCreateTesseractDLLOCREngine, en factory tilføjet i v2.772.0, der dynamisk loader en Tesseract 5-kompatibel DLL, driver dens C API (TessBaseAPIInit2, TessBaseAPIRecognize, result iterator) og returnerer en IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer bruger den engine til at tilføje et usynligt, søgbart Unicode-tekstlag til scannede PDF-sider

Samme recognizer var allerede tilgængelig gennem den eksterne tesseract.exe-adapter, der skriver en BMP og parser TSV. Den vej virker, men hver side betaler for et processtart, en midlertidig bitmap-fil og et tekstformat uden baselines og uden kontrol over pagesegmentering. At kalde DLL'en fjerner alle tre. Den fjerner også procesmuren, hvilket betyder, at en Pascal-binding sidder direkte oven på C-strukturer, C-booleaner og C-allokerede strenge. Det meste af det, der er værd at vide om denne adapter, er, hvor den binding stille kan gå galt

Hvordan kører du Tesseract in-process fra Delphi med HotPDF?

At køre Tesseract in-process med HotPDF tager ét factory-kald i HPDFTesseractRecognition-uniten og samme ApplyLoadedOCRTextLayer-kald, som hver HotPDF OCR-engine bruger. Factoryen validerer tidligt. DLL-filen og tessdata-mappen skal findes, sprog-identifikatoren må kun indeholde ASCII-bogstaver, cifre, _ og +, hver model i en kombination som chi_sim+eng skal have en matchende .traineddata-fil, og alle 21 påkrævede exports skal kunne opløses, før enginen returneres. Konfigurationsfejl rejser EArgumentException; en DLL, der fejler at loade, rejser EOSError med Windows-fejlkoden og et hint om at tjekke arkitektur og afhængigheder

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // En Win64-applikation behøver en 64-bit DLL; afhængigheds-DLL'er lægges ved siden af
  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
    // En tom sideliste betyder alle sider; sider med tekst i forvejen skippes
    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 sætter PageSegMode til tpsAuto, EngineMode til temDefault, TimeoutMilliseconds til 60.000 og MaxPixels til 16.777.216. Pixel-budgettet betyder mere, end det ser ud. En US Letter-side ved default 300 DPI renderer til 2.550 × 3.300 pixels, omkring 8,4 millioner, hvilket er plads. Samme side ved 600 DPI er 5.100 × 6.600, omkring 33,7 millioner, og adapteren afviser den, før Tesseract ser en pixel. Hæv MaxPixels (loftet er 67.108.864) eller lad DPI'en stå; hver side er også kappet ved 32.767 pixels

DLL'en loades med LoadLibraryEx med search-flaggene for DLL'ens egen folder plus default safe directories, så de billedbiblioteker, Tesseract er afhængig af, kan ligge ved siden af uden at røre PATH eller den aktuelle mappe. HotPDF bundler eller downloader intet OCR-runtime eller nogen model; du provisionerer begge

Hvad ændrer sig sammenlignet med tesseract.exe-adapteren?

DLL-adapteren bytter procesisolering til rigere output og lavere omkostning pr. side. Begge adaptere kobler sig på samme tekstlags-pipeline, så koordinatmapping, confidence-filtrering og alt-eller-intet-committet er identiske; det, der adskiller, er, hvordan pixels kommer ind, og hvordan ord kommer ud

Aspekttesseract.exe-adapterTesseract DLL-adapter
FactoryHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Pixels indBMP-fil i en privat midlertidig mappe8-bit grayscale-buffer i hukommelsen
Ord udWord-level TSV, loft 64 MiBResult iterator, UTF-8 pr. ord
BaselinesIkke tilgængeligeGivet videre fra TessPageIteratorBaseline
Pagesegmentering og engine modeKun automatisk segmenteringTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutHård: barneprocessen termineresKooperativ: Tesseract selv må bemærke det
Crash- og hukommelsesisoleringSeparat procesIngen, deler dit address space

Én omkostning forsvinder ikke. Hvert Recognize-kald opretter sin egen API-instans og kalder TessBaseAPIInit2, så sprogmodellerne initialiseres pr. side frem for én gang pr. engine. Operativsystemets filcache blødgør genindlæsningen, men på store multi-sprog-modelsæt er det stadig den dominerende faste omkostning pr. side, og det tæller med i recognition-deadlinen. In-process RapidOCR-DLL-enginen tager det modsatte design og holder sine ONNX-modeller residente i enginens levetid; grænsefladeproblemerne (C ABI, lånte buffere, uafbrydeligt native arbejde) er samme familie

Hvorfor kan Delphi ikke kopiere Tesseract monitor-strukturen?

Delphi kan ikke sikkert spejle Tesseracts progress monitor, for ETEXT_DESC indeholder versionsafhængige interne felter, så en håndkopieret record placerer cancel-callbacket og deadlinen på forkerte offsets på nogle builds. Intet fejler højtlydende, når det sker. Tesseract læser bare din callback-pointer fra et felt, der nu holder noget andet, eller ser aldrig deadlinen

HotPDF behandler derfor monitoren som en opak pointer og rører den kun gennem eksporterede funktioner: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs og TessMonitorDelete. Binder du C API'et selv til et andet formål, gælder samme mønster. Skitsen nedenfor er din egen bindingskode, ikke HotPDF API, og spejler de deklarationer, HotPDF bruger internt

HotPDF'ens Tesseract-DLL monitor-håndtering: at kopiere den versionsafhængige ETEXT_DESC-record placerer cancel-callbacket og deadlinen på forkerte offsets og fejler stille, mens HotPDF behandler monitoren som opak, driver TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc og TessMonitorSetDeadlineMSecs og holder cdecl-callbacket exception-frit
en opak pointer plus fem exports er hele kontrakten; callbacket forbliver en én-byte Boolean, der kun læser et flag og et ur
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, aldrig derefereret
  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
  // Kører på Tesseracts stak: læs flag og uret, rejse aldrig
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Anvendelse, med funktionspointerne opløst af GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

To detaljer i den skitse er bevidste. Callbacket returnerer Boolean, som er én byte i både Delphi og Free Pascal og matcher C bool i TessCancelFunc. Den fire-byte Windows BOOL eller Delphi LongBool ser udskiftelig ud og er det ikke: skriver den ene side én enkelt byte, og læser den anden fire, er de øvre bytes i return-registeret, hvad end der var tilbage dér, og en false kan ankomme som true. Samme header komplicerer det yderligere, for funktioner som TessPageIteratorBoundingBox returnerer en int, som HotPDF deklarerer som Integer. Læs C-typen af hver returværdi i stedet for at antage én konvention for hele API'et

Den anden detalje er, at callbacket aldrig rejser. En Delphi-exception, der unwinder gennem Tesseracts C++-frames, er udefineret adfærd, så HotPDF's callback læser kun cancellation-tokenet og en monoton GetTickCount64-værdi. Adapteren omdanner resultatet til en cancellation- eller timeout-diagnostik, efter TessBaseAPIRecognize returnerer, og den udfører det tjek uanset den native returkode

Hvilke native pointere ejer Delphi-siden?

HotPDF's Tesseract-DLL-adapter ejer tre native objekter pr. request, API-instansen, monitoren og result iteratoren, og låner alt andet. Hvert Recognize-kald opretter sit eget sæt og frigiver det i en finally-blok: TessResultIteratorDelete, derefter TessMonitorDelete, derefter TessBaseAPIDelete. At frigive engine-interfacet unloades biblioteket

HotPDF Tesseract-DLL objektejerskab pr. Recognize-kald: result iteratoren, monitoren og API-instansen ejes og frigøres i den rækkefølge inde i finally, side-iteratoren fra TessResultIteratorGetPageIterator er et lånt view, der aldrig må frigøres, og GetUTF8Text-strenge kopieres og returneres via TessDeleteText
tre objekter ejet, alt andet lånt: frigør i fast rækkefølge, lav aldrig double free på side-iteratoren, og bland aldrig allocators
  • TessResultIteratorGetPageIterator returnerer et lånt view ind i result iteratoren, ikke et nyt objekt. HotPDF bruger det til TessPageIteratorBoundingBox og TessPageIteratorBaseline og frigør det aldrig; at slette det separat ville frigøre samme hukommelse to gange
  • TessResultIteratorGetUTF8Text returnerer en streng allokeret af DLL'ens eget runtime. HotPDF kopierer den og giver den tilbage gennem TessDeleteText i en finally-blok; Pascal FreeMem ville frigive den på den forkerte heap
  • Ordtekst dekodes med strict UTF-8-validering og længdetjekkes før konvertering. Ord med kontroltegn, misdannet UTF-8, boxes uden for billedet, inverterede rektangler eller confidence uden for 0-100 fejler requesten i stedet for at blive lydløst lappet
  • Samlet tekst pr. request er kappet ved 1.048.576 UTF-16 kodeenheder, og ordantallet skal passe til den request-budget, ApplyLoadedOCRTextLayer giver videre

Confidence ankommer som 0-100 og skaleres til 0-1, så THPDFOCRTextLayerOptions.MinimumConfidence betyder det samme for hver engine. Når Tesseract melder en baseline, gives begge endepunkter videre; ellers falder tekstlags-pipelinen tilbage til sit geometriske estimat, præcis som ved TSV-input

Hvorfor validere en enum, før den når DLL'en?

HotPDF kopierer den rå ordinal af PageSegMode og EngineMode ind i en Integer, før den tjekker intervallet, for en kompiler kan antage, at en enum-variabel altid holder en deklareret værdi, og folde Ord(X) > Ord(High(T)) sammen til en konstant false. Ordinalerne er ikke pynt: THPDFTesseractPageSegMode følger Tesseracts pagesegmenterings-nummerering fra 0 til 13, THPDFTesseractEngineMode følger engine mode-nummereringen fra 0 til 3, og begge går til DLL'en som rene integers. En options-record bygget med FillChar, fyldt fra en stream eller givet videre fra C++Builder med et cast integer kan bære en byte som 200. At validere den kopierede ordinal omdanner det til en EArgumentException på factory-tidspunktet i stedet for en udefineret mode inde i native kode. Factoryen afviser desuden tpsOSDOnly og tpsAutoOnly, som ikke producerer nogen ord, og kræver osd.traineddata for tpsAutoOSD og tpsSparseTextOSD

Hvad garanterer recognition-timeouten egentlig?

Tesseract-DLL-timeouten er kooperativ: HotPDF kan stoppe sit eget arbejde og bede Tesseract om at stoppe, men den kan ikke tvinge native kode til at returnere. Uret starter, når Recognize begynder, så bitmap-konvertering og model-initialisering bruger samme budget som recognition. HotPDF tjekker forløbet tid og cancellation-tokenet under grayscale-konverteringen og mellem ord, mens den itererer resultater, og giver de resterende millisekunder til TessMonitorSetDeadlineMSecs, før den kalder TessBaseAPIRecognize

Hullet er inde i det native kald. Tesseracts monitor konsulteres under ord-genkendelse, ikke under TessBaseAPIInit2 eller side-layout-analyse, så en langsom model-load eller et patologisk layout kan løbe forbi deadlinen, før timeouten meldes. Pixel- og output-budgetter begrænser heller ikke det native biblioteks egen hukommelsesbrug. Behøver du en worker, du kan dræbe, så brug procesadapteren; det er den ærlige afvejning, ikke en manglende feature

HotPDF Tesseract-DLL kooperativ timeout-anatomi: uret starter, når Recognize begynder, og dækker grayscale-konvertering, TessBaseAPIInit2 og layout-analyse, men monitoren konsulteres kun under ord-genkendelse, så model-loads og layout kan løbe over, før HotPDF melder otlsEngineError eller otlsCancelled
en deadline her er en anmodning, ikke en garanti: init og layout-analyse kan løbe langt, og en worker, du virkelig kan dræbe, behøver procesadapteren

Pagesegmentering er dér, DLL-adapteren tjener sit hold på besværligt input. Formularer, labels og scannede tabeller med spredte felter genkender ofte bedre med tpsSparseText end med automatisk segmentering, som forsøger at samle kolonner og afsnit, der ikke er der

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;  // spredte felter, ingen kolonne-samling
  TessOptions.EngineMode := temLSTMOnly;     // behøver LSTM-modeller i tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // inkluderer model-initialisering
  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;

En timeout viser sig som otlsEngineError med diagnostikken Tesseract DLL OCR timed out, mens et annulleret token viser sig som otlsCancelled. I begge tilfælde har ApplyLoadedOCRTextLayer genkendt hver valgte side, før den starter commit-transaktionen, så en fejl på side 40 af 50 efterlader det indlæste dokument præcis, som det var. Bemærk, at tpsSingleLine, tpsSingleBlock og tpsSparseText kun ændrer segmentering; ingen af dem retter en skæv scanning op

Free Pascal og Lazarus: forældede pixels og mistet kinesisk

Begge Tesseract-factories virker i Windows Free Pascal og Lazarus Win32- og Win64-builds siden v2.772.1, efter to FPC-specifikke fixes. Genbyg Lazarus-pakken til target-arkitekturen først; den generelle port er dækket i HotPDF på Free Pascal og Lazarus Win64

Det første fix handler om pixels. En LCL TBitmap skrevet gennem scanlines kan opdatere sit raw-billede uden at genopfriske Windows bitmap-handle, så GetDIBits på den handle returnerer de gamle pixels. Symptomet var gådefuldt: tekst tegnet direkte på en bitmap blev genkendt, mens en side rendreret af HotPDF's PDF-renderer gav en tom ordliste. På FPC læser adapteren nu et formatbevidst snapshot gennem CreateIntfImage, som respekterer raw-billedets pixelformat og rækkefølge. Delphi-builden beholder GetDIBits-vejen på en privat 24-bit kopi. Ingen af buildene modificerer kalderens bitmap

Det andet fix hører til tesseract.exe-adapteren. FPC's TStringList gemmer ANSI-strenge, så at tildele dekodet UTF-8 TSV-tekst til Lines.Text droppede stille hvert kinesisk eller supplementary-plane-tegn, som systemets ANSI-kodepage ikke kunne repræsentere. FPC-vejen beholder nu TSV'en som UTF-8-bytes, stripper BOM'en på byte-niveau og dekoder hvert ord til UnicodeString hver for sig. DLL-adapteren havde aldrig dette problem, for den dekoder hvert ord direkte fra iteratoren

Hurtig reference

  • Factory: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) i HPDFTesseractRecognition, tilføjet i v2.772.0, FPC-understøttelse i v2.772.1
  • Defaults: tpsAuto, temDefault, 60.000 ms, 16.777.216 pixels; timeout-interval 1-3.600.000 ms, pixel-loft 67.108.864
  • Match DLL-bitness til applikationen, og læg afhængigheds-DLL'er ved siden af Tesseract-DLL'en
  • Behandl monitoren som opak; kopier aldrig ETEXT_DESC ind i en Pascal-record
  • Deklarér cancel-callbacket cdecl med et én-byte Boolean-resultat, og lad aldrig en exception undslippe det
  • Frigør iterator-tekst med TessDeleteText; frigør aldrig side-iteratoren, der er hentet fra result iteratoren
  • Forvent, at deadlinen er kooperativ: model-initialisering og layout-analyse kan løbe over den
  • Brug tesseract.exe-adapteren, når du behøver hård terminering eller crash-isolering

Tesseract-DLL-adapteren, procesadapterne og den indbyggede OCR-engine skiber alle med HotPDF Delphi PDF-komponenten til Delphi, C++Builder og Free Pascal; se HotPDF-produktsiden for udgaver og downloads