HotPDF wykonuje chiński i wielojęzyczny OCR w Delphi przez swój natywny adapter DLL RapidOCR: THPDFRapidOCRDLLOptions.ForLanguage mapuje tag językowy taki jak 'zh-CN', 'zh-TW', 'ru' albo 'ar' na dopasowany model rozpoznawania i słownik znaków, a THotPDF.ApplyLoadedOCRTextLayer zamienia rozpoznane linie w niewidoczną, przeszukiwalną warstwę tekstu Unicode na zeskanowanych stronach PDF
Odpalenie dema na piśmie łacińskim to łatwa część. Ciekawe awarie zaczynają się, gdy przełączysz się na chiński tradycyjny albo rosyjski, a wyjście zamienia się w pewne siebie, dobrze uformowane bzdury, albo gdy każda linia po cichu gubi ostatni znak, albo gdy strona arabska wraca z boxami tekstu w złej kolejności. Żadna z tych rzeczy nie podnosi wyjątku sama z siebie. Presety językowe dodane w HotPDF v2.775.0 istnieją głównie po to, żeby te luki zamknąć, a cztery pułapki poniżej warto zrozumieć, nawet jeśli nigdy nie dotkniesz kodu natywnego, bo każda z nich wyjaśnia objaw, za którym inaczej mógłbyś gonić cały dzień
Jak ForLanguage wybiera model i słownik?
THPDFRapidOCRDLLOptions.ForLanguage rozwiązuje tag do jednego z dziewięciu profili i zwraca opcje wskazujące na <profile>/recognition.onnx i <profile>/dictionary.txt pod twoim katalogiem modeli, zachowując przy tym współdzielony detektor, opcjonalny klasyfikator obrotu oraz domyślne wartości wątków, pikseli i timeoutu z THPDFRapidOCRDLLOptions.Default. Metoda zamienia tag na małe litery, podkreślenia na łączniki i obcina białe znaki po bokach, więc 'zh_TW', 'ZH-tw' i ' zh-tw ' lądują na tym samym profilu. Aliasy to jawna lista, nie dopasowanie prefiksem: 'zh-Hant-TW' jest przyjmowany, bo jest na liście, podczas gdy dowolny regionalny wariant poza listą podnosi EArgumentException, zanim jakikolwiek model się załaduje
| Profil | Języki | Przykładowe tagi | Przypięty model |
|---|---|---|---|
ch | Chiński uproszczony i angielski | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Chiński tradycyjny | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Angielski | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Francuski, niemiecki, hiszpański, portugalski, włoski, niderlandzki, turecki | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Japoński | ja, ja-JP, jpn | PP-OCRv4 |
korean | Koreański | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Rosyjski, ukraiński, bułgarski, białoruski | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Arabski, perski, urdu | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Hindi, marathi, nepalski | hi, mr, ne | PP-OCRv4 |
Sam adapter niczego nie pobiera. Pliki zaopatrzysz raz dołączonym helperem, na przykład tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (albo -Language All dla wszystkich dziewięciu profili), a helper kładzie wspólny detektor i klasyfikator pod nazwami plików w korzeniu, których oczekuje Default. Po tym skan po chińsku uproszczonym staje się przeszukiwalny kilkoma liniami. Instalacja engine'a to ten sam szew IHPDFOCREngine, który opisuje artykuł o DLL RapidOCR w procesie i jego granicy ABI, więc ten tekst zostaje przy językach
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, wspólny detektor i klasyfikator
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
// pusta lista stron znaczy każdą stronę; strony mające już tekst są pomijane
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;
Dwa szczegóły w tym wyjściu zasługują na uwagę. Natywny potok zwraca jeden wynik na wykrytą linię tekstu, nie na słowo, więc AcceptedWordCount liczy tu linie, a MinimumConfidence jest porównywane ze średnią pewnością znaków całej linii: linia średnio 0,45 jest upuszczana w całości. UniqueScalarCount raportuje, ile odrębnych skalarów Unicode musiała warstwa tekstu zmapować do swojego fontu i tablicy ToUnicode — przydatny test przytomności, że tekst CJK faktycznie dotarł, a nie garść łacińskich fallbacków. Trzymaj interfejs engine'a żywym w poprzek dokumentów, bo inicjalizacja modeli dzieje się w fabryce i to jest kosztowny krok
Dlaczego podmiana samego modelu rozpoznawania produkuje bzdury?
Model rozpoznawania CTC nigdy nie wypisuje znaków, tylko indeksy klas, a słownik jest jedyną rzeczą, która zamienia indeks 1204 w glif. Podmień ch/recognition.onnx na cyrillic/recognition.onnx, zostawiając chiński słownik, a model chętnie wyemituje poprawne indeksy cyrylicy, które stary słownik przetłumaczy na losowe znaki Han. Wynik wygląda jak tekst, przechodzi walidację UTF-8 i jest przeszukiwalny dokładnie dla niczego. Dlatego ForLanguage zawsze ustawia RecognitionModel i CharacterDictionary razem, a ręcznie składane opcje nie powinny nigdy zmieniać jednego bez drugiego
Oczywisty test bezpieczeństwa, porównanie rozmiaru słownika z szerokością wyjścia modelu, jest konieczny, ale niewystarczający. Dwa słowniki mogą mieć tę samą liczbę wpisów w innej kolejności, a pomyłka o jeden w kolejności przesuwa każdy znak o jeden punkt kodowy. HotPDF sprawdza więc dwuetapowo, gdy fabryka inicjalizuje model. Po pierwsze, liczba klas wyjścia musi równać się wpisom słownika plus dwa. Po drugie, gdy plik ONNX osadza metadane listy character, każdy wpis słownika jest z nią porównywany po kolei, a niezgodność wywala inicjalizację z EInvalidOperation i natywną diagnostyką, zamiast produkować wiarygodne bzdury później
Owe „plus dwa" bierze się z układu klas. Klasa 0 to blank CTC, klasy od 1 do N to linie słownika w kolejności z pliku, a klasa finalna to spacja. Niektóre słowniki niosą też własny wpis spacji i ta linia musi zostać dokładnie taka, jaka jest. Tutaj dobrze myślący Trim robi prawdziwe szkody: zamienia wpis z pojedynczą spacją w pusty napis i przesuwa albo łamie tabelę. Jedyne bezpieczne normalizowanie to usunięcie kończącego znaku powrotu karetki, więc słownik zapisany z zakończeniami wierszy CRLF ładuje się poprawnie, natomiast znak BOM UTF-8, pusta linia albo wpis z tabulatorem są odrzucane. Szkic poniżej pokazuje układ w Pascalu; to kod objaśniający, nie API HotPDF
// Tylko ilustracja: tabela klas, jakiej oczekuje rozpoznawacz 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); // nowy wiersz na końcu pliku
SetLength(Result, Last + 3);
Result[0] := ''; // klasa 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: zrzuć tylko CR
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // nigdy Trim: ' ' to klasa
end;
Result[Last + 2] := ' '; // klasa finalna: spacja
// Length(Result) musi równać się liczbie klas wyjścia modelu
end;
Co właściwie robi zachłanne dekodowanie CTC?
Zachłanne dekodowanie CTC wybiera klasę z najwyższą oceną w każdym kroku czasowym, zwija kolejne powtórzenia w jeden znak i zrzuca klasę blank; blank jest tym, co pozwala naprawdę podwojonym literom przeżyć. Model rozpoznawania patrzy na linię tekstu jako na sekwencję wąskich pionowych plasterków i dla każdego plasterka, czyli kroku czasowego, wypisuje prawdopodobieństwo dla każdej klasy. Linia zawierająca AA中 może wyprodukować sekwencję argmax A A blank A 中 space. Zwinięcie pierwszych dwóch kroków A daje jedno A, blank oddziela je od następnego A, a wynikiem jest AA中 z końcową spacją nietkniętą. Bez reguły blanka book i bok byłyby nie do odróżnienia
Skoro dekoder to kilkanaście linii, łatwo o pomyłkę na granicach, a awarie są ciche. Gdy wewnętrzna pętla argmax stanęła o jedną klasę za wcześnie, klasa spacji nigdy nie wygra i każda linia wróci bez odstępów między słowami, co rozbija wyszukiwanie fraz na stronach angielskich i łacińskich. Gdy zewnętrzna pętla stanęła o jeden krok czasowy za wcześnie, ostatni znak każdej linii znika, co dla krótkiej linii może być trzecią części tekstu. A gdy straż powtórzeń nie jest resetowana blankiem, podwojone znaki takie jak ll albo chińskie reduplikacje takie jak 谢谢 zwijają się w jeden. Dekoder HotPDF dołącza ostatnią klasę i ostatni krok czasowy, trzyma powtórzenia oddzielone blankiem i dodatkowo odrzuca oceny nieskończone albo poza zakresem od 0 do 1 oraz każdą liczbę klas niedopasowaną do słownika. Oto ta sama logika jako ilustracja w Pascalu
// Tylko ilustracja: zachłanne dekodowanie CTC z poprawnymi granicami.
// Scores trzyma Steps * Classes prawdopodobieństw, po jednym wierszu na krok czasowy
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; // klasa 0 to blank CTC
for Step := 0 to Steps - 1 do // dołącz ostatni krok czasowy
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // dołącz ostatnią klasę (spację)
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; // blank resetuje straż powtórzeń
end;
end;
Dekodowanie zachłanne nie jest najdokładniejszą dostępną strategią CTC; beam search z modelem językowym potrafi naprawić niektóre dwuznaczne plasterki. Dla drukowanych dokumentów przy 300 DPI wynik zachłanny to zwykle wszystko, co model ma do zaoferowania, a dekoder nie jest miejscem na kompensowanie słabości modelu. Model łaciński PP-OCRv3 potrafi na przykład przeczytać ñ jako n nawet na czystym wejściu. HotPDF nie zaklejuje tego postprocessingowymi podmianami znaków, bo tabela podstawień, która naprawia hiszpański, psuje coś innego, a zły znak w warstwie przeszukiwalnej jest gorszy niż uczciwa pomyłka
Jak HotPDF porządkuje linie tekstu, łącznie z arabskim od prawej do lewej?
HotPDF sortuje wykryte boxy tekstu od góry do dołu, grupuje boxy w wiersz, gdy nakładają się pionowo na co najmniej połowę wysokości mniejszego boxa, i porządkuje każdy wiersz od lewej do prawej, albo od prawej do lewej, gdy włączono RightToLeft; znaki wewnątrz każdej rozpoznanej linii nie są nigdy odwracane. Grupowanie ma znaczenie, bo detektor często dzieli jedną wizualną linię na kilka boxów — na przykład etykietę i wartość rozdzielone szeroką przerwą — a czyste sortowanie po górnej współrzędnej przeplatałoby je z sąsiednią linią za każdym razem, gdy ich górne krawędzie różnią się o piksel czy dwa
Preset arabski ustawia RightToLeft := True, co każe DLL porządkować boxy w każdym wierszu według ich prawej krawędzi, od prawego marginesu do środka. To cały efekt. Tekst, który model zwraca dla linii, jest już w logicznej kolejności Unicode — tej, w której czyta go i wpisuje arabski czytelnik — i to też jest kolejność, jakiej oczekują ekstrakcja tekstu PDF i wyszukiwanie. Mechaniczne odwrócenie napisu, żeby w debuggerze „wyglądał dobrze", połamałoby wyszukiwanie, kopiowanie, wklejanie i czytniki ekranu. Wyświetlanie dwukierunkowe i kształtowanie glifów to zadanie przeglądarki
Jeden engine obsługuje jeden profil językowy. Nie ma automatycznej detekcji pisma, więc dokument mieszający pisma potrzebuje jednego engine'a na profil, aplikowanego do stron, które go używają. Ponieważ ApplyLoadedOCRTextLayer przyjmuje jawną listę stron i zatwierdza każde wywołanie jako własną transakcję albo wszystko, albo nic, sprawa jest prosta
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// podnosi EArgumentException dla nieznanego taga, zanim jakikolwiek model się załaduje
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // miejsce na strony A3 przy 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;
Linia z MaxPixels nie jest tam przypadkiem. Opcje DLL domyślają się 16 777 216 pikseli na żądanie, co spokojnie pokrywa A4 i US Letter przy 300 DPI, ale strona A3 przy 300 DPI to około 3508 na 4961 pikseli, grubo 17,4 miliona, i żądanie jest odrzucane jako ponad budżet. Podnieś MaxPixels (sufit to 67 108 864) albo obniż THPDFOCRTextLayerOptions.DPI dla dużych formatów. Kolejność od prawej do lewej używa opcjonalnego eksportu HPDFRapidOCRSetReadingDirection wersji ABI 1; adapter wymaga go tylko przy ustawionym RightToLeft, więc starsza DLL wciąż obsługuje języki od lewej do prawej i zawodzi przy tworzeniu engine'a z EArgumentException wymieniającym brakujący eksport dla arabskiego
Dlaczego nowsze modele OCR zawodzą przy ładowaniu?
DLL RapidOCR w HotPDF linkuje statyczny ONNX Runtime 1.14, który nie czyta modeli zapisanych w wersji IR 10 ONNX, a nowsze eksporty, jak modele PP-OCRv5, mogą wymagać nowszego runtime'u; taki model zawodzi przy tworzeniu engine'a z natywną diagnostyką. To ograniczenie jest powodem, dla którego pakiety językowe są przypięte do konkretnych par rozpoznawacz–słownik PP-OCRv3 i PP-OCRv4 zamiast do „najnowszego", i dlatego tabela powyżej miesza obie generacje: każda przypięta para to taka, która ładuje się i weryfikuje pod tym runtime'em
Instalator egzekwuje parowanie. Każdy plik w manifeście niesie hash SHA256, istniejący plik o innym hashu zatrzymuje instalację, zamiast zostać nadpisany, a każde pobranie ląduje pod nazwą tymczasową i wędruje na miejsce dopiero po zgadnięciu się hashu. To chroni przed cichą wersją problemu słownika: ktoś wrzuci ręcznie nowszy recognition.onnx do folderu profilu, liczba klas akurat się zgadza i nic nie zawodzi, dopóki klient nie zgłosi, że wyszukiwanie nie znajduje słów, które widać gołym okiem. W trakcie pracy adapter pozostaje offline i nigdy nie dociąga brakującego modelu. Rozpoznawacz waliduje też kształt modelu przy ładowaniu, przyjmując wejście NCHW o stałej wysokości 32 albo 48 pikseli albo wysokości dynamicznej, którą wykonuje przy 48
Jeśli potrzebujesz pisma, którego żaden z dziewięciu profili nie pokrywa, nadal możesz wskazać RecognitionModel i CharacterDictionary na własne pliki. Te same testy obowiązują — i o to chodzi: niedopasowana para zawodzi na inicjalizacji, a nie w archiwum twojego klienta. Dla stron, gdzie żaden profil RapidOCR nie pasuje, adapter Tesseract dla przeszukiwalnego PDF wpina się w to samo wywołanie ApplyLoadedOCRTextLayer, a dla maszynowo drukowanych formularzy ASCII wbudowany engine OCR z dopasowaniem szablonów nie potrzebuje żadnych modeli
Ściąga: lista kontrolna wielojęzycznego RapidOCR
- Twórz opcje przez
THPDFRapidOCRDLLOptions.ForLanguagei traktujEArgumentExceptionjako niewspierany tag, nie awarię runtime'u - Zmieniaj
RecognitionModeliCharacterDictionaryrazem, nigdy jedno z osobna; równe liczby klas nie dowodzą równej kolejności znaków - Trzymaj słowniki jako UTF-8 bez BOM, nigdy nie przycinaj wpisów i oczekuj, że model ma N + 2 klasy: blank, N wpisów, spacja
- Własny dekoder CTC musi obejmować ostatnią klasę i ostatni krok czasowy oraz trzymać powtórzenia oddzielone blankiem
- Używaj jednego engine'a na profil językowy i przekazuj jawne listy stron dla dokumentów mieszających pisma
RightToLeftzmienia tylko kolejność boxów; rozpoznany tekst zostaje w logicznej kolejności Unicode- Instaluj modele przez
Install-RapidOCRModels.ps1, żeby przypięcia SHA256 trzymały parowanie modelu i słownika; ustawUseAngleClassifier := False, jeśli instalowałeś z-SkipClassifier - Podnieś
MaxPixelsponad domyślne 16 777 216, zanim uruchomisz strony A3 albo większe przy 300 DPI
Presety językowe RapidOCR, natywny adapter DLL i potok warstwy tekstu OCR są częścią komponentu HotPDF Delphi PDF dla Delphi, C++Buildera i Windows FPC/Lazarus, od v2.775.0 dla profili wielojęzycznych