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
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
| Export | Rôle | Quand l’adaptateur le résout |
|---|---|---|
HPDFRapidOCRAbiVersion | Renvoie 1 ; toute autre valeur est rejetée | En premier, avant toute chose |
HPDFRapidOCRCreate | Charge les modèles de détection, de classification optionnelle, de reconnaissance et le dictionnaire | Dans la factory |
HPDFRapidOCRRecognize | Traite un bitmap et émet un callback par ligne de texte | Dans la factory |
HPDFRapidOCRDestroy | Libère l’instance de modèles | Dans la factory |
HPDFRapidOCRSetReadingDirection | Ordre de lignes droite à gauche optionnel, ajouté en v2.775.0 | Seulement 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é
À 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
MaxTextCodeUnitsde 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 :
- 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.exefait é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])dansHPDFRapidOCRRecognition, disponible depuis v2.774.0 dans les builds Delphi, C++Builder, et FPC/Lazarus Windows - Gardez le
IHPDFOCREnginerenvoyé 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
TimeoutMillisecondssont 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