HotPDF выполняет китайский и мультиязычный OCR в Delphi через свой нативный адаптер RapidOCR DLL: THPDFRapidOCRDLLOptions.ForLanguage мапит языковой тег вроде 'zh-CN', 'zh-TW', 'ru' или 'ar' на подобранную пару модели распознавания и словаря символов, а THotPDF.ApplyLoadedOCRTextLayer превращает распознанные строки в невидимый searchable 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. После этого отсканированный упрощённый китайский становится searchable за несколько строк. Движковая обвязка — тот же шов IHPDFOCREngine, описанный в статье об in-process 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 и searchable ровно для ничего. Поэтому ForLanguage всегда выставляет RecognitionModel и CharacterDictionary вместе, а рукописные опции не должны менять одно без другого
Очевидная проверка безопасности — сверка размера словаря с шириной выхода модели — необходима, но недостаточна. Два словаря могут иметь поровну записей в разном порядке, и ошибка на единицу в порядке сдвигает каждый символ на одну кодовую точку. Поэтому HotPDF проверяет в два этапа, когда фабрика инициализирует модель. Во-первых, число классов выхода обязано равняться записям словаря плюс два. Во-вторых, когда ONNX-файл вшивает метаданные-список character, каждая запись словаря сверяется с ним по порядку, и расхождение топит инициализацию с EInvalidOperation и нативной диагностикой вместо правдоподобного мусора позже
«Плюс два» растут из раскладки классов. Класс 0 — CTC-пустой, классы 1..N — строки словаря в файловом порядке, а финальный класс — пробел. Некоторые словари несут и собственную запись-пробел, и эту строку надо оставить ровно как есть. Именно здесь добрый Trim наносит реальный ущерб: он превращает запись из одиночного пробела в пустую строку и сдвигает или ломает таблицу. Единственная безопасная нормализация — снять завершающий возврат каретки, так что словарь, сохранённый с окончаниями CRLF, грузится корректно, тогда как BOM в UTF-8, пустая строка или запись с табом отвергаются. Набросок ниже показывает раскладку на Pascal; это пояснительный код, а не API HotPDF
// Только иллюстрация: таблица классов, которую ждёт 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-пустой
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-декодирование берёт класс с высшей оценкой на каждом временном шаге, схлопывает подряд идущие повторы в один символ и выбрасывает пустой класс; пустой — то, что позволяет по-настоящему удвоенным буквам выживать. Модель распознавания смотрит на строку текста как на последовательность узких вертикальных срезов и для каждого среза, или временного шага, выдаёт вероятность по каждому классу. Строка с AA中 может дать argmax-последовательность A A blank A 中 space. Схлопывание первых двух шагов A даёт одну A, пустой отделяет её от следующей A, и результат — AA中 с нетронутым хвостовым пробелом. Без правила пустого book и bok были бы неразличимы
Поскольку декодер — всего дюжина строк, границы легко испортить, а отказы безмолвны. Если внутренний argmax-цикл остановится за один класс до конца, класс пробела никогда не выиграет, и каждая строка вернётся без пробелов между словами, что убивает фразовый поиск на английских и латинских страницах. Если внешний цикл остановится за один временный шаг до конца, у каждой строки исчезнет последний символ, что для короткой строки может быть третья текста. А если сторож повторов не сбрасывается пустым, удвоенные символы вроде ll или китайские редупликации вроде 谢谢 схлопнутся в один. Декодер HotPDF включает последний класс и последний временный шаг, держит повторы, разделённые пустым, и вдобавок отвергает оценки, не являющиеся конечными или выходящие за 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-пустой
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; // пустой сбрасывает сторож повторов
end;
end;
Жадное декодирование — не самая точная из доступных CTC-стратегий; beam search с языковой моделью может вытянуть часть двусмысленных срезов. Для печатных документов при 300 DPI жадный результат — обычно всё, что может предложить модель, и декодер не то место, где стоит компенсировать слабости модели. Латинская модель PP-OCRv3, например, может прочесть ñ как n даже на чистом вводе. HotPDF не замазывает это пост-обработкой с заменами символов: таблица подстановок, чинящая испанский, ломает что-то другое, а неверный символ в searchable-слое хуже честного промаха
Как HotPDF упорядочивает строки текста, включая right-to-left арабский?
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 для крупных форматов. Right-to-left упорядочение пользуется опциональным экспортом HPDFRapidOCRSetReadingDirection ABI версии 1; адаптер требует его, только когда выставлен RightToLeft, так что DLL постарше продолжает обслуживать языки слева направо и падает при создании движка с EArgumentException, называющей отсутствующий экспорт для арабского
Почему новые OCR-модели не грузятся?
RapidOCR DLL от HotPDF линкует статический ONNX Runtime 1.14, который не читает модели, сохранённые с ONNX IR версии 10, а свежие экспорты вроде моделей PP-OCRv5 могут требовать рантайм поновее; такая модель падает при создании движка с нативной диагностикой. Именно это ограничение — причина, почему языковые пакеты приколоты к конкретным парам распознаватель-словарь PP-OCRv3 и PP-OCRv4, а не к «последним», и почему таблица выше мешает оба поколения: каждая приколотая пара — та, что грузится и верифицируется под этим рантаймом
Инсталлер следит за парностью. Каждый файл манифеста несёт хэш SHA256, существующий файл с другим хэшем останавливает установку вместо перезаписи, и каждая загрузка приземляется под временным именем и переезжает на место лишь после совпадения хэша. Это защита от тихой версии словарной проблемы: кто-то вручную подкладывает свежий recognition.onnx в папку профиля, число классов случайно совпадает, и ничего не падает, пока клиент не отчитает, что поиск не находит слова, которые он воочию видит. В рантайме адаптер остаётся офлайн и никогда не дотягивает отсутствующую модель. Распознаватель заодно валидирует форму модели при загрузке, принимая NCHW-вход с фиксированной высотой 32 или 48 пикселей или динамической, которую гоняет на 48
Если вам нужна письменность, которую не покрывает ни один из девяти профилей, можно по-прежнему навести RecognitionModel и CharacterDictionary на собственные файлы. Те же проверки применяются — в этом и смысл: непарная пара падает при инициализации, а не в архиве вашего клиента. Для страниц, где не подходит ни один профиль RapidOCR, адаптер Tesseract для searchable PDF встаёт в тот же вызов ApplyLoadedOCRTextLayer, а для машинопечатных ASCII-форм встроенному OCR-движку на сопоставлении шаблонов модели не нужны вовсе
Шпаргалка: чек-лист мультиязычного RapidOCR
- Создавайте опции через
THPDFRapidOCRDLLOptions.ForLanguageи трактуйтеEArgumentExceptionкак неподдерживаемый тег, а не рантайм-аварию - Меняйте
RecognitionModelиCharacterDictionaryвместе, никогда по одному; равные числа классов не доказывают равный порядок символов - Держите словари в UTF-8 без BOM, никогда не тримте записи и ждите от модели N + 2 классов: пустой, N записей, пробел
- Кастомный CTC-декодер обязан накрывать последний класс и последний временной шаг и держать повторы, разделённые пустым
- Используйте один движок на языковой профиль и передавайте явные списки страниц для документов со смешанными письменностями
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 для мультиязычных профилей