HotPDF виконує китайський і багатомовний OCR у Delphi через свій нативний адаптер RapidOCR DLL: THPDFRapidOCRDLLOptions.ForLanguage мапить мовний тег на кшталт 'zh-CN', 'zh-TW', 'ru' чи 'ar' на підібрану модель розпізнавання та словник символів, а THotPDF.ApplyLoadedOCRTextLayer перетворює розпізнані рядки на невидимий, пошуковий Unicode-текстовий шар на відсканованих сторінках PDF
Запустити демо на латиниці — це легка частина. Цікаві відмови починаються, коли ви перемикаєтеся на традиційну китайську чи російську і вихід перетворюється на впевнену, добре сформовану нісенітницю, або коли кожен рядок тихо втрачає свій останній символ, чи коли арабська сторінка повертається з текстовими боксами в неправильному порядку. Жодна з цих ситуацій сама собою не піднімає виключення. Мовні пресети, додані в HotPDF v2.775.0, існують здебільшого щоб закрити ті прогалини, а чотири пастки нижче варто розуміти, навіть якщо ви ніколи не торкаєтеся нативного коду, бо кожна пояснює симптом, за яким інакше можна ганяти цілий день
Як ForLanguage обирає модель і словник?
THPDFRapidOCRDLLOptions.ForLanguage розрішує тег до одного з дев'яти профілів і повертає опції, що вказують на <profile>/recognition.onnx і <profile>/dictionary.txt нижче вашої папки моделей, зберігаючи спільний детектор, необов'язковий класифікатор кута та усталення потоків, пікселів і таймауту з THPDFRapidOCRDLLOptions.Default. Метод знижує регістр тега, перетворює підкреслення на дефіси і підстрибує навколишні пробіли, тож 'zh_TW', 'ZH-tw' і ' zh-tw ' приземляються на той самий профіль. Аліаси — явний список, а не збіг за префіксом: 'zh-Hant-TW' приймається, бо він перелічений, тоді як довільний регіональний варіант, якого немає в списку, піднімає EArgumentException до завантаження хоч якоїсь моделі
| Профіль | Мови | Приклади тегів | Закріплена модель |
|---|---|---|---|
ch | Спрощена китайська та англійська | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Традиційна китайська | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Англійська | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Французька, німецька, іспанська, португальська, італійська, нідерландська, турецька | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Японська | ja, ja-JP, jpn | PP-OCRv4 |
korean | Корейська | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Російська, українська, болгарська, білоруська | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Арабська, перська, урду | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Гінді, маратхі, непальська | hi, mr, ne | PP-OCRv4 |
Сам адаптер ніколи нічого не завантажує з мережі. Файли ви постачаєте один раз комплектним хелпером, скажімо tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (чи -Language All для всіх дев'яти профілів), і хелпер кладе спільні детектор і класифікатор у кореневі імена файлів, яких очікує Default. Після того відсканований документ спрощеною китайською стає пошуковим кількома рядками. Рушійна обв'язка — це той самий шов IHPDFOCREngine, описаний у статті про RapidOCR DLL у процесі та її межу ABI, тож ця стаття лишається зосередженою на мовах
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, спільні детектор і класифікатор
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
// порожній список сторінок означає всі; сторінки з наявним текстом пропускаються
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;
Дві деталі в тому виводі заслуговують на нотатку. Нативний конвеєр повертає один результат на виявлений рядок тексту, а не на слово, тож AcceptedWordCount тут лічить рядки, а MinimumConfidence порівнюється із середньою впевненістю символів усього рядка: рядок, що в середньому дає 0.45, скидається як єдине ціле. UniqueScalarCount звітує, скільки окремих Unicode-скалярів текстовому шару довелося замапити у свій шрифт і таблицю ToUnicode, — корисна санітарна перевірка, що CJK-текст справді прибув, а не жменька латинських фолбеків. Тримайте інтерфейс рушія живим між документами, бо ініціалізація моделей відбувається у фабриці і є дорогим кроком
Чому заміна лише моделі розпізнавання породжує сміття?
CTC-модель розпізнавання ніколи не виводить символи, лише індекси класів, і словник — це єдине, що перетворює індекс 1 204 на гліф. Замініть ch/recognition.onnx на cyrillic/recognition.onnx, але тримайте китайський словник, — і модель радісно виводитиме валідні кириличні індекси, які старий словник перекладає у випадкові ханьські символи. Результат виглядає як текст, проходить UTF-8-валідацію і є пошуковим рівно для нічого. Саме тому ForLanguage завжди ставить RecognitionModel і CharacterDictionary разом, і саме тому рукописні опції ніколи не мають змінювати одне без іншого
Очевидна перевірка безпеки — порівняння розміру словника з шириною виходу моделі — необхідна, але недостатня. Два словники можуть мати однакову кількість записів в іншому порядку, і off-by-one у порядку зсуває кожен символ на один code point. Тому HotPDF перевіряє у два стадії, коли фабрика ініціалізує модель. Перше: кількість класів виходу мусить дорівнювати записам словника плюс два. Друге: коли ONNX-файл вбудовує метасписок character, кожен запис словника порівнюється з ним по порядку, і розбіжність валить ініціалізацію з EInvalidOperation та нативною діагностикою замість породження правдоподібного сміття пізніше
«Плюс два» походить із компонування класів. Клас 0 — це CTC blank, класи 1 по N — це рядки словника в порядку файлу, а фінальний клас — пробіл. Деякі словники ще й несуть власний запис пробілу, і той рядок мусить лишатися точно як є. Саме тут добронамірний Trim чинить справжню шкоду: він перетворює запис з одного пробілу на порожній рядок і зсуває чи ламає таблицю. Єдина безпечна нормалізація — прибрати кінцевий повертий кар'єр, тож словник, збережений з кінцями рядків CRLF, завантажується правильно, тоді як BOM у UTF-8, порожній рядок чи запис із табом відхиляються. Ескіз нижче показує компонування на Pascal; це пояснювальний код, а не HotPDF API
// Лише ілюстрація: таблиця класів, яку очікує 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); // переклад рядка в кінці файлу
SetLength(Result, Last + 3);
Result[0] := ''; // клас 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: скидаємо лише CR
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // ніколи не Trim: ' ' — це клас
end;
Result[Last + 2] := ' '; // фінальний клас: пробіл
// Length(Result) мусить дорівнювати кількості класів виходу моделі
end;
Що насправді робить жадібне CTC-декодування?
Жадібне CTC-декодування обирає клас із найвищим рахунком на кожному часовому кроці, стискає підрядні повтори в один символ і скидає клас blank; blank — це те, що дозволяє справді подвоєним літерам виживати. Модель розпізнавання дивиться на рядок тексту як на послідовність вузьких вертикальних зрізів, і для кожного зрізу, чи часового кроку, вона виводить імовірність для кожного класу. Рядок, що містить AA中, міг би породити argmax-послідовність A A blank A 中 space. Стискання перших двох кроків A дає одну A, blank відділяє її від наступної A, а результат — AA中 із кінцевим пробілом недоторканим. Без правила blank book і bok були б нерозрізненні
Оскільки декодер — це зо дюжини рядків, межі легко зіпсувати, а відмови тихі. Якщо внутрішній argmax-цикл зупиниться на один клас раніше, клас пробілу ніколи не переможе, і кожен рядок повернеться без пробілів слів, що руйнує пошук фраз на англійських і латинських сторінках. Якщо зовнішній цикл зупиниться на один часовий крок раніше, останній символ кожного рядка зникає, що для короткого рядка може бути третиною тексту. А якщо охоронець повторів не скидається blank-ом, подвоєні символи, як-от ll, чи китайські редуплікації, як-от 谢谢, стискаються в один. Декодер HotPDF включає останній клас і останній часовий крок, тримає розділені blank-ом повтори і додатково відхиляє рахунки, що не скінченні чи виходять з 0 до 1, і будь-яку кількість класів, що не збігається зі словником. Ось та сама логіка як Pascal-ілюстрація
// Лише ілюстрація: жадібне CTC-декодування з правильними межами.
// Scores тримає Steps * Classes імовірностей, один рядок на часовий крок
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; // клас 0 — це CTC blank
for Step := 0 to Steps - 1 do // включаємо останній часовий крок
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // включаємо останній клас (пробіл)
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 скидає охоронця повторів
end;
end;
Жадібне декодування — не найточніша наявна CTC-стратегія; beam search із мовною моделлю може полагодити деякі неоднозначні зрізи. Для надрукованих документів при 300 DPI жадібний результат — зазвичай усе, що модель має запропонувати, а декодер — не те місце, де варто компенсувати слабкості моделі. Латинська модель PP-OCRv3, скажімо, може прочитати ñ як n навіть на чистому вводі. HotPDF не замазує того постобробковими замінами символів, бо таблиця підстановок, що полагоджує іспанську, ламає щось інше, а неправильний символ у пошуковому шарі гірший за чесний промах
Як HotPDF упорядковує рядки тексту, включно з арабською справа ліворуч?
HotPDF сортує виявлені текстові бокси згори вниз, групує бокси в ряд, коли вони перетинаються вертикально щонайменше на половину висоти меншого бокса, і впорядковує кожен ряд зліва праворуч, чи справа ліворуч, коли увімкнено RightToLeft; символи всередині кожного розпізнаного рядка ніколи не обертаються. Групування важить тому, що детектор часто розколює один візуальний рядок на кілька боксів, скажімо мітку та значення, розділені широким проміжком, і чисте сортування за верхньою координатою переплело б їх із сусіднім рядком щоразу, коли їхні верхи різняться на піксель чи два
Арабський пресет ставить RightToLeft := True, що каже DLL упорядковувати бокси в кожному ряду за їхнім правим краєм, від правого поля всередину. Це вся дія. Текст, який модель повертає для рядка, уже в Unicode-логічному порядку — тому, в якому арабський читач читає і набирає, — і це також порядок, якого очікують екстракція та пошук тексту PDF. Механічне обертання рядка, щоб той «виглядав правильно» в дебагері, зламало б пошук, копіювання, вставляння та скрінрідери. Двонаправлений показ і формування гліфів — робота переглядача
Один рушій обслуговує один мовний профіль. Автоматичної детекції писемності немає, тож документ, що змішує писемності, потребує по одному рушію на профіль, застосованому до сторінок, які ним користуються. Оскільки ApplyLoadedOCRTextLayer бере явний список сторінок і комітить кожен виклик як власну транзакцію все-або-нічого, це прямо
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// піднімає EArgumentException для невідомого тега, до завантаження якоїсь моделі
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // місце для сторінок 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'); // профіль chinese_cht
Arabic := CreateRapidEngine('ar-SA'); // профіль 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;
Рядок MaxPixels там недаремно. Опції DLL усталюються до 16 777 216 пікселів на запит, що комфортно покриває A4 та US Letter при 300 DPI, але сторінка A3 при 300 DPI — це приблизно 3508 на 4961 пікселів, зо 17,4 мільйона, і запит відхиляється як поза бюджетом. Піднімайте MaxPixels (стеля — 67 108 864) чи знижуйте THPDFOCRTextLayerOptions.DPI для великих форматів. Упорядкування справа ліворуч користується необов'язковим експортом HPDFRapidOCRSetReadingDirection версії ABI 1; адаптер вимагає його лише коли встановлено RightToLeft, тож старіший DLL досі обслуговує мови зліва праворуч і падає при створенні рушія з EArgumentException, що називає відсутній експорт для арабської
Чому новіші OCR-моделі падають при завантаженні?
RapidOCR DLL від HotPDF лінкує статичний ONNX Runtime 1.14, який не може читати моделі, збережені з ONNX IR версією 10, а новіші експорти, як-от моделі PP-OCRv5, можуть вимагати новішого runtime; така модель падає при створенні рушія з нативною діагностикою. Саме те обмеження — причина, чому мовні пакети закріплені за конкретними парами розпізнавача і словника PP-OCRv3 та PP-OCRv4 замість «найсвіжішого», і чому таблиця вище змішує два покоління: кожна закріплена пара — це пара, що завантажується і верифікується під тим runtime
Інсталятор застосовує парування. Кожен файл у його маніфесті несе хеш SHA256, наявний файл з іншим хешем зупиняє встановлення замість перезаписування, і кожне завантаження приземлюється під тимчасовим ім'ям і пересувається на місце лише після збігу хешу. Те захищає від тихої версії проблеми словника: хтось кидає новіший recognition.onnx у папку профілю вручну, кількість класів випадково збігається, і нічого не падає, доки клієнт не звітує, що пошук не знаходить слів, які він plainly бачить. У виконанні адаптер лишається офлайн і ніколи не тягне відсутню модель. Розпізнавач ще й валідує форму моделі при завантаженні, приймаючи NCHW-ввід з фіксованою висотою 32 чи 48 пікселів чи динамічною висотою, яку він ганяє при 48
Якщо вам потрібна писемність, яку жоден із дев'яти профілів не покриває, ви досі можете націлити RecognitionModel і CharacterDictionary на власні файли. Ті самі перевірки застосовуються — у тому й сенс: невідповідна пара падає при ініціалізації, а не в архіві вашого клієнта. Для сторінок, де жоден профіль RapidOCR не пасує, адаптер Tesseract для пошукового PDF встромляється в той самий виклик ApplyLoadedOCRTextLayer, а для машинодрукованих ASCII-форм вбудований рушій OCR на template matching узагалі не потребує моделей
Шпаргалка: чекліст багатомовного RapidOCR
- Створюйте опції через
THPDFRapidOCRDLLOptions.ForLanguageі трактуйтеEArgumentExceptionяк непідтримуваний тег, а не fault виконання - Змінюйте
RecognitionModelіCharacterDictionaryразом, ніколи не поодинці; рівні кількості класів не доводять рівного порядку символів - Тримайте словники як UTF-8 без BOM, ніколи не підстрибуйте записи і очікуйте від моделі N + 2 класів: blank, N записів, пробіл
- Власний CTC-декодер мусять покривати останній клас і останній часовий крок і тримати повтори, розділені blank-ом
- Користуйтеся одним рушієм на мовний профіль і передавайте явні списки сторінок для документів зі змішаними писемностями
RightToLeftзмінює лише порядок боксів; розпізнаний текст лишається в Unicode-логічному порядку- Встановлюйте моделі через
Install-RapidOCRModels.ps1, щоб SHA256-закріплення тримали парування моделі та словника; ставтеUseAngleClassifier := False, якщо встановлювали з-SkipClassifier - Піднімайте
MaxPixelsпонад усталені 16 777 216 перед прогоном сторінок A3 чи більших при 300 DPI
Мовні пресети RapidOCR, нативний DLL-адаптер і конвеєр OCR-текстового шару — частина HotPDF Delphi PDF Component для Delphi, C++Builder і Windows FPC/Lazarus, починаючи з v2.775.0 для багатомовних профілів