Technisch artikel

HotPDF Tesseract DLL-OCR: de C API aanroepen vanuit Delphi

HotPDF draait Tesseract binnen uw Delphi-proces via HPDFCreateTesseractDLLOCREngine, een factory toegevoegd in v2.772.0 die dynamisch een Tesseract 5-compatibele DLL laadt, zijn C API aanstuurt (TessBaseAPIInit2, TessBaseAPIRecognize, de resultaatiterator) en een IHPDFOCREngine teruggeeft. THotPDF.ApplyLoadedOCRTextLayer gebruikt die engine om een onzichtbare, doorzoekbare Unicode-tekstlaag toe te voegen aan gescande PDF-pagina's

Dezelfde recognizer was al bereikbaar via de externe tesseract.exe-adapter die een BMP wegschrijft en TSV parset. Dat pad werkt, maar elke pagina betaalt voor een processtart, een tijdelijk bitmapbestand en een tekstformaat zonder baselines en zonder zeggenschap over paginasegmentatie. Het aanroepen van de DLL schrapt alle drie. Het schrapt ook de procesmuur, en dat betekent dat een Pascal-binding rechtstreeks op C-structuren, C-booleans en door C toegewezen strings zit. Het meeste wat de moeite van weten waard is over deze adapter, is waar die binding stilletjes mis kan gaan

Hoe draait u Tesseract in-process vanuit Delphi met HotPDF?

Tesseract in-process draaien met HotPDF kost één factory-aanroep in de unit HPDFTesseractRecognition en dezelfde ApplyLoadedOCRTextLayer-aanroep die elke HotPDF-OCR-engine gebruikt. De factory valideert gretig. Het DLL-bestand en de tessdata-map moeten bestaan, de taalidentifier mag alleen ASCII-letters, cijfers, _ en + bevatten, elk model in een combinatie zoals chi_sim+eng moet een matchend .traineddata-bestand hebben, en alle 21 vereiste exports moeten resolvbaar zijn voordat de engine wordt teruggegeven. Configuratiefouten geven een EArgumentException; een DLL die niet laadt geeft een EOSError met de Windows-foutcode en de hint om architectuur en afhankelijkheden te controleren

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Een Win64-applicatie heeft een 64-bit DLL nodig; afhankelijkheids-DLL's ernaast
  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
    // Een lege paginalijst betekent elke pagina; pagina's die al tekst hebben worden overgeslagen
    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 zet PageSegMode op tpsAuto, EngineMode op temDefault, TimeoutMilliseconds op 60.000 en MaxPixels op 16.777.216. Het pixelbudget telt zwaarder dan het lijkt. Een US Letter-pagina op de default 300 DPI rendert naar 2.550 × 3.300 pixels, zo'n 8,4 miljoen, wat past. Dezelfde pagina op 600 DPI is 5.100 × 6.600, zo'n 33,7 miljoen, en de adapter weigert haar voordat Tesseract één pixel ziet. Verhoog MaxPixels (het plafond is 67.108.864) of laat de DPI staan; elke zijde is bovendien geplafonneerd op 32.767 pixels

De DLL wordt geladen met LoadLibraryEx met de zoekvlaggen voor de eigen map van de DLL plus de default veilige directories, dus de beeldbibliotheken waar Tesseract van afhankelijk is kunnen naast hem huizen zonder PATH of de huidige directory aan te raken. HotPDF bundelt of downloadt geen enkele OCR-runtime of model; beide voorziet u zelf

Wat verandert er vergeleken met de tesseract.exe-adapter?

De DLL-adapter ruilt procesisolatie in voor rijkere uitvoer en lagere overhead per pagina. Beide adapters koppelen in op dezelfde tekstlaag-pijplijn, dus coördinaatmapping, confidencefiltering en de alles-of-niets-commit zijn identiek; wat verschilt is hoe pixels erin gaan en woorden eruit komen

Aspecttesseract.exe-adapterTesseract DLL-adapter
FactoryHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Pixels erinBMP-bestand in een privé-tijdelijke directory8-bit grijswaardenbuffer in het geheugen
Woorden eruitWord-niveau TSV, geplafonneerd op 64 MiBResultaatiterator, UTF-8 per woord
BaselinesNiet beschikbaarDoorgegeven vanuit TessPageIteratorBaseline
Paginasegmentatie en engine-modusAlleen automatische segmentatieTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutHard: het kindproces wordt beëindigdCoöperatief: Tesseract moet het merken
Crash- en geheugenisolatieApart procesGeen, deelt uw adresruimte

Eén kost verdwijnt niet. Elke Recognize-aanroep maakt zijn eigen API-instantie aan en roept TessBaseAPIInit2 aan, dus de taalmodellen worden per pagina geïnitialiseerd in plaats van één keer per engine. De bestandscache van het besturingssysteem verzacht het herladen, maar bij grote meertalige modelsets blijft het de dominante vaste kostenpost per pagina, en hij telt mee binnen de recognition-deadline. De in-process RapidOCR-DLL-engine kiest het tegenovergestelde ontwerp en houdt zijn ONNX-modellen resident gedurende de levensduur van de engine; de grensproblemen (C ABI, geleende buffers, onderbreekbaar native werk) zijn dezelfde familie

Waarom kan Delphi de Tesseract-monitorstructuur niet kopiëren?

Delphi kan de Tesseract-voortgangsmonitor niet veilig spiegelen omdat ETEXT_DESC versieafhankelijke interne velden bevat, dus een met de hand gekopieerd record zet de cancel-callback en deadline op verkeerde offsets op sommige builds. Niets faalt luidruchtig zodra dat gebeurt. Tesseract leest simpelweg uw callbackpointer uit een veld dat inmiddels iets anders bevat, of ziet de deadline helemaal nooit

HotPDF behandelt de monitor daarom als een opake pointer en raakt haar alleen aan via geëxporteerde functies: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs en TessMonitorDelete. Bindt u de C API zelf voor een ander doel, dan geldt hetzelfde patroon. De schets hieronder is uw eigen bindingscode, geen HotPDF-API, en spiegelt de declaraties die HotPDF intern gebruikt

HotPDF Tesseract DLL-monitorafhandeling: het kopiëren van het versieafhankelijke ETEXT_DESC-record zet de cancel-callback en deadline op verkeerde offsets en faalt stilletjes, terwijl HotPDF de monitor als opaak behandelt, TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc en TessMonitorSetDeadlineMSecs aanstuurt, en de cdecl-callback exceptionvrij houdt
een opake pointer plus vijf exports is het hele contract; de callback blijft een Boolean van één byte die alleen een vlag en een klok leest
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, nooit gederefereerd
  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
  // Draait op de stack van Tesseract: lees vlaggen en de klok, geef nooit een exception
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Gebruik, met de functiepointers resolvbaar via GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Twee details in die schets zijn bewust zo. De callback geeft een Boolean terug, die in zowel Delphi als Free Pascal één byte is, matchend met de C bool in TessCancelFunc. De vier-byte Windows BOOL of Delphi LongBool oogt uitwisselbaar en is dat niet: schrijft de ene kant één byte en leest de andere er vier, dan zijn de bovenste bytes van het retourregister wat er toevallig stond, en kan een false als true aankomen. Dezelfde header maakt het extra ingewikkeld, omdat functies zoals TessPageIteratorBoundingBox een int teruggeven, die HotPDF als Integer declareert. Lees het C-type van elke retourwaarde in plaats van één conventie voor de hele API te veronderstellen

Het tweede detail is dat de callback nooit een exception gooit. Een Delphi-exception die door de C++-frames van Tesseract heen ontvouwt, is ongedefinieerd gedrag, dus de callback van HotPDF leest alleen de cancellation token en een monotone GetTickCount64-waarde. De adapter zet het resultaat na terugkeer van TessBaseAPIRecognize om in een annulerings- of timeoutdiagnostiek, en voert die controle uit ongeacht de native retourcode

Welke native pointers bezit de Delphi-kant?

De HotPDF Tesseract DLL-adapter bezit drie native objecten per verzoek, de API-instantie, de monitor en de resultaatiterator, en leent alles wat verder gaat. Elke Recognize-aanroep maakt zijn eigen set aan en geeft haar vrij in een finally-blok: TessResultIteratorDelete, dan TessMonitorDelete, dan TessBaseAPIDelete. De engine-interface vrijgeven ontlaadt de bibliotheek

HotPDF Tesseract DLL-objecteigendom per Recognize-aanroep: de resultaatiterator, monitor en API-instantie zijn in bezit en worden in die volgorde vrijgegeven binnen finally, de pagina-iterator uit TessResultIteratorGetPageIterator is een geleende blik die nooit mag worden vrijgegeven, en GetUTF8Text-strings worden gekopieerd en teruggegeven via TessDeleteText
drie objecten in bezit, alles daarbuiten geleend: geef vrij in de vaste volgorde, geef de pagina-iterator nooit dubbel vrij, en meng nooit allocators
  • TessResultIteratorGetPageIterator geeft een geleende blik in de resultaatiterator terug, geen nieuw object. HotPDF gebruikt hem voor TessPageIteratorBoundingBox en TessPageIteratorBaseline en geeft hem nooit vrij; hem apart vrijgeven zou hetzelfde geheugen twee keer vrijgeven
  • TessResultIteratorGetUTF8Text geeft een string terug die door de eigen runtime van de DLL is toegewezen. HotPDF kopieert haar en geeft haar terug via TessDeleteText in een finally-blok; Pascal FreeMem zou haar op de verkeerde heap vrijgeven
  • Woordtekst wordt gedecodeerd met strikte UTF-8-validatie en lengtecontrole vóór conversie. Woorden met stuurttekens, misvormde UTF-8, boxes buiten de afbeelding, omgekeerde rechthoeken of confidence buiten 0-100 laten het verzoek falen in plaats van stilletjes te worden geplakt
  • De totale tekst per verzoek is geplafonneerd op 1.048.576 UTF-16 code units, en het woordaantal moet binnen het verzoekbudget blijven dat ApplyLoadedOCRTextLayer doorgeeft

Confidence komt binnen als 0-100 en wordt geschaald naar 0-1, dus THPDFOCRTextLayerOptions.MinimumConfidence betekent hetzelfde voor elke engine. Meldt Tesseract een baseline, dan worden beide eindpunten doorgegeven; anders valt de tekstlaag-pijplijn terug op zijn geometrische schatting, precies zoals bij TSV-invoer

Waarom een enum valideren voordat hij de DLL bereikt?

HotPDF kopieert de rauwe ordinaal van PageSegMode en EngineMode naar een Integer voordat er op bereik wordt gecontroleerd, want een compiler mag aannemen dat een enumvariabele altijd een gedeclareerde waarde bevat en Ord(X) > Ord(High(T)) Invouwen naar een constante false. De ordinalen zijn geen versiering: THPDFTesseractPageSegMode volgt de paginasegmentatienummering van Tesseract van 0 tot 13, THPDFTesseractEngineMode volgt de engine-modusnummering van 0 tot 3, en beide gaan als kale integers naar de DLL. Een optierecord gebouwd met FillChar, gevuld vanuit een stream of vanuit C++Builder met een gecast integer binnenkomend, kan een waarde als 200 bevatten. De gekopieerde ordinaal valideren maakt daar op factory-tijd een EArgumentException van in plaats van een ongedefinieerde modus binnen native code. De factory weigert bovendien tpsOSDOnly en tpsAutoOnly, die geen woorden opleveren, en eist osd.traineddata voor tpsAutoOSD en tpsSparseTextOSD

Wat garandeert de recognition-timeout eigenlijk?

De Tesseract DLL-timeout is coöperatief: HotPDF kan zijn eigen werk stoppen en Tesseract vragen te stoppen, maar hij kan native code niet dwingen terug te keren. De klok begint zodra Recognize begint, dus bitmapconversie en modelinitialisatie verbruiken hetzelfde budget als de herkenning. HotPDF controleert verstreken tijd en de cancellation token tijdens de grijswaardenconversie en tussen woorden door tijdens het itereren van resultaten, en geeft de resterende milliseconden door aan TessMonitorSetDeadlineMSecs vóór de aanroep van TessBaseAPIRecognize

De kloof zit binnen de native aanroep. De monitor van Tesseract wordt geraadpleegd tijdens woordherkenning, niet tijdens TessBaseAPIInit2 of paginalayoutanalyse, dus een trage modellaad of een pathologische layout kan voorbij de deadline lopen voordat de timeout wordt gemeld. Pixel- en uitvoerbudgetten plafonneren ook het eigen geheugengebruik van de native bibliotheek niet. Heeft u een werker nodig die u kunt neerschieten, gebruik dan de procesadapter; dat is de eerlijke afweging, geen ontbrekende functie

HotPDF Tesseract DLL-coöperatieve timeout-anatomie: de klok begint zodra Recognize begint en dekt grijswaardenconversie, TessBaseAPIInit2 en layoutanalyse, maar de monitor wordt alleen geraadpleegd tijdens woordherkenning, dus modelladingen en layout kunnen overschrijden voordat HotPDF otlsEngineError of otlsCancelled meldt
een deadline hier is een verzoek, geen garantie: initialisatie en layoutanalyse kunnen lang doorlopen, en een werker die u echt kunt neerschieten heeft de procesadapter nodig

Paginasegmentatie is waar de DLL-adapter zijn brood verdient op moeilijke invoer. Formulieren, labels en gescande tabellen met verspreide velden herkennen vaak beter met tpsSparseText dan met automatische segmentatie, die kolommen en alinea's probeert samen te stellen die er niet zijn

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;  // verspreide velden, geen kolombouw
  TessOptions.EngineMode := temLSTMOnly;     // heeft LSTM-modellen in tessdata nodig
  TessOptions.TimeoutMilliseconds := 20000;  // omvat modelinitialisatie
  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;

Een timeout komt naar boven als otlsEngineError met de diagnostiek Tesseract DLL OCR timed out, terwijl een geannuleerde token naar boven komt als otlsCancelled. In beide gevallen heeft ApplyLoadedOCRTextLayer elke geselecteerde pagina herkend voordat de commit-transactie begint, dus een falering op pagina 40 van 50 laat het geladen document exact zoals het was. Bedenk dat tpsSingleLine, tpsSingleBlock en tpsSparseText alleen de segmentatie veranderen; geen van hen zet een scheve scan recht

Free Pascal en Lazarus: oude pixels en verdwenen Chinees

Beide Tesseract-factories werken in Windows Free Pascal en Lazarus Win32- en Win64-builds sinds v2.772.1, na twee FPC-specifieke fixes. Bouw het Lazarus-pakket eerst voor de doelarchitectuur; de algemene port wordt behandeld in HotPDF op Free Pascal en Lazarus Win64

De eerste fix gaat over pixels. Een LCL TBitmap weggeschreven via scanlines kan zijn rauwe afbeelding bijwerken zonder de Windows-bitmaphandle te verversen, dus GetDIBits op die handle geeft de oude pixels terug. Het symptoom was duister: tekst die rechtstreeks op een bitmap werd getekend werd herkend, terwijl een pagina gerenderd door de PDF-renderer van HotPDF een lege woordenlijst opleverde. Op FPC leest de adapter nu een formaatbewuste snapshot via CreateIntfImage, die het pixelformaat en de rijvolgorde van de rauwe afbeelding eert. De Delphi-build houdt het GetDIBits-pad op een privé-kopie van 24 bits. Geen van beide builds verandert de bitmap van de aanroeper

De tweede fix hoort bij de tesseract.exe-adapter. De TStringList van FPC bewaart ANSI-strings, dus gedecodeerde UTF-8 TSV-tekst aan Lines.Text toewijzen liet stilletjes elk Chinees of supplementair-vlak-karakter vallen dat de systeem-ANSI-codepagina niet kon representeren. Het FPC-pad houdt de TSV nu als UTF-8-bytes, stript de BOM op byteniveau en decodeert elk woord afzonderlijk naar UnicodeString. De DLL-adapter had dit probleem nooit, omdat hij elk woord rechtstreeks uit de iterator decodet

Snelnaslag

  • Factory: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) in HPDFTesseractRecognition, toegevoegd in v2.772.0, FPC-ondersteuning in v2.772.1
  • Defaults: tpsAuto, temDefault, 60.000 ms, 16.777.216 pixels; timeoutbereik 1-3.600.000 ms, pixelplafond 67.108.864
  • Match de DLL-bitness op de applicatie en leg afhankelijkheids-DLL's naast de Tesseract-DLL
  • Behandel de monitor als opaak; kopieer ETEXT_DESC nooit naar een Pascal-record
  • Declareer de cancel-callback cdecl met een Boolean-resultaat van één byte, en laat nooit een exception haar ontvluchten
  • Geef iteratortekst vrij met TessDeleteText; geef de pagina-iterator uit de resultaatiterator nooit vrij
  • Verwacht dat de deadline coöperatief is: modelinitialisatie en layoutanalyse kunnen haar overschrijden
  • Gebruik de tesseract.exe-adapter wanneer u harde beëindiging of crashisolatie nodig heeft

De Tesseract DLL-adapter, de procesadapters en de ingebouwde OCR-engine worden alle geleverd met de HotPDF Delphi PDF-component voor Delphi, C++Builder en Free Pascal; zie de HotPDF-productpagina voor edities en downloads