Article technique

OCR Tesseract vers PDF consultable en Delphi avec HotPDF

HotPDF transforme des pages PDF numérisées en PDF consultable avec Tesseract via HPDFCreateTesseractOCREngine, une fabrique qui enveloppe un exécutable Tesseract installé localement en tant que IHPDFOCREngine. Vous passez ce moteur à ApplyLoadedOCRTextLayer, qui rend chaque page, lance Tesseract une fois par page, analyse sa sortie TSV au niveau des mots, et engage une couche de texte Unicode invisible pour toutes les pages demandées en une seule transaction, ou pour aucune

Pipeline OCR HotPDF par page : rendre la page au DPI configuré, sauvegarder input.bmp dans un répertoire privé HotPDF-OCR, lancer le processus enfant Tesseract avec tessedit_create_tsv, analyser le TSV à douze colonnes, filtrer les mots par confiance, et engager la couche de texte invisible pour toutes les pages demandées ou aucune
L'adaptateur ne remplace que la reconnaissance : rendu, analyse, validation et engagement tout-ou-rien restent dans le pipeline de couche de texte existant, donc le code en aval ne change jamais

La raison d'être de cet adaptateur, c'est le périmètre. Le moteur OCR intégré à appariement de gabarits est délibérément étroit : lettres et chiffres ASCII imprimés par machine, rien d'autre. Des factures avec des noms accentués, des contrats en chinois et des archives multilingues ont besoin d'un vrai recogniseur avec des modèles de langue entraînés, et Tesseract est le candidat évident parce que c'est un programme en ligne de commande que vous pouvez provisionner à côté de votre application. Appeler un programme externe depuis une bibliothèque documentaire paraît trivial. Ça ne l'est pas, et l'essentiel du code intéressant de l'adaptateur porte sur ce qui arrive quand le programme se conduit mal, se bloque, se fait annuler, ou hérite de choses qu'il ne devrait jamais voir

Comment HotPDF pilote-t-il Tesseract depuis une application Delphi ?

HotPDF lance Tesseract comme processus enfant caché par page, en lui donnant un bitmap rendu et en relisant un fichier TSV, et expose le résultat par la même couture IHPDFOCREngine qu'utilise le moteur intégré. Rien ne change en aval : cartographie des coordonnées, gestion de rotation, validation Unicode, filtrage par confiance et engagement atomique, c'est le pipeline de couche de texte que vous avez déjà. La fabrique vit dans l'unité HPDFTesseractRecognition et valide avec empressement : l'exécutable doit exister, le répertoire tessdata doit exister, le timeout doit être entre 1 et 3 600 000 millisecondes, et l'identifiant de langue ne peut contenir que des lettres ASCII, des chiffres, _ et +. Ce dernier contrôle compte parce que la chaîne de langue finit sur une ligne de commande, et eng+chi_sim est une valeur Tesseract légitime tandis que tout ce qui porte guillemets ou espaces n'en est pas

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // lève EArgumentException pour un exécutable manquant, un tessdata manquant,
  // un identifiant de langue faux, ou un timeout hors 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // plusieurs modèles joints par '+'
    120000);            // limite par page, le défaut est 60000
  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
    Options.CancellationToken := Token;
    // une liste de pages vide vaut toutes les pages ; les pages avec du texte sont sautées par défaut
    if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
    begin
      Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
        ' words accepted, ', Info.DroppedWordCount, ' dropped');
      Doc.SaveLoadedDocument(TargetFile);
    end
    else
      case Info.Status of
        otlsCancelled:      Writeln('Cancelled, document unchanged');
        otlsEngineError:    Writeln('Engine: ', string(Info.Diagnostic));
        otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
      else
        Writeln(string(Info.Diagnostic));
      end;
  finally
    Doc.Free;
  end;
end;

Pour chaque page, Recognize crée un répertoire privé sous le chemin temporaire nommé HotPDF-OCR-{GUID}, sauvegarde le bitmap rendu comme input.bmp, et lance tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, avec chaque argument de chemin entre guillemets selon les règles d'échappement de ligne de commande Windows pour les antislashs et les guillemets embarqués. La valeur --dpi est le DPI de rendu de THPDFOCRTextLayerOptions.DPI, donc Tesseract n'a jamais à deviner la résolution depuis les métadonnées de l'image, et --psm 3 demande une segmentation de page entièrement automatique. Le moteur se déclare Tesseract (local CLI), ce qui atterrit dans Info.EngineName. Tesseract et ses modèles de langue ne sont pas livrés avec HotPDF ; les installer est le travail de l'application

Pourquoi l'analyseur TSV est-il si strict ?

L'analyseur TSV de HotPDF fait échouer toute la page à la première rangée mal formée, parce qu'une liste de mots partiellement analysée produit une couche de texte qui contredit l'image en silence. La sortie TSV de Tesseract a un en-tête fixe de douze colonnes, de level à text, et HotPDF compare la première ligne à cet en-tête exact après avoir retiré une éventuelle marque d'ordre des octets. Chaque rangée suivante doit se découper en exactement douze champs, et la découpe s'arrête après la onzième tabulation pour qu'une tabulation dans le texte reconnu reste partie du mot au lieu de créer une treizième colonne. Seules les rangées de niveau 5 sont des mots ; les niveaux 1 à 4 décrivent pages, blocs, paragraphes et lignes, et ils sont sautés. Les rangées de niveau 5 dont le texte est vide ou tout blanc sont sautées aussi, parce qu'un mot blanc a une boîte mais rien à localiser ni chercher. Tout le reste est vérifié durement : géométrie entière, une confiance analysée avec un format invariant en-US pour qu'une locale allemande ne lise pas 93.5 comme des ordures, une boîte entièrement à l'intérieur du bitmap, et une confiance entre 0 et 100. Un seul échec lève, le moteur renvoie False, et le tableau de mots est vidé. Les tests de régression incluent exactement ce cas : un mot valide suivi d'une rangée cassée doit donner zéro mot, pas un

Six portes que passe chaque rangée TSV de Tesseract dans HotPDF : en-tête exact de douze colonnes, exactement douze champs, niveau 5 seulement, texte non vide, une boîte à l'intérieur du bitmap, et une confiance de 0 à 100 analysée de façon invariante, où une seule rangée cassée fait échouer toute la page jusqu'à zéro mot
Une liste de mots partiellement analysée contredirait l'image en silence, donc l'analyseur refuse la page entière à la première rangée mal formée au lieu de garder les mots déjà lus
// condensé depuis la boucle de niveau 5 dans HPDFLocalTSVRecognition
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
  raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue;                 // rangées page/bloc/paragraphe/ligne
WordText := Fields[11];
if Trim(WordText) = '' then Continue;        // les mots tout blancs n'ont pas de position
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
  not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
  not TryStrToFloat(Fields[10], Confidence, Settings) then
  raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
  (Int64(X) + W > Request.Bitmap.Width) or
  (Int64(Y) + H > Request.Bitmap.Height) or
  not ((Confidence >= 0) and (Confidence <= 100)) then
  raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100;  // le pipeline attend 0..1

Cette dernière ligne interagit avec un défaut auquel vous ne vous attendiez peut-être pas. La confiance Tesseract court de 0 à 100, le pipeline travaille en 0 à 1, et THPDFOCRTextLayerOptions.MinimumConfidence vaut 0.5 par défaut, donc tout mot Tesseract sous 50 est compté dans Info.DroppedWordCount et n'atteint jamais la page. Sur un scan propre à 300 DPI, c'est un plancher raisonnable. Sur un fax bruité, cela peut éjecter une part surprenante de la page, et le bon geste est de regarder le compte d'éjectés avant d'abaisser le seuil, parce que les mots à faible confiance sont exactement ceux qui ont le plus de chances d'être faux

Que hérite le processus enfant Tesseract ?

Le processus enfant Tesseract hérite d'exactement deux handles de HotPDF : un handle NUL pour l'entrée et la sortie standard, et un handle de fichier pour l'erreur standard. Cette précision est le point. CreateProcess avec bInheritHandles = True, c'est comment on passe des handles standards à un enfant, mais à lui seul il passe chaque handle héritable du processus hôte, fichiers, pipes et événements ouverts par du code sans rapport dans votre application compris. L'enfant garde alors ces objets en vie jusqu'à sa sortie, si bien qu'un fichier reste verrouillé ou qu'un pipe ne voit jamais sa fin pendant que Tesseract broie une page. HotPDF comble ce trou avec un enregistrement de démarrage étendu : STARTUPINFOEX, une liste d'attributs portant PROC_THREAD_ATTRIBUTE_HANDLE_LIST, et le drapeau de création EXTENDED_STARTUPINFO_PRESENT. Avec la liste de handles en place, bInheritHandles doit toujours être True, mais seuls les handles listés franchissent la frontière. La même pensée de confinement anime l'isolation des codecs d'images PDF dans des processus ouvriers, où l'enfant est du code non fiable ; ici l'enfant est fiable, mais l'hôte n'est pas le seul propriétaire de sa propre table de handles

Héritage de handles du processus enfant Tesseract dans HotPDF : un CreateProcess simple avec bInheritHandles passe à l'enfant chaque handle de fichier, pipe et événement héritable, tandis que STARTUPINFOEX avec PROC_THREAD_ATTRIBUTE_HANDLE_LIST limite l'ensemble à un handle NUL pour stdin et stdout plus le handle de fichier stderr
Sans la liste d'attributs, l'enfant garde des objets sans rapport en vie jusqu'à sa sortie, verrouillant des fichiers et affamant des pipes ; avec elle, seuls les deux handles listés franchissent la frontière
// constantes montrées par nom ; la source passe leurs valeurs numériques
// les deux handles sont créés avec bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin et stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt dans le répertoire privé
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
  PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
  @InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
  True,                                        // exigé par la liste de handles
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Pourquoi une session OCR annulée peut-elle ressembler à un échec de moteur ?

Une session OCR annulée ressemble à un échec de moteur parce que IHPDFOCREngine.Recognize renvoie un unique booléen, et que False signifie à la fois « Tesseract a échoué » et « l'utilisateur a appuyé sur Annuler ». L'adaptateur sonde le jeton d'annulation et le timeout toutes les 25 millisecondes pendant que l'enfant tourne, et quand le jeton se déclenche, il lève dans Recognize, attrape sa propre exception, nettoie, et renvoie False avec un diagnostic. Si le pipeline traitait cela comme une erreur moteur, l'appelant verrait otlsEngineError pour un travail que l'utilisateur a délibérément arrêté. ApplyLoadedOCRTextLayer vérifie donc le jeton d'abord chaque fois que Recognize renvoie False, et ne convertit le résultat en échec moteur que si le jeton n'était pas armé. Cet ordre préserve le contrat multi-pages : reconnaissance, validation, comptabilité de budget et construction de contenu tournent pour chaque page demandée avant que la transaction du graphe ne s'ouvre, si bien qu'une annulation à la page 40 sur 50 rapporte otlsCancelled et laisse le document, premières 39 pages comprises, intact. Il n'y a pas de fichier à moitié consultable à expliquer plus tard, et le reste du traitement d'échecs suit le même style borné :

  • Le timeout est par appel Recognize, mesuré depuis son début, donc le défaut de 60 000 ms s'applique à chaque page plutôt qu'à tout le document
  • Un enfant encore en vie au timeout ou à l'annulation est terminé, attendu jusqu'à 5 secondes, et son répertoire privé est supprimé dans un bloc finally
  • output.tsv est plafonné à 64 Mio et stderr.txt à 1 Mio, vérifié pendant que l'enfant tourne comme après sa sortie
  • Le compte de mots et les unités de code UTF-16 sont plafonnés par page par les budgets restants MaxWordsPerPage, MaxTotalWords et MaxTextCodeUnits, et les dépasser fait échouer la session au lieu de tronquer la liste de mots
  • La sortie standard va vers NUL parce que Tesseract écrit output.tsv, tandis que l'erreur standard va vers un fichier pour qu'un code de sortie non nul soit rapporté avec jusqu'à 4 096 caractères de la plainte propre du moteur, en général le plus rapide moyen d'apprendre qu'un fichier .traineddata manque

Comment les mots reconnus deviennent une couche de texte invisible

HotPDF écrit les mots Tesseract en texte invisible avec le mode de rendu de texte 3, le mode ni-remplissage-ni-tracé défini en ISO 32000-1 §9.3.6, si bien que la page montre toujours l'image numérisée pendant que recherche et copie travaillent sur les mots reconnus. Le flux de contenu ouvre BT avec 3 Tr, et chaque mot reçoit une matrice Tm à sa ligne de base, une taille de police dérivée de la hauteur de la boîte en pixels au DPI de rendu, et une échelle horizontale Tz qui étire la course de glyphes à la largeur mesurée de la boîte, et c'est pourquoi un surlignage de recherche atterrit sur le mot dans l'image au lieu de dériver au travers

Le TSV de Tesseract a des boîtes mais pas de lignes de base, donc l'adaptateur rapporte chaque mot sans ligne de base et le pipeline estime la ligne de base à un cinquième de la hauteur de la boîte au-dessus du bord inférieur. Le texte lui-même passe par une police Type0 partagée non embarquée avec encodage Identity-H et une CMap ToUnicode générée, un CID par scalaire Unicode distinct sur toute la course, et c'est ainsi que chinois, latin accentué et caractères du plan complémentaire survivent tous à la copie et à la recherche. Cette conception a deux limites à énoncer d'emblée : une course peut porter au plus 65 535 scalaires distincts, et la police non embarquée ne satisfait pas l'exigence d'embarquement de police d'ISO 19005, donc une sortie PDF/A demande une police conforme embarquée séparément. Vérifier le résultat est simple et mérite d'être automatisé : sauvegardez, rechargez, et faites tourner le chemin texte ordinaire de document chargé de l'extraction de texte d'un PDF chargé en Delphi ; si les mots reviennent dans les pages attendues, la couche est réelle

RapidOCR et autres moteurs sur le même protocole TSV

HotPDF réutilise le même lanceur de processus et le même analyseur TSV pour RapidOCR via HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), le choix le plus utile pour des scans en chinois simplifié. La ligne de commande est identique, sauf que le chemin du script pont est inséré après l'exécutable Python, et la langue est fixée à chi_sim. HotPDF livre le pont en tools/OCR/rapidocr_tsv.py ; il attend les paquets rapidocr et onnxruntime plus trois modèles ONNX locaux, désactive le téléchargement automatique de modèles, et écrit du TSV en forme Tesseract pour que le côté Delphi n'ait pas besoin d'un second analyseur. Le nom de moteur rapporté dans Info.EngineName est RapidOCR (local ONNX). Cette forme suggère la recette générale : tout recogniseur que vous pouvez envelopper dans un petit script qui accepte la liste d'arguments à la Tesseract et émet le TSV à douze colonnes hérite gratuitement de l'isolation de handles, du timeout, de l'annulation, des budgets de sortie et de l'engagement tout-ou-rien. Les adaptateurs sont Windows seulement, tournent une page à la fois en synchrone, et ne redressent ni ne prétraitent l'image au-delà de ce que le moteur de rendu produit, si bien que la qualité d'image en entrée fixe toujours le plafond de ce qui sort

Les adaptateurs Tesseract et RapidOCR, l'écrivain de couche de texte invisible, le moteur de rendu de pages qui les alimente, et l'extraction de texte qui vérifie le résultat sont tous livrés dans le même composant VCL natif pour Delphi et C++Builder. Si vous ajoutez de l'OCR à une application de capture documentaire ou d'archivage, le composant PDF HotPDF pour Delphi vous donne le pipeline, ne restant que le moteur OCR lui-même à installer