Article technique

RapidOCR in-process HotPDF : OCR DLL native en Delphi

HotPDF rend cherchables les pages PDF scannées avec RapidOCR en process via HPDFCreateRapidOCRDLLOCREngine, une factory ajoutée en v2.774.0 qui charge HotPDFRapidOCR.dll, garde résidents en mémoire les modèles ONNX de détection, de classification d’angle et de reconnaissance, et renvoie un IHPDFOCREngine. Vous passez ce moteur à THotPDF.ApplyLoadedOCRTextLayer, qui rend chaque page, exécute l’inférence CPU sans Python ni processus enfant, et commit une couche de texte Unicode invisible

La motivation, c’est le coût par page. L’adaptateur process RapidOCR livré plus tôt, HPDFCreateRapidOCREngine, démarre un worker Python pour chaque appel Recognize, et ce worker importe son runtime et charge ses modèles ONNX avant de lire le moindre pixel. Sur une archive de 500 pages, cette taxe de démarrage se répète 500 fois, et le déploiement signifie expédier un environnement Python à côté d’un exécutable Delphi. La DLL native charge les modèles une fois, quand vous créez le moteur, et le déploiement se réduit à la DLL, ses fichiers de modèles, et un dictionnaire de caractères. Ce qu’on abandonne en échange, c’est la possibilité de tuer un moteur de reconnaissance bloqué, et l’essentiel de l’ingénierie de cet adaptateur consiste à vivre avec ça honnêtement

Comment rendre cherchable un PDF scanné avec la DLL RapidOCR ?

Produire un PDF cherchable avec la DLL RapidOCR native tient en un appel de factory et le même appel ApplyLoadedOCRTextLayer qu’utilise tout moteur OCR HotPDF. La factory vit dans l’unité HPDFRapidOCRRecognition et valide tôt : la DLL et le dossier de modèles doivent exister, chaque fichier de modèle et de dictionnaire doit se résoudre, la version d’ABI doit être 1, et tous les exports requis doivent être présents avant qu’un modèle ne soit initialisé. Les erreurs de configuration lèvent EArgumentException ; un modèle qui échoue au chargement lève EInvalidOperation portant le texte de diagnostic écrit par la DLL

Séquence de validation de la factory DLL RapidOCR de HotPDF pour HPDFCreateRapidOCRDLLOCREngine : les chemins et fichiers de modèles doivent exister, HPDFRapidOCRAbiVersion doit renvoyer 1, les exports requis doivent se résoudre, et HPDFRapidOCRCreate doit initialiser les modèles, avec EArgumentException ou EInvalidOperation levées tôt avant tout lancement de reconnaissance, la seconde portant le texte de diagnostic natif
la validation est précoce à dessein : les problèmes de configuration lèvent avant toute initialisation de modèle, donc un mauvais chemin ou une mauvaise ABI n’atteint jamais une échéance de reconnaissance
uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Les modèles se chargent ici, hors de toute échéance de reconnaissance.
  // Les noms de modèles relatifs de THPDFRapidOCRDLLOptions.Default se
  // résolvent par rapport au dossier de modèles.
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models');
  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
    // liste de pages vide = 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,
      ' lines accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

THPDFRapidOCRDLLOptions.Default nomme ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx et ppocr_keys_v1.txt, avec un thread CPU, une limite d’entrée de 16 777 216 pixels, et une échéance de reconnaissance de 60 000 ms. Depuis v2.775.0, THPDFRapidOCRDLLOptions.ForLanguage introduit un modèle de reconnaissance et un dictionnaire assortis pour le chinois traditionnel, le russe, le japonais, l’arabe et d’autres profils ; pourquoi le modèle et le dictionnaire doivent changer ensemble est traité dans les modèles multilingues RapidOCR et les dictionnaires CTC dans HotPDF. Le moteur se déclare RapidOCR (native DLL) dans Info.EngineName, ce qui garde les journaux sans ambiguïté à côté de l’adaptateur process OCR Tesseract externe et du moteur OCR intégré à comparaison de gabarits

Pourquoi l’ABI C ne parle-t-elle qu’en int32_t et octets UTF-8 ?

L’ABI de HotPDFRapidOCR.dll n’utilise que des entiers à largeur fixe, des pointeurs bruts, et des longueurs en octets explicites parce que Delphi, C++Builder et Free Pascal ne partagent rien avec MSVC au-delà de la convention d’appel C. Un std::string, un std::vector, ou une exception C++ a un agencement et un modèle de déroulement qui appartiennent à un compilateur et une bibliothèque runtime donnés. Laissez-en traverser la frontière et l’échec est une pile corrompue ou un bloc de tas libéré par le mauvais allocateur, pas une erreur propre

La version d’ABI 1 suit donc une courte liste de règles. Chaque export est cdecl et renvoie un statut int32_t, où 1 signifie succès et 0 échec. Toute fonction qui peut échouer prend un tampon de diagnostic dont l’appelant est propriétaire et sa capacité en octets ; la DLL y écrit un message UTF-8 terminé par NUL et tronqué pour tenir, et l’adaptateur le décode avec un terminateur en dur dans le dernier octet de son propre tampon de 4 096 octets. Chaque corps d’export est enveloppé dans un try avec à la fois catch (const std::exception &) et catch (...), si bien qu’une erreur ONNX Runtime, une assertion OpenCV, ou un dictionnaire invalide devient statut 0 plus texte, jamais une exception s’échappant vers du code Pascal

ExportRôleQuand l’adaptateur le résout
HPDFRapidOCRAbiVersionRenvoie 1 ; toute autre valeur est rejetéeEn premier, avant toute chose
HPDFRapidOCRCreateCharge les modèles de détection, de classification optionnelle, de reconnaissance et le dictionnaireDans la factory
HPDFRapidOCRRecognizeTraite un bitmap et émet un callback par ligne de texteDans la factory
HPDFRapidOCRDestroyLibère l’instance de modèlesDans la factory
HPDFRapidOCRSetReadingDirectionOrdre de lignes droite à gauche optionnel, ajouté en v2.775.0Seulement si RightToLeft est défini

L’export optionnel est résolu paresseusement à dessein : une DLL v2.774.0 qui en manque sert quand même les demandes de gauche à droite. La DLL est chargée avec LoadLibraryEx et des drapeaux de recherche qui couvrent le propre dossier de la DLL plus les répertoires sûrs par défaut, si bien que des dépendances ONNX Runtime ou OpenCV posées à côté de HotPDFRapidOCR.dll sont trouvées sans toucher au PATH. Les chemins de modèles et de dictionnaires voyagent en UTF-8 et la DLL les convertit avec MultiByteToWideChar en mode strict avant d’ouvrir les fichiers par les API à caractères larges, si bien qu’un dossier de modèles sous un nom d’utilisateur chinois ou cyrillique marche au lieu d’être élargi octet par octet en charabia

Une règle vit dans le build plutôt que dans l’en-tête. La DLL lie statiquement ONNX Runtime et OpenCV, et la configuration CMake par défaut utilise le CRT release statique (/MT). Des bibliothèques statiques compilées contre /MD mélangées à une DLL /MT produisent au mieux des erreurs d’édition de liens et au pire deux tas indépendants, donc les bibliothèques provisionnées doivent correspondre au mode CRT que la DLL utilise

Que se passe-t-il entre un TBitmap et une ligne de texte ?

HotPDF remet à la DLL un instantané BGR indépendant de haut en bas de la page rendue, et la DLL rend un callback par ligne de texte reconnue avec du texte UTF-8 emprunté que l’adaptateur doit copier avant de retourner

Côté Delphi, l’adaptateur affecte le bitmap de page à un TBitmap privé, force pf24bit, et lit les lignes avec GetDIBits en utilisant un biHeight négatif, ce qui donne des lignes de haut en bas alignées sur quatre octets ; ce stride est passé explicitement. Côté FPC, la lecture passe par CreateIntfImage, parce que des écritures scanline LCL peuvent mettre à jour l’image brute sans rafraîchir le handle GDI. Le bitmap de l’appelant n’est jamais modifié, et le budget de pixels (MaxPixels, 16 777 216 par défaut et configurable jusqu’à 67 108 864) et la limite de 32 767 pixels par dimension sont vérifiés avant l’allocation du tampon d’instantané

Pipeline DLL RapidOCR de HotPDF du bitmap à la couche de texte : l’adaptateur prend la page en instantané pf24bit BGR de haut en bas, la DLL comble, détecte, ordonne et reconnaît les découpes, livre un callback par ligne avec texte UTF-8 emprunté, boîte et confiance, et l’adaptateur valide chaque ligne avant le commit de la couche de texte
les pixels traversent l’ABI une fois en instantané, les lignes reviennent un callback à la fois, et rien n’atteint la couche cherchable tant que chaque contrôle n’est pas passé

À l’intérieur de la DLL, l’instantané est complété par 50 pixels blancs, les régions de texte sont détectées avec un côté maximum de 1 024 pixels, les boîtes sont ordonnées en lignes horizontales, et chaque découpe est optionnellement tournée par le classificateur d’angle avant la reconnaissance. Chaque ligne de texte passe ensuite par un callback qui reçoit un const char*, un compte d’octets, une boîte entière en pixels de l’image d’origine, et la confiance moyenne par caractère. Le pointeur de texte n’est valide que pendant le callback, donc l’adaptateur le copie aussitôt, et il est strict sur ce qu’il accepte :

  • L’UTF-8 est décodé avec MB_ERR_INVALID_CHARS ; une séquence mal formée fait échouer la page au lieu de produire des caractères de remplacement dans une couche cherchable
  • Les caractères de contrôle C0 et C1 sont rejetés, et les lignes d’espaces seules sont sautées
  • La boîte doit se trouver dans le bitmap et la confiance doit être une valeur finie de 0 à 1
  • Le texte est compté contre le MaxTextCodeUnits de la requête avec un plafond dur de 1 048 576 unités UTF-16 par appel, et les caractères hors plan multilingue coûtent deux unités
  • Toute exception Pascal à l’intérieur du callback y est attrapée, stockée, et transformée en retour 0, ce qui fait s’arrêter la DLL et signaler l’échec ; le message stocké devient alors le diagnostic

Deux conséquences comptent pour le réglage. D’abord, l’unité de sortie est une ligne, pas un mot : chaque ligne consomme un créneau MaxWords, Info.AcceptedWordCount et Info.DroppedWordCount comptent des lignes, et le surlignage de recherche couvre la boîte de la ligne. Ensuite, MinimumConfidence (0.5 par défaut) est comparé à la confiance moyenne par caractère de la ligne, si bien qu’une ligne avec un caractère illisible parmi vingt propres survit en général. La DLL ne fournit pas de baseline, donc le pipeline de couche de texte en estime une depuis la boîte. Une page vide réussit avec zéro lignes, et tout échec efface les résultats partiels pour que le commit multipages reste tout-ou-rien

Possession des modèles et thread safety

Chaque moteur DLL RapidOCR possède exactement une instance de modèles pour toute sa durée de vie, et les appels à Recognize sur ce moteur sont sérialisés par une section critique. Détenir l’interface IHPDFOCREngine est ce qui garde les modèles chauds, donc le bon motif pour du travail par lots est de créer le moteur une fois et de le réutiliser entre documents

procedure OcrBatch(const Files: TStrings; const OutputDir: string);
var
  Models: THPDFRapidOCRDLLOptions;
  Engine: IHPDFOCREngine;
  Doc: THotPDF;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
  I: Integer;
begin
  Models := THPDFRapidOCRDLLOptions.Default;
  Models.UseAngleClassifier := False;    // scans droits : aucun modèle classificateur chargé
  Models.Threads := 4;                   // 1..64, plafonné au nombre de processeurs logiques
  Models.TimeoutMilliseconds := 120000;  // par appel Recognize, coopératif
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
  Options := THPDFOCRTextLayerOptions.Default;
  for I := 0 to Files.Count - 1 do
  begin
    Doc := THotPDF.Create(nil);
    try
      Doc.AutoLaunch := False;
      if (Doc.LoadFromFile(Files[I]) > 0) and
        Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
        Doc.SaveLoadedDocument(IncludeTrailingPathDelimiter(OutputDir) +
          ExtractFileName(Files[I]))
      else
        Writeln(Files[I], ': ', string(Info.Diagnostic));
    finally
      Doc.Free;
    end;
  end;
end;  // dernière référence libérée : modèles détruits, puis la DLL est déchargée

La valeur Threads règle à la fois les comptes de threads intra-op et inter-op de chaque session ONNX, et la DLL la borne au nombre de processeurs actifs. Deux threads partageant un moteur ne tournent pas en parallèle ; le second attend le verrou. Cette attente n’est pas un EnterCriticalSection aveugle : l’adaptateur appelle TryEnterCriticalSection toutes les 25 ms et vérifie le token d’annulation et l’échéance entre les tentatives, si bien qu’une requête en file peut toujours être annulée ou expirer. Si vous avez besoin d’un vrai parallélisme, créez un moteur par worker et acceptez que chaque moteur garde sa propre copie des modèles en mémoire

L’ordre de démontage est fixé par le destructeur du moteur : HPDFRapidOCRDestroy libère d’abord l’instance de modèles, puis FreeLibrary décharge la DLL. Côté natif, l’initialisation des modèles est tout aussi soignée ; quand le modèle de reconnaissance échoue après que les sessions du détecteur et du classificateur ont déjà été construites, ces sessions sont libérées avant que l’erreur ne soit rapportée, et le compte de classes du dictionnaire est vérifié contre la sortie du modèle pendant l’initialisation plutôt qu’à la première page

Pourquoi un appel OCR natif ne peut-il pas être tué en pleine inférence ?

Un appel RapidOCR natif ne peut pas être tué en pleine inférence parce qu’il tourne sur votre thread, à l’intérieur de votre processus, au milieu d’une session ONNX Runtime qui n’accepte pas d’interruption. L’annulation dans l’adaptateur DLL de HotPDF est donc coopérative : la DLL appelle un callback d’abandon avant et après la détection, après la classification, et après chaque ligne reconnue, et s’arrête au premier point de contrôle où le callback renvoie 0. Un Run ONNX qui a commencé finira d’abord

Les alternatives sont pires qu’attendre. TerminateThread laisserait le verrou de tas du CRT, le thread pool d’ONNX Runtime, et tout état OpenCV dans la condition qui est la leur à ce moment, empoisonnant le reste du processus. FreeLibrary pendant qu’un appel s’exécute encore décharge du code qui est sur la pile. Ni l’un ni l’autre ne peut être rendu sûr, donc l’adaptateur ne tente jamais. L’échéance de TimeoutMilliseconds est par conséquent une échéance coopérative, et une échéance expirée se manifeste en erreur moteur avec un diagnostic de dépassement, tandis qu’un token annulé se manifeste en otlsCancelled :

// Le token est créé par l’appelant et partagé avec le thread UI,
// qui appelle Token.Cancel quand l’utilisateur appuie sur Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
  case Info.Status of
    otlsCancelled:
      // renvoyé à la prochaine frontière d’étape ou de ligne ; document inchangé
      Writeln('Cancelled');
    otlsEngineError:
      // inclut une échéance coopérative expirée et des diagnostics natifs
      Writeln('Engine: ', string(Info.Diagnostic));
    otlsBudgetExceeded:
      Writeln('Budget: ', string(Info.Diagnostic));
  else
    Writeln(string(Info.Diagnostic));
  end;

C’est le compromis central entre les adaptateurs process de HotPDF et la DLL en process, et aucun des deux ne gagne sur chaque ligne :

Compromis des adaptateurs OCR HotPDF : les adaptateurs process démarrent un worker et chargent les modèles à chaque page mais peuvent être tués et contiennent les crashes, tandis que la DLL RapidOCR en process charge les modèles une fois, ne s’arrête qu’aux points de contrôle coopératifs, partage l’espace d’adressage, et se déploie en DLL avec ses modèles et son dictionnaire
choisissez selon la charge : une appli bureau page par page profite de la DLL chaude, tandis qu’un serveur qui avale des scans non fiables doit payer pour le mur du processus
  • Coût de démarrage : les adaptateurs Tesseract et RapidOCR Python lancent un processus et chargent les modèles pour chaque page ; la DLL charge les modèles une fois par moteur
  • Arrêt : un processus enfant peut être terminé sans façon, et le worker Python tourne dans un Job Object kill-on-close si bien que tout son arbre de processus part avec lui ; la DLL ne peut s’arrêter qu’aux frontières d’étape et de ligne
  • Confinement des pannes : un crash de tesseract.exe fait échouer une page ; une violation d’accès à l’intérieur de la DLL emporte votre processus
  • Déploiement : les adaptateurs process ont besoin d’un programme installé ou d’un environnement Python ; la DLL a besoin d’elle-même, de ses modèles et de son dictionnaire, appariés à l’architecture de l’application
  • Mémoire : les adaptateurs process libèrent tout quand l’enfant sort ; un moteur DLL garde ses modèles résidents jusqu’à ce que la dernière référence d’interface soit libérée

Pour une application bureau interactive qui OCRise une page à la fois, la réactivité de la DLL gagne en général. Pour un serveur qui avale des scans non fiables jour et nuit, la frontière de processus vaut son coût de démarrage

Construire et déployer HotPDFRapidOCR.dll

HotPDFRapidOCR.dll se construit depuis les sources C++ de Native/RapidOCR avec MSVC, C++17, un Windows SDK, et CMake 3.20 ou ultérieur, à l’aide d’un script auxiliaire qui prend les répertoires des sources réseau natives, ONNX Runtime, et OpenCV plus une plateforme Win32 ou Win64. Construisez les deux si vous livrez les deux, parce qu’une application Delphi 32 bits ne peut pas charger une DLL 64 bits, et les bibliothèques statiques que vous provisionnez doivent correspondre à l’architecture cible aussi bien qu’au mode CRT

Le côté modèles a ses propres limites de compatibilité. Le détecteur est un détecteur de texte DB ; le reconnaisseur accepte les modèles CTC en agencement NCHW avec une hauteur d’entrée fixe de 32 ou 48, et utilise 48 pour les modèles à hauteur dynamique. Le ONNX Runtime statique livré ne peut pas charger les modèles sauvegardés avec une version IR plus récente, si bien que des exports PP-OCRv5 récents échouent à l’initialisation avec un diagnostic au lieu de se charger partiellement. Le dictionnaire doit être en UTF-8 sans BOM, dans exactement l’ordre de caractères du modèle, et son compte de classes doit correspondre à la sortie du modèle ; les fins de ligne CRLF sont acceptées. La reconnaissance est hors ligne : la DLL ne télécharge jamais un modèle manquant

Aide-mémoire

  • Factory : HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options]) dans HPDFRapidOCRRecognition, disponible depuis v2.774.0 dans les builds Delphi, C++Builder, et FPC/Lazarus Windows
  • Gardez le IHPDFOCREngine renvoyé en vie entre pages et documents ; sa libération détruit les modèles et décharge la DLL
  • Un moteur exécute une reconnaissance à la fois ; créez plusieurs moteurs pour des workers en parallèle et budgétez la mémoire pour chaque copie de modèles
  • La sortie est une entrée par ligne de texte avec confiance moyenne par caractère, filtrée par THPDFOCRTextLayerOptions.MinimumConfidence
  • L’annulation et TimeoutMilliseconds sont coopératives ; un run ONNX en cours finit toujours
  • Faites correspondre la bits de la DLL à l’application, et le mode CRT des bibliothèques statiques ONNX Runtime et OpenCV à la DLL
  • Choisissez un profil de langue par moteur avec THPDFRapidOCRDLLOptions.ForLanguage (v2.775.0) ; un moteur ne détecte pas les langues tout seul

L’adaptateur RapidOCR natif, les adaptateurs OCR par processus, le moteur de rendu de pages qui les alimente, et l’écrivain de couche de texte Unicode invisible se livrent ensemble dans HotPDF, un composant PDF VCL natif pour Delphi et C++Builder. Si votre application de capture ou d’archivage de documents a besoin d’une sortie cherchable sans runtime Python sur la machine cible, le composant HotPDF Delphi PDF fournit tout le pipeline, ne restant à déployer que la DLL et ses modèles