HotPDF fait de l’OCR chinois et multilingue en Delphi par son adaptateur DLL RapidOCR natif : THPDFRapidOCRDLLOptions.ForLanguage associe un tag de langue tel que 'zh-CN', 'zh-TW', 'ru' ou 'ar' à un modèle de reconnaissance et un dictionnaire de caractères assortis, et THotPDF.ApplyLoadedOCRTextLayer transforme les lignes reconnues en une couche de texte Unicode invisible et cherchable sur les pages PDF scannées
Faire marcher une démo en écriture latine, c’est la partie facile. Les échecs intéressants commencent quand vous passez au chinois traditionnel ou au russe et que la sortie devient un charabia assuré et bien formé, ou quand chaque ligne perd silencieusement son dernier caractère, ou quand une page arabe revient avec ses boîtes de texte dans le mauvais ordre. Aucun de ces cas ne lève d’exception de lui-même. Les profils de langue ajoutés dans HotPDF v2.775.0 existent surtout pour fermer ces brèches, et les quatre pièges ci-dessous méritent d’être compris même si vous ne touchez jamais au code natif, parce que chacun explique un symptôme que vous pourriez sinon courir une journée à traquer
Comment ForLanguage choisit-il un modèle et un dictionnaire ?
THPDFRapidOCRDLLOptions.ForLanguage résout un tag vers l’un des neuf profils et renvoie des options qui pointent vers <profile>/recognition.onnx et <profile>/dictionary.txt sous votre dossier de modèles, tout en gardant le détecteur partagé, le classificateur d’angle optionnel, et les défauts de threads, pixels et timeout de THPDFRapidOCRDLLOptions.Default. La méthode met le tag en minuscules, remplace les underscores par des tirets et rogne les espaces autour, si bien que 'zh_TW', 'ZH-tw' et ' zh-tw ' atterrissent tous sur le même profil. Les alias sont une liste explicite plutôt qu’une correspondance par préfixe : 'zh-Hant-TW' est accepté parce qu’il est listé, tandis qu’une variante régionale arbitraire non listée lève EArgumentException avant que tout modèle ne soit chargé
| Profil | Langues | Tags d’exemple | Modèle épinglé |
|---|---|---|---|
ch | Chinois simplifié et anglais | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Chinois traditionnel | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Anglais | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Français, allemand, espagnol, portugais, italien, néerlandais, turc | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Japonais | ja, ja-JP, jpn | PP-OCRv4 |
korean | Coréen | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Russe, ukrainien, bulgare, biélorusse | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Arabe, persan, ourdou | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Hindi, marathi, népalais | hi, mr, ne | PP-OCRv4 |
L’adaptateur lui-même ne télécharge rien. Vous provisionnez les fichiers une fois avec le helper livré, par exemple tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (ou -Language All pour les neuf profils), et le helper pose un détecteur et un classificateur partagés aux noms de fichiers racine que Default attend. Après ça, un scan en chinois simplifié devient cherchable en quelques lignes. La plomberie du moteur est la même couture IHPDFOCREngine décrite dans l’article sur la DLL RapidOCR en process et sa frontière ABI, donc celui-ci reste centré sur les langues
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeChineseScanSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Models: THPDFRapidOCRDLLOptions;
Layer: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// ch/recognition.onnx + ch/dictionary.txt, détecteur et classificateur partagés
Models := THPDFRapidOCRDLLOptions.ForLanguage('zh-CN');
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Layer := 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, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' lines, ', Info.UniqueScalarCount, ' distinct characters');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
Deux détails de cette sortie méritent une note. Le pipeline natif renvoie un résultat par ligne de texte détectée, pas par mot, donc AcceptedWordCount compte des lignes ici, et MinimumConfidence est comparé à la confiance moyenne par caractère de toute la ligne : une ligne qui moyenne 0.45 est abandonnée d’un bloc. UniqueScalarCount rapporte combien de scalaires Unicode distincts la couche de texte a dû mapper dans sa police et sa table ToUnicode, un contrôle de bon sens utile pour vérifier que du texte CJK est réellement arrivé au lieu d’une poignée de replis latins. Gardez l’interface du moteur en vie entre documents, parce que l’initialisation des modèles se produit dans la factory et est l’étape coûteuse
Pourquoi ne changer que le modèle de reconnaissance produit-il du charabia ?
Un modèle de reconnaissance CTC ne sort jamais des caractères, seulement des indices de classes, et le dictionnaire est la seule chose qui transforme l’index 1 204 en glyphe. Remplacez ch/recognition.onnx par cyrillic/recognition.onnx en gardant le dictionnaire chinois, et le modèle émettra joyeusement des indices cyrilliques valides que l’ancien dictionnaire traduit en caractères Han aléatoires. Le résultat ressemble à du texte, passe la validation UTF-8, et est cherchable pour exactement rien. Voilà pourquoi ForLanguage définit toujours RecognitionModel et CharacterDictionary ensemble, et pourquoi des options bâties à la main ne devraient jamais changer l’une sans l’autre
Le contrôle de sécurité évident, comparer la taille du dictionnaire à la largeur de sortie du modèle, est nécessaire mais pas suffisant. Deux dictionnaires peuvent avoir le même nombre d’entrées dans un ordre différent, et un décalage d’un dans l’ordre déplace chaque caractère d’un point de code. HotPDF vérifie donc en deux étapes quand la factory initialise le modèle. D’abord, le compte de classes en sortie doit égaler les entrées du dictionnaire plus deux. Ensuite, quand le fichier ONNX embarque une liste de métadonnées character, chaque entrée du dictionnaire est comparée à elle dans l’ordre, et un désaccord fait échouer l’initialisation avec EInvalidOperation et un diagnostic natif au lieu de produire plus tard du charabia plausible
Le « plus deux » vient de l’agencement des classes. La classe 0 est le blank CTC, les classes 1 à N sont les lignes du dictionnaire dans l’ordre du fichier, et la classe finale est une espace. Certains dictionnaires portent aussi leur propre entrée espace, et cette ligne doit rester exactement telle quelle. C’est là qu’un Trim bien intentionné fait de vrais dégâts : il transforme une entrée d’une seule espace en chaîne vide et décale ou casse la table. La seule normalisation sûre est de retirer un retour chariot traînant, si bien qu’un dictionnaire sauvegardé avec des fins de ligne CRLF se charge correctement, tandis qu’une marque d’ordre des octets UTF-8, une ligne vide, ou une entrée contenant une tabulation est rejetée. Le croquis ci-dessous montre l’agencement en Pascal ; c’est du code explicatif, pas une API HotPDF
// Illustration seulement : la table de classes qu’attend un reconnaisseur CTC
uses
SysUtils, IOUtils;
function BuildCTCClassTable(const FileName: string): TArray<string>;
var
Text, Entry: string;
Lines: TArray<string>;
I, Last: Integer;
begin
Text := TEncoding.UTF8.GetString(TFile.ReadAllBytes(FileName));
if (Text <> '') and (Text[1] = #$FEFF) then
raise EArgumentException.Create('Dictionary must be UTF-8 without a BOM');
Lines := Text.Split([#10]);
Last := High(Lines);
if (Last >= 0) and (Lines[Last] = '') then
Dec(Last); // retour à la ligne en fin de fichier
SetLength(Result, Last + 3);
Result[0] := ''; // classe 0 : blank CTC
for I := 0 to Last do
begin
Entry := Lines[I];
if (Entry <> '') and (Entry[Length(Entry)] = #13) then
SetLength(Entry, Length(Entry) - 1); // CRLF : retire seulement le CR
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // jamais de Trim : ' ' est une classe
end;
Result[Last + 2] := ' '; // classe finale : espace
// Length(Result) doit égaler le compte de classes en sortie du modèle
end;
Que fait réellement le décodage CTC glouton ?
Le décodage CTC glouton choisit la classe au score le plus haut à chaque pas de temps, réduit les répétitions consécutives en un caractère, et jette la classe blank ; le blank est ce qui permet aux lettres réellement doublées de survivre. Un modèle de reconnaissance regarde une ligne de texte comme une séquence de fines tranches verticales, et pour chaque tranche, ou pas de temps, il sort une probabilité pour chaque classe. Une ligne contenant AA中 peut produire la séquence argmax A A blank A 中 space. Réduire les deux premiers pas A donne un seul A, le blank le sépare du A suivant, et le résultat est AA中 avec l’espace finale intacte. Sans la règle du blank, book et bok seraient indiscernables
Le décodeur ne faisant qu’une douzaine de lignes, il est facile de se tromper sur les frontières, et les échecs sont silencieux. Si la boucle argmax interne s’arrête une classe trop tôt, la classe espace ne peut jamais gagner et chaque ligne revient sans espacement de mots, ce qui ruine la recherche d’expressions sur les pages anglaises et latines. Si la boucle externe s’arrête un pas de temps trop tôt, le dernier caractère de chaque ligne disparaît, ce qui pour une ligne courte peut être un tiers du texte. Et si le garde-fou de répétition n’est pas réarmé par un blank, des caractères doublés comme ll ou des redoublements chinois comme 谢谢 se réduisent en un seul. Le décodeur HotPDF inclut la dernière classe et le dernier pas de temps, garde les répétitions séparées par un blank, et rejette en plus les scores non finis ou hors de 0 à 1, et tout compte de classes qui ne correspond pas au dictionnaire. Voici la même logique en illustration Pascal
// Illustration seulement : décodage CTC glouton avec bonnes frontières.
// Scores tient Steps * Classes probabilités, une ligne par pas de temps
function GreedyCTCDecode(const Scores: array of Single;
Steps, Classes: Integer; const Characters: array of string): string;
var
Step, C, Best, Previous: Integer;
BestScore: Single;
begin
if (Classes < 3) or (Length(Characters) <> Classes) or
(Length(Scores) <> Steps * Classes) then
raise EArgumentException.Create('Model output does not match the dictionary');
Result := '';
Previous := 0; // la classe 0 est le blank CTC
for Step := 0 to Steps - 1 do // inclure le dernier pas de temps
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // inclure la dernière classe (espace)
if Scores[Step * Classes + C] > BestScore then
begin
Best := C;
BestScore := Scores[Step * Classes + C];
end;
if (Best <> 0) and (Best <> Previous) then
Result := Result + Characters[Best];
Previous := Best; // un blank réarme le garde-fou de répétition
end;
end;
Le décodage glouton n’est pas la stratégie CTC la plus précise qui soit ; une beam search avec un modèle de langue peut corriger certaines tranches ambiguës. Pour des documents imprimés à 300 DPI, le résultat glouton est d’ordinaire ce que le modèle a à offrir, et le décodeur n’est pas l’endroit où compenser les faiblesses du modèle. Le modèle latin PP-OCRv3, par exemple, peut lire ñ comme n même sur une entrée propre. HotPDF ne maquille pas ça avec des remplacements de caractères en post-traitement, parce qu’une table de substitution qui arrange l’espagnol casse autre chose, et un caractère faux dans une couche cherchable est pire qu’un raté honnête
Comment HotPDF ordonne-t-il les lignes de texte, y compris l’arabe de droite à gauche ?
HotPDF trie les boîtes de texte détectées de haut en bas, groupe les boîtes en une ligne quand elles se chevauchent verticalement d’au moins la moitié de la hauteur de la plus petite boîte, et ordonne chaque ligne de gauche à droite, ou de droite à gauche quand RightToLeft est activé ; les caractères à l’intérieur de chaque ligne reconnue ne sont jamais inversés. Le groupement compte parce qu’un détecteur sépare souvent une ligne visuelle en plusieurs boîtes, par exemple une étiquette et une valeur séparées par un large blanc, et un tri pur sur la coordonnée haute les entrelacerait avec la ligne voisine dès que leurs hauts diffèrent d’un pixel ou deux
Le profil arabe définit RightToLeft := True, ce qui dit à la DLL d’ordonner les boîtes de chaque ligne par leur bord droit, depuis la marge droite vers l’intérieur. C’est tout l’effet. Le texte que le modèle renvoie pour une ligne est déjà en ordre logique Unicode, l’ordre dans lequel un lecteur arabe le lit et le tape, et c’est aussi l’ordre qu’attendent l’extraction et la recherche de texte PDF. Inverser mécaniquement la chaîne pour qu’elle « ait l’air correcte » dans un débogueur casserait la recherche, le copier-coller, et les lecteurs d’écran. L’affichage bidirectionnel et la mise en forme des glyphes sont le travail de la visionneuse
Un moteur sert un profil de langue. Il n’y a pas de détection d’écriture automatique, donc un document qui mélange des écritures a besoin d’un moteur par profil, appliqué aux pages qui l’utilisent. Comme ApplyLoadedOCRTextLayer prend une liste de pages explicite et commit chaque appel comme sa propre transaction tout-ou-rien, c’est direct
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// lève EArgumentException pour un tag inconnu, avant tout chargement de modèle
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // de la place pour des pages A3 à 300 DPI
Result := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
end;
procedure OCRMixedArchive(Doc: THotPDF);
var
Chinese, Arabic: IHPDFOCREngine;
Layer: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
Chinese := CreateRapidEngine('zh-TW'); // profil chinese_cht
Arabic := CreateRapidEngine('ar-SA'); // profil arabic, RightToLeft = True
Layer := THPDFOCRTextLayerOptions.Default;
if not Doc.ApplyLoadedOCRTextLayer([0, 1, 2], Chinese, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
if not Doc.ApplyLoadedOCRTextLayer([3], Arabic, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
end;
La ligne MaxPixels est là pour une raison. Les options de la DLL valent par défaut 16 777 216 pixels par requête, ce qui couvre confortablement A4 et US Letter à 300 DPI, mais une page A3 à 300 DPI fait environ 3508 par 4961 pixels, à peu près 17,4 millions, et la requête est refusée comme hors budget. Montez MaxPixels (le plafond est 67 108 864) ou baissez THPDFOCRTextLayerOptions.DPI pour les grands formats. L’ordonnancement de droite à gauche utilise l’export optionnel HPDFRapidOCRSetReadingDirection de la version d’ABI 1 ; l’adaptateur ne l’exige que quand RightToLeft est défini, si bien qu’une DLL plus ancienne continue de servir les langues de gauche à droite et échoue à la création du moteur avec une EArgumentException nommant l’export manquant pour l’arabe
Pourquoi les modèles OCR plus récents échouent-ils à se charger ?
La DLL RapidOCR de HotPDF lie un ONNX Runtime 1.14 statique, qui ne peut pas lire les modèles sauvegardés avec la version IR 10 d’ONNX, et des exports plus récents comme les modèles PP-OCRv5 peuvent exiger un runtime plus récent que ça ; un tel modèle échoue à la création du moteur avec un diagnostic natif. Cette contrainte est la raison pour laquelle les paquets de langue sont épinglés à des paires reconnaisseur-dictionnaire PP-OCRv3 et PP-OCRv4 précises plutôt qu’à « la dernière », et pourquoi la table ci-dessus mélange les deux générations : chaque paire épinglée est une qui se charge et se vérifie sous ce runtime
L’installeur fait respecter l’appariement. Chaque fichier de son manifeste porte un hash SHA256, un fichier existant avec un hash différent arrête l’installation au lieu d’être écrasé, et chaque téléchargement atterrit sous un nom temporaire et ne bouge en place qu’après que son hash correspond. Ça protège contre la version silencieuse du problème de dictionnaire : quelqu’un dépose à la main un recognition.onnx plus récent dans un dossier de profil, le compte de classes tombe par hasard juste, et rien n’échoue jusqu’à ce qu’un client signale que la recherche ne trouve pas des mots qu’il voit parfaitement. À l’exécution, l’adaptateur reste hors ligne et ne va jamais chercher un modèle manquant. Le reconnaisseur valide aussi la forme du modèle au chargement, acceptant une entrée NCHW à hauteur fixe de 32 ou 48 pixels ou à hauteur dynamique, qu’il exécute à 48
Si vous avez besoin d’une écriture qu’aucun des neuf profils ne couvre, vous pouvez toujours pointer RecognitionModel et CharacterDictionary vers vos propres fichiers. Les mêmes contrôles s’appliquent, et c’est le but : une paire désappariée échoue à l’initialisation, pas dans l’archive de votre client. Pour les pages où aucun profil RapidOCR ne convient, l’adaptateur Tesseract pour PDF cherchable se branche sur le même appel ApplyLoadedOCRTextLayer, et pour des formulaires ASCII imprimés par machine le moteur OCR intégré à comparaison de gabarits n’a besoin d’aucun modèle
Aide-mémoire : checklist RapidOCR multilingue
- Créez les options avec
THPDFRapidOCRDLLOptions.ForLanguageet traitezEArgumentExceptioncomme un tag non pris en charge, pas comme une panne runtime - Changez
RecognitionModeletCharacterDictionaryensemble, jamais l’un seul ; des comptes de classes égaux ne prouvent pas un ordre de caractères égal - Gardez les dictionnaires en UTF-8 sans BOM, ne rognez jamais les entrées, et attendez-vous à N + 2 classes du modèle : blank, N entrées, espace
- Un décodeur CTC personnalisé doit couvrir la dernière classe et le dernier pas de temps, et garder les répétitions séparées par un blank
- Utilisez un moteur par profil de langue et passez des listes de pages explicites pour les documents multi-écritures
RightToLeftne change que l’ordre des boîtes ; le texte reconnu reste en ordre logique Unicode- Installez les modèles avec
Install-RapidOCRModels.ps1pour que les épinglages SHA256 tiennent l’appariement modèle-dictionnaire ; mettezUseAngleClassifier := Falsesi vous avez installé avec-SkipClassifier - Montez
MaxPixelsau-dessus du défaut de 16 777 216 avant de traiter des pages A3 ou plus à 300 DPI
Les profils de langue RapidOCR, l’adaptateur DLL natif et le pipeline de couche de texte OCR font partie du HotPDF Delphi PDF Component pour Delphi, C++Builder et FPC/Lazarus Windows, à partir de v2.775.0 pour les profils multilingues