HotPDF fährt chinesische und mehrsprachige OCR in Delphi über seinen nativen RapidOCR-DLL-Adapter: THPDFRapidOCRDLLOptions.ForLanguage mappt einen Sprach-Tag wie 'zh-CN', 'zh-TW', 'ru' oder 'ar' auf ein zusammenpassendes Erkennungsmodell und Zeichwörterbuch, und THotPDF.ApplyLoadedOCRTextLayer macht aus den erkannten Zeilen eine unsichtbare, durchsuchbare Unicode-Textebene auf gescannten PDF-Seiten
Eine Demo in lateinischer Schrift zum Laufen zu bringen ist der leichte Teil. Die interessanten Ausfälle beginnen, wenn Sie auf Traditionelles Chinesisch oder Russisch umschalten und die Ausgabe in selbstbewussten, wohlgeformten Unsinn übergeht, oder wenn jede Zeile stillschweigend ihr letztes Zeichen verliert, oder wenn eine arabische Seite mit ihren Textboxen in falscher Reihenfolge zurückkommt. Keines davon wirft von sich aus eine Exception. Die in HotPDF v2.775.0 hinzugekommenen Sprachprofile existieren größtenteils, um diese Lücken zu schließen, und die vier Fallstricke unten sind es wert, verstanden zu werden, selbst wenn Sie den nativen Code nie anfassen — jeder erklärt ein Symptom, dem Sie sonst einen Tag lang hinterherjagen könnten
Wie wählt ForLanguage Modell und Wörterbuch?
THPDFRapidOCRDLLOptions.ForLanguage löst einen Tag auf eines von neun Profilen auf und liefert Optionen, die auf <profile>/recognition.onnx und <profile>/dictionary.txt unter Ihrem Modellverzeichnis zeigen, während der geteilte Detector, der optionale Winkelklassifikator und die Thread-, Pixel- und Timeout-Defaults von THPDFRapidOCRDLLOptions.Default erhalten bleiben. Die Methode kleinschreibt den Tag, macht aus Unterstrichen Bindestriche und trimmt umgebenden Whitespace, 'zh_TW', 'ZH-tw' und ' zh-tw ' landen also alle auf demselben Profil. Aliase sind eine explizite Liste statt eines Präfix-Abgleichs: 'zh-Hant-TW' wird akzeptiert, weil es gelistet ist, während eine willkürliche, nicht gelistete Regionalvariante EArgumentException wirft, bevor irgendein Modell geladen wird
| Profil | Sprachen | Beispiel-Tags | Festgenageltes Modell |
|---|---|---|---|
ch | Vereinfachtes Chinesisch und Englisch | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Traditionelles Chinesisch | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Englisch | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Französisch, Deutsch, Spanisch, Portugiesisch, Italienisch, Niederländisch, Türkisch | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Japanisch | ja, ja-JP, jpn | PP-OCRv4 |
korean | Koreanisch | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Russisch, Ukrainisch, Bulgarisch, Weißrussisch | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Arabisch, Persisch, Urdu | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Hindi, Marathi, Nepalesisch | hi, mr, ne | PP-OCRv4 |
Der Adapter selbst lädt nie etwas herunter. Sie stellen die Dateien einmal mit dem gebündelten Helfer bereit, etwa tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (oder -Language All für alle neun Profile), und der Helfer legt einen geteilten Detector und Klassifikator unter den Root-Dateinamen ab, die Default erwartet. Danach wird ein vereinfacht-chinesischer Scan mit ein paar Zeilen durchsuchbar. Die Engine-Verkabelung ist dieselbe IHPDFOCREngine-Naht aus dem Artikel zur In-Process-RapidOCR-DLL und ihrer ABI-Grenze, dieser hier bleibt also bei den Sprachen
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, geteilter Detector und Klassifikator
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
// eine leere Seitenliste bedeutet jede Seite; Seiten mit vorhandenem Text werden übersprungen
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;
Zwei Details in dieser Ausgabe verdienen eine Notiz. Die native Pipeline liefert ein Ergebnis pro erkannter Textzeile, nicht pro Wort, AcceptedWordCount zählt hier also Zeilen, und MinimumConfidence wird gegen die mittlere Zeichen-Confidence der ganzen Zeile verglichen: Eine Zeile mit Durchschnitt 0,45 wird als Einheit verworfen. UniqueScalarCount meldet, wie viele unterschiedliche Unicode-Skalare die Textebene in ihren Font und ihre ToUnicode-Tabelle mappen musste — ein nützlicher Sanity-Check, dass tatsächlich CJK-Text angekommen ist statt einer Handvoll lateinischer Fallbacks. Halten Sie die Engine-Schnittstelle dokumentübergreifend am Leben, denn die Modellinitialisierung passiert in der Factory und ist der teure Schritt
Warum produziert der Wechsel nur des Erkennungsmodells Datenmüll?
Ein CTC-Erkennungsmodell gibt nie Zeichen aus, nur Klassenindizes, und das Wörterbuch ist das einzige, das aus Index 1.204 ein Glyph macht. Tauschen Sie ch/recognition.onnx gegen cyrillic/recognition.onnx, behalten aber das chinesische Wörterbuch, gibt das Modell fröhlich gültige kyrillische Indizes aus, die das alte Wörterbuch in zufällige Han-Zeichen übersetzt. Das Ergebnis sieht aus wie Text, besteht die UTF-8-Validierung und ist durchsuchbar für exakt nichts. Deshalb setzt ForLanguage RecognitionModel und CharacterDictionary stets zusammen, und deshalb sollten handgebaute Optionen niemals das eine ohne das andere ändern
Die naheliegende Sicherheitsprüfung, die Wörterbuchgröße gegen die Modellausgabebreite zu vergleichen, ist nötig, aber nicht hinreichend. Zwei Wörterbücher können dieselbe Anzahl Einträge in unterschiedlicher Reihenfolge haben, und ein Off-by-one in der Reihenfolge verschiebt jedes Zeichen um einen Code Point. HotPDF prüft deshalb in zwei Stufen, wenn die Factory das Modell initialisiert. Erstens muss die Ausgabeklassenanzahl den Wörterbucheinträgen plus zwei entsprechen. Zweitens wird, wenn die ONNX-Datei eine character-Metadatenliste einbettet, jeder Wörterbucheintrag der Reihe nach damit verglichen, eine Diskrepanz lässt die Initialisierung mit EInvalidOperation und einer nativen Diagnose scheitern, statt später plausiblen Müll zu produzieren
Das „plus zwei“ kommt aus dem Klassenlayout. Klasse 0 ist das CTC-Blank, die Klassen 1 bis N sind die Wörterbuchzeilen in Dateireihenfolge, und die letzte Klasse ist ein Leerzeichen. Manche Wörterbücher tragen zudem einen eigenen Leerzeichen-Eintrag, und diese Zeile muss exakt so bleiben, wie sie ist. Hier richtet ein wohlmeinendes Trim echten Schaden an: Es macht aus einem Einzele-Leerzeichen-Eintrag einen leeren String und verschiebt oder bricht die Tabelle. Die einzige sichere Normalisierung ist, ein abschließendes Wagenrücklauf zu entfernen, ein mit CRLF-Zeilenenden gespeichertes Wörterbuch lädt also korrekt, während eine UTF-8-Byteordnungsmarke, eine leere Zeile oder ein Eintrag mit Tab abgewiesen wird. Die Skizze unten zeigt das Layout in Pascal; es ist erläuternder Code, keine HotPDF-API
// Nur Illustration: die Klassentabelle, die ein CTC-Recognizer erwartet
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); // Zeilenumbruch am Dateiende
SetLength(Result, Last + 3);
Result[0] := ''; // Klasse 0: CTC-Blank
for I := 0 to Last do
begin
Entry := Lines[I];
if (Entry <> '') and (Entry[Length(Entry)] = #13) then
SetLength(Entry, Length(Entry) - 1); // CRLF: nur das CR wegnehmen
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // nie Trim: ' ' ist eine Klasse
end;
Result[Last + 2] := ' '; // letzte Klasse: Leerzeichen
// Length(Result) muss der Ausgabeklassenanzahl des Modells entsprechen
end;
Was macht Greedy-CTC-Decoding tatsächlich?
Greedy-CTC-Decoding wählt an jedem Zeitschritt die höchstbewertete Klasse, kollabiert aufeinanderfolgende Wiederholungen zu einem Zeichen und verwirft die Blank-Klasse; das Blank ist es, was echten verdoppelten Buchstaben das Überleben erlaubt. Ein Erkennungsmodell betrachtet eine Textzeile als Folge schmaler vertikaler Scheiben, und für jede Scheibe, jeden Zeitschritt, gibt es eine Wahrscheinlichkeit für jede Klasse aus. Eine Zeile mit AA中 könnte die Argmax-Folge A A blank A 中 space produzieren. Die ersten zwei A-Schritte zu einem A zu kollabieren, das Blank trennt es vom nächsten A, und das Ergebnis ist AA中 mit intaktem nachgestelltem Leerzeichen. Ohne die Blank-Regel wären book und bok ununterscheidbar
Da der Dekoder nur ein Dutzend Zeilen hat, sind die Grenzen leicht falsch zu setzen, und die Ausfälle sind still. Hält die innere Argmax-Schleife eine Klasse zu früh an, kann die Leerzeichen-Klasse nie gewinnen, jede Zeile kommt ohne Wortabstände zurück, was die Phrasensuche auf englischen und lateinischen Seiten ruiniert. Hält die äußere Schleife einen Zeitschritt zu früh an, verschwindet das letzte Zeichen jeder Zeile, bei einer kurzen Zeile also ein Drittel des Texts. Und wird der Wiederholungswächter nicht durch ein Blank zurückgesetzt, kollabieren Doppelzeichen wie ll oder chinesische Reduplikationen wie 谢谢 zu einem. Der HotPDF-Dekoder schließt die letzte Klasse und den letzten Zeitschritt ein, behält durch Blanks getrennte Wiederholungen und weist zudem Bewertungen ab, die nicht endlich sind oder außerhalb 0 bis 1 fallen, sowie jede Klassenanzahl, die nicht zum Wörterbuch passt. Hier dieselbe Logik als Pascal-Illustration
// Nur Illustration: Greedy-CTC-Decoding mit korrekten Grenzen.
// Scores hält Steps * Classes Wahrscheinlichkeiten, eine Zeile pro Zeitschritt
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; // Klasse 0 ist das CTC-Blank
for Step := 0 to Steps - 1 do // den letzten Zeitschritt einschließen
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // die letzte Klasse einschließen (Leerzeichen)
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; // ein Blank setzt den Wiederholungswächter zurück
end;
end;
Greedy-Decoding ist nicht die genaueste verfügbare CTC-Strategie; Beam Search mit einem Sprachmodell kann manche mehrdeutigen Scheiben retten. Für gedruckte Dokumente bei 300 DPI ist das Greedy-Ergebnis meist das, was das Modell zu bieten hat, und der Dekoder ist nicht der Ort, Modell-Schwächen zu kompensieren. Das lateinische PP-OCRv3-Modell liest etwa ein ñ als n, selbst auf sauberer Eingabe. HotPDF überklebt das nicht mit nachgeschalteten Zeichenersetzungen, denn eine Ersetzungstabelle, die Spanisch fixt, bricht etwas anderes, und ein falsches Zeichen in einer durchsuchbaren Ebene ist schlimmer als ein ehrlicher Fehlschlag
Wie ordnet HotPDF Textzeilen, eingeschlossen rechts-nach-links-Arabisch?
HotPDF sortiert erkannte Textboxen von oben nach unten, gruppiert Boxen zu einer Zeile, wenn sie vertikal um mindestens die halbe Höhe der kleineren Box überlappen, und ordnet jede Zeile von links nach rechts — oder von rechts nach links, wenn RightToLeft aktiviert ist; die Zeichen innerhalb jeder erkannten Zeile werden nie umgedreht. Die Gruppierung zählt, denn ein Detector zerschneidet eine visuelle Zeile oft in mehrere Boxen, etwa ein Label und ein Wert, getrennt durch eine breite Lücke, und eine reine Top-Koordinaten-Sortierung würde sie mit der Nachbarzeile verschachteln, sobald sich ihre Tops um ein, zwei Pixel unterscheiden
Das arabische Profil setzt RightToLeft := True, was der DLL sagt, die Boxen jeder Zeile an ihrer rechten Kante zu ordnen, vom rechten Rand nach innen. Das ist die gesamte Wirkung. Der Text, den das Modell für eine Zeile zurückgibt, ist bereits in Unicode-Logischer Reihenfolge — der Reihenfolge, in der ein arabischer Leser liest und tippt — und das ist auch die Reihenfolge, die PDF-Textextraktion und Suche erwarten. Den String mechanisch umzudrehen, damit er im Debugger „richtig aussieht“, würde Suche, Kopieren, Einfügen und Screenreader brechen. Bidirektionale Anzeige und Glyphenformung sind Sache des Viewers
Eine Engine bedient ein Sprachprofil. Es gibt keine automatische Schriftenerkennung, ein Dokument, das Schriften mischt, braucht also eine Engine pro Profil, angewandt auf die Seiten, die es nutzen. Weil ApplyLoadedOCRTextLayer eine explizite Seitenliste nimmt und jeden Aufruf als eigene Alles-oder-Nichts-Transaktion committet, ist das unkompliziert
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// wirft EArgumentException für einen unbekannten Tag, bevor irgendein Modell lädt
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // Raum für A3-Seiten bei 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'); // chinese_cht-Profil
Arabic := CreateRapidEngine('ar-SA'); // arabic-Profil, 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;
Die MaxPixels-Zeile steht da mit Grund. Die DLL-Optionen haben einen Default von 16.777.216 Pixeln pro Anfrage, was A4 und US Letter bei 300 DPI bequem abdeckt, aber eine A3-Seite bei 300 DPI ist etwa 3508 mal 4961 Pixel, grob 17,4 Millionen, und die Anfrage wird als über dem Budget abgewiesen. Heben Sie MaxPixels an (die Obergrenze ist 67.108.864) oder senken Sie THPDFOCRTextLayerOptions.DPI für große Formate. Die Rechts-nach-links-Ordnung nutzt den optionalen HPDFRapidOCRSetReadingDirection-Export der ABI-Version 1; der Adapter braucht ihn nur, wenn RightToLeft gesetzt ist, eine ältere DLL bedient also weiterhin Links-nach-rechts-Sprachen und scheitert bei der Engine-Erzeugung mit einer EArgumentException, die den fehlenden Export für Arabisch nennt
Warum lassen sich neuere OCR-Modelle nicht laden?
Die HotPDF-RapidOCR-DLL linkt eine statische ONNX Runtime 1.14, die keine Modelle lesen kann, die mit ONNX-IR-Version 10 gespeichert wurden, und neuere Exporte wie PP-OCRv5-Modelle können eine neuere Runtime brauchen als das; so ein Modell scheitert an der Engine-Erzeugung mit einer nativen Diagnose. Diese Einschränkung ist der Grund, warum die Sprachpakete auf spezifische PP-OCRv3- und PP-OCRv4-Recognizer- und Wörterbuch-Paare festgenagelt sind statt auf „latest“, und warum die Tabelle oben die beiden Generationen mischt: Jedes festgenagelte Paar ist eines, das unter dieser Runtime lädt und verifiziert
Der Installer erzwingt das Pairing. Jede Datei in seinem Manifest trägt einen SHA256-Hash, eine existierende Datei mit anderem Hash stoppt die Installation, statt überschrieben zu werden, und jeder Download landet unter einem temporären Namen und rückt erst an seinen Platz, wenn sein Hash passt. Das schützt vor der stillen Variante des Wörterbuchproblems: Jemand wirft von Hand ein neueres recognition.onnx in einen Profilordner, die Klassenanzahl passt zufällig, und nichts scheitert, bis ein Kunde meldet, dass die Suche Wörter nicht findet, die er klar sehen kann. Zur Laufzeit bleibt der Adapter offline und holt nie ein fehlendes Modell. Der Recognizer validiert zudem die Modellform beim Laden, akzeptiert NCHW-Eingabe mit fester Höhe 32 oder 48 Pixeln oder dynamischer Höhe, die er bei 48 fährt
Brauchen Sie eine Schrift, die keines der neun Profile abdeckt, können Sie RecognitionModel und CharacterDictionary weiterhin auf eigene Dateien zeigen lassen. Dieselben Prüfungen gelten, und das ist der Punkt: Ein unpassendes Paar scheitert an der Initialisierung, nicht im Archiv Ihres Kunden. Für Seiten, auf die keines der RapidOCR-Profile passt, dockt der Tesseract-Adapter für durchsuchbare PDFs an denselben ApplyLoadedOCRTextLayer-Aufruf an, und für maschinengedruckte ASCII-Formulare braucht die eingebaute Template-Matching-OCR-Engine überhaupt keine Modelle
Kurzreferenz: Mehrsprachige-RapidOCR-Checkliste
- Optionen mit
THPDFRapidOCRDLLOptions.ForLanguageerzeugen undEArgumentExceptionals nicht unterstützten Tag behandeln, nicht als Runtime-Fehler RecognitionModelundCharacterDictionarygemeinsam ändern, nie einzeln; gleiche Klassenanzahlen beweisen keine gleiche Zeichenreihenfolge- Wörterbücher als UTF-8 ohne BOM halten, Einträge nie trimmen und damit rechnen, dass das Modell N + 2 Klassen hat: Blank, N Einträge, Leerzeichen
- Ein eigener CTC-Dekoder muss die letzte Klasse und den letzten Zeitschritt abdecken und Wiederholungen, die durch ein Blank getrennt sind, behalten
- Eine Engine pro Sprachprofil nutzen und für gemischtschriftliche Dokumente explizite Seitenlisten übergeben
RightToLeftändert nur die Boxreihenfolge; erkannter Text bleibt in Unicode-Logischer Reihenfolge- Modelle mit
Install-RapidOCRModels.ps1installieren, damit SHA256-Nagelungen das Modell-Wörterbuch-Pairing halten;UseAngleClassifier := Falsesetzen, wenn Sie mit-SkipClassifierinstalliert haben MaxPixelsüber den 16.777.216er Default heben, bevor Sie A3- oder größere Seiten bei 300 DPI fahren
Die RapidOCR-Sprachprofile, der native DLL-Adapter und die OCR-Textebenen-Pipeline sind Teil der HotPDF Delphi PDF Component für Delphi, C++Builder und Windows FPC/Lazarus, ab v2.775.0 für die mehrsprachigen Profile