HotPDF 通过原生 RapidOCR DLL 适配器在 Delphi 里做中文与多语言 OCR:THPDFRapidOCRDLLOptions.ForLanguage 把 'zh-CN'、'zh-TW'、'ru' 或 'ar' 这类语言标签映射到配套的识别模型与字符字典,THotPDF.ApplyLoadedOCRTextLayer 再把识别出的行变成扫描 PDF 页面上一层不可见、可搜索的 Unicode 文本
让拉丁字母的 demo 跑起来是容易的部分。有意思的失败从你切到繁体中文或俄语开始:输出变成一本正经、格式工整的胡话;或者每一行都无声丢掉最后一个字符;又或者阿拉伯语页面的文本框顺序不对。这些情形没有哪个会自己抛异常。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 期望的根文件名上。之后,一份简体中文扫描件几行代码就能变得可搜索。引擎的管线就是 进程内 RapidOCR DLL 及其 ABI 边界一文描述的同一个 IHPDFOCREngine 接缝,所以本文专注于语言
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 报告文本层为字体和 ToUnicode 表映射了多少个不同 Unicode 标量——一个不错的健全性检查,确认到手的真是 CJK 文本,而不是几个拉丁回退字符。引擎接口要跨文档保持存活,因为模型初始化发生在工厂里,是最贵的一步
为什么只换识别模型会产出垃圾?
CTC 识别模型从不输出字符,只输出类索引,把索引 1,204 变成一个字形的全靠字典。把 ch/recognition.onnx 换成 cyrillic/recognition.onnx 却留着中文字典,模型会高高兴兴发出合法的西里尔索引,旧字典把它们译成随机的汉字。结果看着像文本,通过 UTF-8 校验,可搜索的东西精确为零。所以 ForLanguage 总是同时设置 RecognitionModel 和 CharacterDictionary,手工构建的选项也绝不该只改一个
直觉上的安全检查——字典大小与模型输出宽度比对——必要但不充分。两本字典可以条目数相同而顺序不同,顺序上差一,每个字符就错位一个码点。所以工厂初始化模型时 HotPDF 分两阶段检查。第一,输出类数必须等于字典条目数加二。第二,ONNX 文件若内嵌 character 元数据列表,每个字典条目都与它按序比对,不匹配就以 EInvalidOperation 加原生诊断初始化失败,而不是晚些时候产出像模像样的垃圾
「加二」来自类布局。类 0 是 CTC blank,类 1 到 N 是按文件顺序的字典行,最后一个类是空格。有些字典还自带自己的空格条目,那一行必须原样保留。好心的 Trim 正是在这里闯祸:它把单个空格条目变成空字符串,挪动甚至弄坏整张表。唯一安全的归一化是去掉行尾的回车符,这样 CRLF 行尾保存的字典能正确加载;而 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 循环少跑一个类,space 类永远赢不了,每行回来都没有词间距——英语和拉丁页面的短语搜索就此报废。外层循环少跑一个时间步,每行的最后一个字符消失,短行上那就是三分之一的文本。重复守卫不被 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; // 300 DPI 下 A3 页面的余量
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 像素、约合 1,740 万,请求会以超预算被拒。大格式要么调高 MaxPixels(上限 67,108,864),要么调低 THPDFOCRTextLayerOptions.DPI。从右到左排序用的是 ABI 版本 1 的可选导出 HPDFRapidOCRSetReadingDirection;适配器只在设了 RightToLeft 时才要求它,所以旧 DLL 照样服务从左到右的语言,而阿拉伯语在引擎创建时就以 EArgumentException 失败、指名缺失的导出
为什么新的 OCR 模型加载失败?
HotPDF RapidOCR DLL 静态链接 ONNX Runtime 1.14,读不了以 ONNX IR 版本 10 保存的模型,而 PP-OCRv5 这类新导出可能要求比那更新的运行时;这类模型在引擎创建时带着原生诊断失败。正是这个约束让语言包钉死在特定的 PP-OCRv3 与 PP-OCRv4 识别器加字典对上,而不是「最新」,也是上表两代混排的原因:每个钉死的对都能在那个运行时下加载并通过验证
安装器强制配对。清单里每个文件带 SHA256 哈希,已有文件哈希不同就中止安装而不是覆盖,每个下载先落临时名,哈希匹配后才挪到正式位置。这防的是字典问题的安静版本:有人手工往配置文件夹里丢了个新的 recognition.onnx,类数恰好对上,于是直到客户报告「看得见的词搜不到」之前什么都不会失败。运行时适配器保持离线,从不抓取缺失的模型。识别器还在加载时验证模型形状,接受固定高度 32 或 48 像素或动态高度的 NCHW 输入,动态高度按 48 跑
九个配置都盖不住的文字种类,你仍可以把 RecognitionModel 和 CharacterDictionary 指到自己的文件。同样的检查照样适用——这正是意义所在:不匹配的对在初始化时失败,而不是在你客户的档案里。两个 RapidOCR 配置都不合适的页面,面向可搜索 PDF 的 Tesseract 适配器接进同一个 ApplyLoadedOCRTextLayer 调用;机器印刷的 ASCII 表单,内置模板匹配 OCR 引擎连模型都不需要
速查:多语言 RapidOCR 检查清单
- 用
THPDFRapidOCRDLLOptions.ForLanguage创建选项,把EArgumentException当作不支持的标签,而不是运行时故障 RecognitionModel和CharacterDictionary一起换,绝不单换;类数相等证明不了字符顺序相等- 字典保持无 BOM 的 UTF-8,绝不 trim 条目,模型应有 N + 2 个类:blank、N 个条目、空格
- 自定义 CTC 解码器必须覆盖最后一个类和最后一个时间步,并保留被 blank 分隔的重复
- 每个语言配置一个引擎,混合文字种类的文档传显式页面列表
RightToLeft只改框的顺序;识别出的文本保持 Unicode 逻辑序- 用
Install-RapidOCRModels.ps1装模型,让 SHA256 钉住模型与字典的配对;用-SkipClassifier安装的话设UseAngleClassifier := False - 300 DPI 跑 A3 或更大页面前,把
MaxPixels调到默认值 16,777,216 之上
RapidOCR 语言预设、原生 DLL 适配器和 OCR 文本层流水线都是 HotPDF Delphi PDF Component 的一部分,面向 Delphi、C++Builder 与 Windows FPC/Lazarus,多语言配置自 v2.775.0 起提供