Artykuł techniczny

OCR przez DLL Tesseract w HotPDF: wywołania C API z Delphi

HotPDF uruchamia Tesseracta wewnątrz twojego procesu Delphi przez HPDFCreateTesseractDLLOCREngine — fabrykę dodaną w v2.772.0, która dynamicznie ładuje zgodną z Tesseract 5 DLL, obsługuje jej C API (TessBaseAPIInit2, TessBaseAPIRecognize, iterator wyników) i zwraca IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer używa tego engine'a, żeby dodać niewidoczną, przeszukiwalną warstwę tekstu Unicode do zeskanowanych stron PDF

Ten sam rozpoznawacz był już osiągalny przez zewnętrzny adapter tesseract.exe, który pisze BMP i parsuje TSV. Ta ścieżka działa, ale każda strona płaci za odpalenie procesu, tymczasowy plik bitmapy i format tekstu bez linii bazowych i bez kontroli nad segmentacją strony. Wywołanie DLL usuwa wszystkie trzy. Usuwa też ścianę procesu, a to znaczy, że wiązanie Pascal siedzi wprost na strukturach C, booleanach C i napisach alokowanych przez C. Większość tego, co warto wiedzieć o tym adapterze, to miejsca, w których takie wiązanie potrafi po cichu pójść źle

Jak uruchomić Tesseracta w procesie z Delphi przez HotPDF?

Uruchomienie Tesseracta w procesie z HotPDF to jedno wywołanie fabryki w jednostce HPDFTesseractRecognition i to samo wywołanie ApplyLoadedOCRTextLayer, którego używa każdy engine OCR HotPDF. Fabryka waliduje z góry. Plik DLL i katalog tessdata muszą istnieć, identyfikator języka może zawierać tylko litery ASCII, cyfry, _ i +, każdy model w kombinacji takiej jak chi_sim+eng musi mieć pasujący plik .traineddata, a wszystkie 21 wymaganych eksportów musi się rozwiązać, zanim engine zostanie zwrócony. Błędy konfiguracji podnoszą EArgumentException; DLL, która nie chce się załadować, podnosi EOSError z kodem błędu Windows i sugestią sprawdzenia architektury i zależności

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Aplikacja Win64 potrzebuje 64-bitowej DLL; DLL zależności lądują obok
  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
    // Pusta lista stron znaczy każdą stronę; strony mające już tekst są pomijane
    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 ustawia PageSegMode na tpsAuto, EngineMode na temDefault, TimeoutMilliseconds na 60 000 i MaxPixels na 16 777 216. Budżet pikseli ma większe znaczenie, niż wygląda. Strona US Letter przy domyślnych 300 DPI renderuje się do 2550 × 3300 pikseli, około 8,4 miliona, co się mieści. Ta sama strona przy 600 DPI to 5100 × 6600, około 33,7 miliona, i adapter odrzuca ją, zanim Tesseract zobaczy piksel. Podnieś MaxPixels (sufit to 67 108 864) albo zostaw DPI gdzie jest; każdy bok jest dodatkowo ograniczony do 32 767 pikseli

DLL jest ładowana przez LoadLibraryEx z flagami wyszukiwania dla własnego folderu DLL plus domyślnych bezpiecznych katalogów, więc biblioteki obrazów, od których Tesseract zależy, mogą mieszkać obok niego bez ruszania PATH ani katalogu bieżącego. HotPDF nie dołącza ani nie pobiera żadnego runtime'u ani modelu OCR; oba zaopatrujesz sam

Co się zmienia względem adaptera tesseract.exe?

Adapter DLL wymienia izolację procesów na bogatsze wyjście i niższy narzut na stronę. Oba adaptery wpinają się w ten sam potok warstwy tekstu, więc mapowanie współrzędnych, filtrowanie pewności i zatwierdzanie albo wszystko, albo nic są identyczne; różni się to, jak piksele wchodzą i słowa wychodzą

AspektAdapter tesseract.exeAdapter DLL Tesseract
FabrykaHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Wejście pikseliPlik BMP w prywatnym katalogu tymczasowym8-bitowy bufor w skali szarości w pamięci
Wyjście słówTSV per słowo, z sufitem 64 MiBIterator wyników, UTF-8 na słowo
Linie bazoweNiedostępnePrzekazywane z TessPageIteratorBaseline
Segmentacja strony i tryb engineTylko segmentacja automatycznaTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutTwardy: proces potomny jest kończonyKooperatywny: Tesseract musi się zorientować
Izolacja awarii i pamięciOsobny procesBrak, dzieli twoją przestrzeń adresową

Jeden koszt nie znika. Każde wywołanie Recognize tworzy własną instancję API i woła TessBaseAPIInit2, więc modele językowe są inicjalizowane per strona, a nie raz na engine. Plikowy cache systemu operacyjnego łagodzi przeładowanie, ale na dużych zestawach modeli wielojęzycznych to wciąż dominujący stały koszt na stronę i wlicza się do deadline'u rozpoznawania. Engine DLL RapidOCR w procesie przyjmuje odwrotną konstrukcję i trzyma swoje modele ONNX rezydentnie przez życie engine'a; problemy graniczne (ABI C, pożyczone bufory, nieprzerywalna praca natywna) to ta sama rodzina

Dlaczego Delphi nie może skopiować struktury monitora Tesseracta?

Delphi nie może bezpiecznie odzwierciedlić monitora postępu Tesseracta, bo ETEXT_DESC zawiera pola wewnętrzne zależne od wersji, więc rekord przepisany ręcznie kładzie callback anulowania i deadline na złych offsetach na niektórych kompilacjach. Nic nie wybucha głośno, gdy to się stanie. Tesseract po prostu czyta twój wskaźnik callbacka z pola, które trzyma teraz coś innego, albo w ogóle nie widzi deadline'u

HotPDF traktuje więc monitor jako nieprzezroczysty wskaźnik i dotyka go wyłącznie przez wyeksportowane funkcje: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs i TessMonitorDelete. Jeśli wiążesz C API samodzielnie w innym celu, ten sam wzorzec obowiązuje. Szkic poniżej to twój własny kod wiązania, nie API HotPDF, i odzwierciedla deklaracje, których HotPDF używa wewnętrznie

Obsługa monitora DLL Tesseracta w HotPDF: skopiowanie rekordu ETEXT_DESC zależnego od wersji kładzie callback anulowania i deadline na złych offsetach i zawodzi po cichu, podczas gdy HotPDF traktuje monitor jako nieprzezroczysty, obsługuje TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc i TessMonitorSetDeadlineMSecs i trzyma callback cdecl bez wyjątków
nieprzezroczysty wskaźnik plus pięć eksportów to cały kontrakt; callback pozostaje jednobajtowym Boolean, który czyta tylko flagę i zegar
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, nigdy nie dereferencjonowany
  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
  // Biegnie na stosie Tesseracta: czytaj flagi i zegar, nigdy nie podnoś wyjątku
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Użycie, ze wskaźnikami funkcji rozwiązanymi przez GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Dwa szczegóły w tym szkicu są zamierzone. Callback zwraca Boolean, czyli jeden bajt i w Delphi, i w Free Pascal, pasujący do C bool w TessCancelFunc. Czterobajtowe Windows BOOL albo Delphi LongBool wygląda wymiennie i nie jest: gdy jedna strona zapisuje pojedynczy bajt, a druga czyta cztery, górne bajty rejestru zwrotnego zawierają to, co tam zostało, i false może przyjść jako true. Ten sam nagłówek komplikuje sprawę dalej, bo funkcje takie jak TessPageIteratorBoundingBox zwracają int, które HotPDF deklaruje jako Integer. Czytaj typ C każdej wartości zwrotnej, zamiast zakładać jedną konwencję dla całego API

Drugi szczegół: callback nigdy nie podnosi wyjątku. Wyjątek Delphi rozwijający się przez ramki C++ Tesseracta to zachowanie niezdefiniowane, więc callback HotPDF czyta wyłącznie token anulowania i monotoniczną wartość GetTickCount64. Adapter zamienia wynik w diagnostykę anulowania albo timeoutu po powrocie TessBaseAPIRecognize i wykonuje to sprawdzenie niezależnie od natywnego kodu zwrotnego

Które natywne wskaźniki posiada strona Delphi?

Adapter DLL Tesseracta w HotPDF posiada trzy natywne obiekty na żądanie — instancję API, monitor i iterator wyników — a wszystko inne pożycza. Każde wywołanie Recognize tworzy własny zestaw i zwalnia go w bloku finally: TessResultIteratorDelete, potem TessMonitorDelete, potem TessBaseAPIDelete. Zwolnienie interfejsu engine'a wyładowuje bibliotekę

Własność obiektów natywnych DLL Tesseracta w HotPDF na wywołanie Recognize: iterator wyników, monitor i instancja API są posiadane i zwalniane w tej kolejności wewnątrz finally, iterator strony z TessResultIteratorGetPageIterator to pożyczony widok, którego nigdy nie wolno zwalniać, a napisy z GetUTF8Text są kopiowane i oddawane przez TessDeleteText
trzy obiekty posiadane, wszystko inne pożyczone: zwalniaj w stałej kolejności, nigdy nie zwalniaj iteratora strony dwa razy i nigdy nie mieszaj alokatorów
  • TessResultIteratorGetPageIterator zwraca pożyczony widok wewnątrz iteratora wyników, nie nowy obiekt. HotPDF używa go do TessPageIteratorBoundingBox i TessPageIteratorBaseline i nigdy go nie zwalnia; skasowanie go osobno zwolniłoby tę samą pamięć dwa razy
  • TessResultIteratorGetUTF8Text zwraca napis alokowany przez własny runtime DLL. HotPDF go kopiuje i oddaje przez TessDeleteText w bloku finally; Pascalowe FreeMem zwolniłoby go na złej stercie
  • Tekst słowa jest dekodowany ze ścisłą walidacją UTF-8 i sprawdzany na długość przed konwersją. Słowa ze znakami kontrolnymi, zniekształconym UTF-8, boxami poza obrazem, prostokątami odwróconymi albo pewnością poza 0–100 wywalają żądanie, zamiast być po cichu łatanymi
  • Łączny tekst na żądanie ma sufit 1 048 576 jednostek kodowych UTF-16, a liczba słów musi się mieścić w budżecie żądania przekazanym przez ApplyLoadedOCRTextLayer

Pewność przychodzi jako 0–100 i jest skalowana do 0–1, więc THPDFOCRTextLayerOptions.MinimumConfidence znaczy to samo dla każdego engine'a. Gdy Tesseract raportuje linię bazową, oba jej końce są przekazywane; w przeciwnym razie potok warstwy tekstu cofa się do swojego szacunku geometrycznego, dokładnie jak przy wejściu TSV

Dlaczego walidować enum, zanim dotrze do DLL?

HotPDF kopiuje surowy porządek PageSegMode i EngineMode do Integer przed testem zakresu, bo kompilator może założyć, że zmienna enumowa zawsze trzyma zadeklarowaną wartość, i zwinąć Ord(X) > Ord(High(T)) do stałego false. Porządki nie są dekoracją: THPDFTesseractPageSegMode podąża za numeracją segmentacji strony Tesseracta od 0 do 13, THPDFTesseractEngineMode za numeracją trybów engine od 0 do 3, i oba jadą do DLL jako gołe liczby całkowite. Rekord opcji zbudowany przez FillChar, wypełniony ze strumienia albo przekazany z C++Buildera z rzutowaną liczbą może nieść bajt typu 200. Walidacja skopiowanego porządku zamienia to w EArgumentException w czasie budowania fabryki zamiast w niezdefiniowany tryb wewnątrz kodu natywnego. Fabryka odrzuca też tpsOSDOnly i tpsAutoOnly, które nie produkują słów, i wymaga osd.traineddata dla tpsAutoOSD i tpsSparseTextOSD

Co faktycznie gwarantuje timeout rozpoznawania?

Timeout DLL Tesseracta jest kooperatywny: HotPDF może zatrzymać własną pracę i poprosić Tesseracta, żeby stanął, ale nie może zmusić kodu natywnego do powrotu. Zegar startuje, gdy Recognize się zaczyna, więc konwersja bitmapy i inicjalizacja modeli zjadają ten sam budżet co rozpoznawanie. HotPDF sprawdza upływ czasu i token anulowania podczas konwersji do skali szarości i między słowami przy iterowaniu wyników, a pozostałe milisekundy przekazuje do TessMonitorSetDeadlineMSecs przed wywołaniem TessBaseAPIRecognize

Szczelina jest wewnątrz wywołania natywnego. Monitor Tesseracta jest odpytywany podczas rozpoznawania słów, nie podczas TessBaseAPIInit2 ani analizy układu strony, więc powolne ładowanie modeli albo patologiczny układ mogą przebiec za deadline, zanim timeout zostanie zgłoszony. Budżety pikseli i wyjścia też nie ograniczają własnego zużycia pamięci biblioteki natywnej. Jeśli potrzebujesz pracownika, którego można zabić, użyj adaptera procesowego; to uczciwy kompromis, nie brakująca funkcja

Anatomia kooperatywnego timeoutu DLL Tesseracta w HotPDF: zegar startuje, gdy zaczyna się Recognize, i obejmuje konwersję do skali szarości, TessBaseAPIInit2 i analizę układu, ale monitor jest odpytywany tylko podczas rozpoznawania słów, więc ładowanie modeli i układ mogą przekroczyć deadline, zanim HotPDF zgłosi otlsEngineError albo otlsCancelled
deadline to tutaj prośba, nie gwarancja: inicjalizacja i analiza układu mogą biec długo, a pracownik, którego naprawdę można zabić, potrzebuje adaptera procesowego

Segmentacja strony to miejsce, gdzie adapter DLL zarabia na trudnym wejściu. Formularze, etykiety i zeskanowane tabele z rozrzuconymi polami często rozpoznają się lepiej przy tpsSparseText niż przy segmentacji automatycznej, która próbuje składać kolumny i akapity, których tam nie ma

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;  // pola rozrzucone, bez składania kolumn
  TessOptions.EngineMode := temLSTMOnly;     // wymaga modeli LSTM w tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // łącznie z inicjalizacją modeli
  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 wychodzi jako otlsEngineError z diagnostyką Tesseract DLL OCR timed out, a anulowany token jako otlsCancelled. W obu przypadkach ApplyLoadedOCRTextLayer rozpoznało każdą wybraną stronę, zanim zaczęło transakcję zatwierdzania, więc porażka na stronie 40 z 50 zostawia wczytany dokument dokładnie taki, jaki był. Uwaga: tpsSingleLine, tpsSingleBlock i tpsSparseText zmieniają tylko segmentację; żaden z nich nie wyprostuje skrzywionego skanu

Free Pascal i Lazarus: stare piksele i zgubiony chiński

Obie fabryki Tesseracta działają w kompilacjach Windows Free Pascal i Lazarus Win32 oraz Win64 od v2.772.1, po dwóch poprawkach specyficznych dla FPC. Przebuduj najpierw pakiet Lazarusa dla architektury docelowej; ogólny port opisuje HotPDF na Free Pascal i Lazarus Win64

Pierwsza poprawka dotyczy pikseli. LCL-owe TBitmap pisane przez scanline potrafi zaktualizować surowy obraz bez odświeżenia uchwytu bitmapy Windows, więc GetDIBits na tym uchwycie zwraca stare piksele. Objaw był zagadkowy: tekst narysowany wprost na bitmapie był rozpoznawany, a strona wyrenderowana przez renderer PDF HotPDF produkowała pustą listę słów. Na FPC adapter czyta teraz migawkę świadomą formatu przez CreateIntfImage, która respektuje format pikseli i kolejność wierszy surowego obrazu. Kompilacja Delphi trzyma ścieżkę GetDIBits na prywatnej kopii 24-bitowej. Żadna kompilacja nie modyfikuje bitmapy wołającego

Druga poprawka należy do adaptera tesseract.exe. TStringList w FPC trzyma napisy ANSI, więc przypisanie zdekodowanego tekstu TSV UTF-8 do Lines.Text po cichu gubiło każdy chiński znak albo znak z płaszczyzny uzupełniającej, którego systemowa strona kodowa ANSI nie umiała przedstawić. Ścieżka FPC trzyma teraz TSV jako bajty UTF-8, zrzuca BOM na poziomie bajtów i dekoduje każde słowo osobno do UnicodeString. Adapter DLL nigdy tego problemu nie miał, bo dekoduje każde słowo wprost z iteratora

Ściąga

  • Fabryka: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) w HPDFTesseractRecognition, dodana w v2.772.0, wsparcie FPC w v2.772.1
  • Domyślne: tpsAuto, temDefault, 60 000 ms, 16 777 216 pikseli; zakres timeoutu 1–3 600 000 ms, sufit pikseli 67 108 864
  • Dopasuj bitowość DLL do aplikacji i połóż DLL zależności obok DLL Tesseracta
  • Traktuj monitor jako nieprzezroczysty; nigdy nie kopiuj ETEXT_DESC do rekordu Pascal
  • Deklaruj callback anulowania jako cdecl z jednobajtowym wynikiem Boolean i nigdy nie wypuszczaj z niego wyjątku
  • Zwalniaj tekst iteratora przez TessDeleteText; nigdy nie zwalniaj iteratora strony uzyskanego z iteratora wyników
  • Oczekuj, że deadline jest kooperatywny: inicjalizacja modeli i analiza układu mogą go przeskoczyć
  • Używaj adaptera tesseract.exe, gdy potrzebujesz twardego zakończenia albo izolacji awarii

Adapter DLL Tesseracta, adaptery procesowe i wbudowany engine OCR jadą wszystkie z komponentem HotPDF Delphi PDF dla Delphi, C++Buildera i Free Pascal; wydania i pobrania znajdziesz na stronie produktu HotPDF