Article technique

OCR DLL Tesseract HotPDF : appeler l’API C depuis Delphi

HotPDF exécute Tesseract à l’intérieur de votre processus Delphi via HPDFCreateTesseractDLLOCREngine, une factory ajoutée en v2.772.0 qui charge dynamiquement une DLL compatible Tesseract 5, pilote son API C (TessBaseAPIInit2, TessBaseAPIRecognize, l’itérateur de résultats) et renvoie un IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer utilise ce moteur pour ajouter une couche de texte Unicode invisible et cherchable aux pages PDF scannées

Le même reconnaisseur était déjà accessible par l’adaptateur tesseract.exe externe qui écrit un BMP et analyse du TSV. Ce chemin marche, mais chaque page paie un lancement de processus, un fichier bitmap temporaire et un format de texte sans baselines ni contrôle de la segmentation de page. Appeler la DLL supprime les trois. Elle supprime aussi le mur du processus, ce qui veut dire qu’une liaison Pascal se pose directement sur des structures C, des booléens C et des chaînes allouées en C. L’essentiel de ce qui vaut d’être su sur cet adaptateur, ce sont les endroits où cette liaison peut se tromper en silence

Comment exécuter Tesseract en process depuis Delphi avec HotPDF ?

Exécuter Tesseract en process avec HotPDF tient en un appel de factory dans l’unité HPDFTesseractRecognition et le même appel ApplyLoadedOCRTextLayer qu’utilise tout moteur OCR HotPDF. La factory valide tôt. Le fichier DLL et le dossier tessdata doivent exister, l’identifiant de langue ne peut contenir que des lettres ASCII, des chiffres, _ et +, chaque modèle d’une combinaison telle que chi_sim+eng doit avoir un fichier .traineddata correspondant, et les 21 exports requis doivent tous se résoudre avant que le moteur ne soit renvoyé. Les erreurs de configuration lèvent EArgumentException ; une DLL qui échoue au chargement lève EOSError avec le code d’erreur Windows et une astuce de vérifier architecture et dépendances

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Une application Win64 exige une DLL 64 bits ; les DLL de dépendances à côté
  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
    // Une liste de pages vide veut dire toutes les pages ; pages avec texte déjà sautées
    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 règle PageSegMode sur tpsAuto, EngineMode sur temDefault, TimeoutMilliseconds sur 60 000 et MaxPixels sur 16 777 216. Le budget de pixels compte plus qu’il n’y paraît. Une page US Letter aux 300 DPI par défaut se rend en 2 550 × 3 300 pixels, environ 8,4 millions, ce qui tient. La même page à 600 DPI fait 5 100 × 6 600, environ 33,7 millions, et l’adaptateur la refuse avant que Tesseract ne voie un pixel. Montez MaxPixels (le plafond est 67 108 864) ou laissez le DPI où il est ; chaque côté est en plus plafonné à 32 767 pixels

La DLL est chargée avec LoadLibraryEx et les drapeaux de recherche du propre dossier de la DLL plus les répertoires sûrs par défaut, si bien que les bibliothèques d’images dont Tesseract dépend peuvent vivre à côté sans toucher au PATH ni au répertoire courant. HotPDF n’embarque ni ne télécharge aucun runtime ou modèle OCR ; vous provisionnez les deux

Qu’est-ce qui change par rapport à l’adaptateur tesseract.exe ?

L’adaptateur DLL échange l’isolation de processus contre une sortie plus riche et un coût par page plus bas. Les deux adaptateurs se branchent sur le même pipeline de couche de texte, donc la cartographie de coordonnées, le filtrage par confiance et le commit tout-ou-rien sont identiques ; ce qui diffère, c’est la façon dont les pixels entrent et les mots sortent

AspectAdaptateur tesseract.exeAdaptateur DLL Tesseract
FactoryHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Pixels en entréeFichier BMP dans un répertoire temporaire privéTampon en niveaux de gris 8 bits en mémoire
Mots en sortieTSV au niveau du mot, plafonné à 64 MioItérateur de résultats, UTF-8 par mot
BaselinesIndisponiblesTransmises depuis TessPageIteratorBaseline
Segmentation de page et mode moteurSegmentation automatique seulementTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutDur : le processus enfant est terminéCoopératif : Tesseract doit s’en apercevoir
Isolation de crash et mémoireProcessus séparéAucune, partage votre espace d’adressage

Un coût ne disparaît pas. Chaque appel Recognize crée sa propre instance d’API et appelle TessBaseAPIInit2, si bien que les modèles de langue sont initialisés par page plutôt qu’une fois par moteur. Le cache de fichiers du système adoucit le rechargement, mais sur de grands ensembles de modèles multilingues ça reste le coût fixe dominant par page, et ça compte contre l’échéance de reconnaissance. Le moteur DLL RapidOCR en process prend le design opposé et garde ses modèles ONNX résidents pour la durée de vie du moteur ; les problèmes de frontière (ABI C, tampons empruntés, travail natif ininterruptible) sont de la même famille

Pourquoi Delphi ne peut-il pas copier la structure moniteur de Tesseract ?

Delphi ne peut pas refléter en toute sécurité le moniteur de progression de Tesseract parce que ETEXT_DESC contient des champs internes dépendants de la version, si bien qu’un record copié à la main place le callback d’annulation et l’échéance aux mauvais offsets sur certains builds. Rien n’échoue bruyamment quand ça arrive. Tesseract lit simplement votre pointeur de callback dans un champ qui contient désormais autre chose, ou ne voit jamais l’échéance du tout

HotPDF traite donc le moniteur comme un pointeur opaque et n’y touche qu’à travers des fonctions exportées : TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs et TessMonitorDelete. Si vous liez l’API C vous-même pour un autre usage, le même motif s’applique. Le croquis ci-dessous est votre propre code de liaison, pas une API HotPDF, et reflète les déclarations que HotPDF utilise en interne

Gestion du moniteur DLL Tesseract dans HotPDF : copier le record ETEXT_DESC dépendant de la version place le callback d’annulation et l’échéance aux mauvais offsets et échoue en silence, tandis que HotPDF traite le moniteur comme opaque, pilote TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc et TessMonitorSetDeadlineMSecs, et garde le callback cdecl sans exception
un pointeur opaque plus cinq exports, voilà tout le contrat ; le callback reste un Boolean d’un octet qui ne lit qu’un drapeau et une horloge
type
  // C : typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, jamais déréférencé
  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
  // Tourne sur la pile de Tesseract : lisez drapeaux et horloge, ne levez jamais
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Usage, avec les pointeurs de fonctions résolus par GetProcAddress :
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Deux détails de ce croquis sont délibérés. Le callback renvoie un Boolean, qui fait un octet en Delphi comme en Free Pascal, à l’image du bool C de TessCancelFunc. Le BOOL Windows à quatre octets ou le LongBool Delphi paraissent interchangeables et ne le sont pas : quand un côté écrit un seul octet et que l’autre en lit quatre, les octets hauts du registre de retour valent ce qui y restait, et un false peut arriver en true. Le même en-tête complique encore les choses, parce que des fonctions comme TessPageIteratorBoundingBox renvoient un int, que HotPDF déclare en Integer. Lisez le type C de chaque valeur de retour au lieu de supposer une convention unique pour toute l’API

Le second détail est que le callback ne lève jamais. Une exception Delphi se déroulant à travers les frames C++ de Tesseract est un comportement indéfini, si bien que le callback de HotPDF ne lit que le token d’annulation et une valeur GetTickCount64 monotone. L’adaptateur transforme le résultat en diagnostic d’annulation ou de timeout après le retour de TessBaseAPIRecognize, et il fait ce contrôle quel que soit le code de retour natif

Quels pointeurs natifs le côté Delphi possède-t-il ?

L’adaptateur DLL Tesseract de HotPDF possède trois objets natifs par requête, l’instance d’API, le moniteur et l’itérateur de résultats, et emprunte tout le reste. Chaque appel Recognize crée son propre jeu et le libère dans un bloc finally : TessResultIteratorDelete, puis TessMonitorDelete, puis TessBaseAPIDelete. Libérer l’interface du moteur décharge la bibliothèque

Possession des objets DLL Tesseract dans HotPDF par appel Recognize : l’itérateur de résultats, le moniteur et l’instance d’API sont possédés et libérés dans cet ordre dans un finally, l’itérateur de page issu de TessResultIteratorGetPageIterator est une vue empruntée à ne jamais libérer, et les chaînes GetUTF8Text sont copiées puis rendues via TessDeleteText
trois objets possédés, tout le reste emprunté : libérez dans l’ordre fixé, ne libérez jamais deux fois l’itérateur de page, et ne mélangez jamais les allocateurs
  • TessResultIteratorGetPageIterator renvoie une vue empruntée dans l’itérateur de résultats, pas un nouvel objet. HotPDF s’en sert pour TessPageIteratorBoundingBox et TessPageIteratorBaseline et ne la libère jamais ; la supprimer séparément libérerait deux fois la même mémoire
  • TessResultIteratorGetUTF8Text renvoie une chaîne allouée par le runtime propre de la DLL. HotPDF la copie et la rend via TessDeleteText dans un bloc finally ; un FreeMem Pascal la libérerait sur le mauvais tas
  • Le texte des mots est décodé avec une validation UTF-8 stricte et contrôlé en longueur avant conversion. Les mots avec caractères de contrôle, UTF-8 mal formé, boîtes hors de l’image, rectangles inversés ou confiance hors de 0–100 font échouer la requête au lieu d’être rafistolés en silence
  • Le texte total par requête est plafonné à 1 048 576 unités de code UTF-16, et le compte de mots doit tenir dans le budget de requête transmis par ApplyLoadedOCRTextLayer

La confiance arrive en 0–100 et est ramenée à 0–1, si bien que THPDFOCRTextLayerOptions.MinimumConfidence veut dire la même chose pour chaque moteur. Quand Tesseract rapporte une baseline, les deux extrémités sont transmises ; sinon le pipeline de couche de texte retombe sur son estimation géométrique, exactement comme pour une entrée TSV

Pourquoi valider un enum avant qu’il atteigne la DLL ?

HotPDF copie l’ordinal brut de PageSegMode et EngineMode dans un Integer avant le contrôle de bornes, parce qu’un compilateur peut supposer qu’une variable enum détient toujours une valeur déclarée et replier Ord(X) > Ord(High(T)) en un constant false. Les ordinaux ne sont pas décoratifs : THPDFTesseractPageSegMode suit la numérotation de segmentation de page de Tesseract de 0 à 13, THPDFTesseractEngineMode suit la numérotation des modes moteur de 0 à 3, et les deux vont à la DLL comme de simples entiers. Un record d’options construit avec FillChar, rempli depuis un flux, ou passé depuis C++Builder avec un entier casté peut porter un octet comme 200. Valider l’ordinal copié transforme ça en EArgumentException au moment de la factory au lieu d’un mode indéfini à l’intérieur du code natif. La factory rejette aussi tpsOSDOnly et tpsAutoOnly, qui ne produisent aucun mot, et exige osd.traineddata pour tpsAutoOSD et tpsSparseTextOSD

Que garantit réellement le timeout de reconnaissance ?

Le timeout de la DLL Tesseract est coopératif : HotPDF peut arrêter son propre travail et demander à Tesseract de s’arrêter, mais il ne peut pas forcer le code natif à revenir. L’horloge démarre quand Recognize commence, si bien que la conversion du bitmap et l’initialisation des modèles consomment le même budget que la reconnaissance. HotPDF vérifie le temps écoulé et le token d’annulation pendant la conversion en niveaux de gris et entre les mots pendant le parcours des résultats, et passe les millisecondes restantes à TessMonitorSetDeadlineMSecs avant d’appeler TessBaseAPIRecognize

La brèche est à l’intérieur de l’appel natif. Le moniteur de Tesseract est consulté pendant la reconnaissance de mots, pas pendant TessBaseAPIInit2 ni l’analyse de mise en page, si bien qu’un chargement de modèle lent ou une mise en page pathologique peut dépasser l’échéance avant que le timeout ne soit rapporté. Les budgets de pixels et de sortie ne plafonnent pas non plus la propre consommation mémoire de la bibliothèque native. Si vous avez besoin d’un worker que vous pouvez tuer, utilisez l’adaptateur process ; c’est le compromis honnête, pas une fonctionnalité manquante

Anatomie du timeout coopératif de la DLL Tesseract dans HotPDF : l’horloge démarre au début de Recognize et couvre la conversion en niveaux de gris, TessBaseAPIInit2 et l’analyse de mise en page, mais le moniteur n’est consulté que pendant la reconnaissance de mots, si bien que les chargements de modèles et la mise en page peuvent dépasser avant que HotPDF ne rapporte otlsEngineError ou otlsCancelled
une échéance ici est une demande, pas une garantie : init et analyse de mise en page peuvent durer, et un worker vraiment tuables exige l’adaptateur process

La segmentation de page est l’endroit où l’adaptateur DLL se rentabilise sur les entrées difficiles. Les formulaires, étiquettes et tableaux scannés aux champs éparpillés se reconnaissent souvent mieux avec tpsSparseText qu’avec la segmentation automatique, qui essaie d’assembler des colonnes et des paragraphes qui n’existent pas

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;  // champs éparpillés, pas d’assemblage de colonnes
  TessOptions.EngineMode := temLSTMOnly;     // exige les modèles LSTM dans tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // inclut l’initialisation des modèles
  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;

Un timeout se manifeste en otlsEngineError avec le diagnostic Tesseract DLL OCR timed out, tandis qu’un token annulé se manifeste en otlsCancelled. Dans les deux cas, ApplyLoadedOCRTextLayer a reconnu chaque page sélectionnée avant de démarrer la transaction de commit, si bien qu’un échec à la page 40 sur 50 laisse le document chargé exactement comme il était. Notez que tpsSingleLine, tpsSingleBlock et tpsSparseText ne changent que la segmentation ; aucun ne redresse un scan penché

Free Pascal et Lazarus : pixels périmés et chinois perdu

Les deux factories Tesseract marchent dans les builds Windows Free Pascal et Lazarus Win32 et Win64 depuis v2.772.1, après deux corrections propres à FPC. Rebuildez d’abord le paquet Lazarus pour l’architecture cible ; le portage général est traité dans HotPDF sous Free Pascal et Lazarus Win64

La première correction concerne les pixels. Un TBitmap LCL écrit par scanlines peut mettre à jour son image brute sans rafraîchir le handle de bitmap Windows, si bien que GetDIBits sur ce handle renvoie les anciens pixels. Le symptôme était déroutant : du texte dessiné directement sur un bitmap était reconnu, tandis qu’une page rendue par le moteur PDF de HotPDF produisait une liste de mots vide. Côté FPC, l’adaptateur lit désormais un instantané conscient du format via CreateIntfImage, qui respecte le format de pixels et l’ordre des lignes de l’image brute. Le build Delphi garde le chemin GetDIBits sur une copie privée 24 bits. Aucun des deux builds ne modifie le bitmap de l’appelant

La seconde correction appartient à l’adaptateur tesseract.exe. Le TStringList de FPC stocke des chaînes ANSI, si bien qu’affecter du texte TSV UTF-8 décodé à Lines.Text abandonnait en silence chaque caractère chinois ou hors plan multilingue que la page de code ANSI du système ne pouvait pas représenter. Le chemin FPC garde désormais le TSV en octets UTF-8, retire le BOM au niveau octet et décode chaque mot en UnicodeString individuellement. L’adaptateur DLL n’a jamais eu ce problème parce qu’il décode chaque mot directement depuis l’itérateur

Aide-mémoire

  • Factory : HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) dans HPDFTesseractRecognition, ajoutée en v2.772.0, support FPC en v2.772.1
  • Défauts : tpsAuto, temDefault, 60 000 ms, 16 777 216 pixels ; plage de timeout 1–3 600 000 ms, plafond de pixels 67 108 864
  • Faites correspondre la bits de la DLL à l’application et placez les DLL de dépendances à côté de la DLL Tesseract
  • Traitez le moniteur comme opaque ; ne copiez jamais ETEXT_DESC dans un record Pascal
  • Déclarez le callback d’annulation cdecl avec un résultat Boolean d’un octet, et ne laissez jamais une exception s’en échapper
  • Libérez le texte de l’itérateur avec TessDeleteText ; ne libérez jamais l’itérateur de page obtenu depuis l’itérateur de résultats
  • Attendez-vous à une échéance coopérative : initialisation des modèles et analyse de mise en page peuvent la dépasser
  • Utilisez l’adaptateur tesseract.exe quand vous avez besoin d’une terminaison dure ou d’une isolation de crash

L’adaptateur DLL Tesseract, les adaptateurs process et le moteur OCR intégré se livrent tous avec le composant HotPDF Delphi PDF pour Delphi, C++Builder et Free Pascal ; voyez la page produit HotPDF pour les éditions et les téléchargements