HotPDF 在 Delphi 裡做中文與多語言 OCR,靠的是它的原生 RapidOCR DLL 配接器:THPDFRapidOCRDLLOptions.ForLanguage 把 'zh-CN'、'zh-TW'、'ru' 或 'ar' 這類語言標籤映射到一套匹配的辨識模型與字元字典,THotPDF.ApplyLoadedOCRTextLayer 再把辨識出的行變成掃描 PDF 頁面上隱形、可搜尋的 Unicode 文字層
讓拉丁字母的示範跑起來是簡單的部分。有趣的故障從您切到繁體中文或俄文開始:輸出變成一串言之鑿鑿、格式工整的胡言亂語;或每一行悄悄丟掉最後一個字元;或阿拉伯文頁面回來時文字框順序不對。這些都不會自己丟例外。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 純量——一個很好的 sanity check,確認送進來的真是 CJK 文字、而不是幾個拉丁後備字元。引擎介面要跨文件握著不放,因為模型初始化發生在工廠函式裡,是最貴的那一步
只換辨識模型為什麼會生出亂碼?
CTC 辨識模型從不輸出字元,只輸出類別索引,把索引 1,204 變成字形的唯一東西是字典。把 ch/recognition.onnx 換成 cyrillic/recognition.onnx、卻留著中文字典,模型會高高興興吐出合法的西里爾索引,舊字典把它們譯成隨機的漢字。結果看起來像文字、過得了 UTF-8 驗證,可搜尋到的東西恰好是零。這就是 ForLanguage 永遠一起設定 RecognitionModel 與 CharacterDictionary 的原因,也是手工組選項時絕不該只改其一的原因
最直覺的安全檢查——比對字典大小與模型輸出寬度——必要但不充分。兩份字典可以有相同的條目數、卻是不同的順序,順序差一位,每個字元就平移一個 code point。所以工廠函式初始化模型時分兩階段檢查。第一,輸出類別數必須等於字典條目數加二。第二,ONNX 檔若內嵌 character 中繼資料清單,每個字典條目都按順序跟它比對,不吻合就讓初始化以 EInvalidOperation 加原生診斷失敗,而不是晚點產出像模像樣的亂碼
「加二」來自類別版面。類別 0 是 CTC blank,類別 1 到 N 是按檔案順序排列的字典行,最後一個類別是空格。有些字典自己還帶一個空格條目,那一行必須原樣保留。Trim 的好意在這裡造成真傷害:它把單一空格條目變成空字串,表格隨之平移或損毀。唯一安全的正規化是移掉行尾的回車字元,所以以 CRLF 行尾存的字典載得進來;UTF-8 位元組順序記號、空行、含 tab 的條目則一律拒收。下面用 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;
Greedy CTC 解碼到底在做什麼?
Greedy 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 素描如下
// 僅供解說:邊界正確的 greedy 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;
Greedy 解碼不是現有最準的 CTC 策略;帶語言模型的 beam search 能救回一些曖昧的切片。對 300 DPI 的印刷文件,greedy 結果通常就是模型能給的全部,解碼器也不是補模型短板的地方。舉例來說,拉丁 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 像素,300 DPI 的 A4 與 US Letter 綽綽有餘,但 300 DPI 的 A3 頁面約 3508 × 4961 像素、差不多 1,740 萬,請求會因超過預算被拒。大格式請調高 MaxPixels(上限 67,108,864)或調低 THPDFOCRTextLayerOptions.DPI。由右至左排序用的是 ABI 版本 1 選用的 HPDFRapidOCRSetReadingDirection export;配接器只在設了 RightToLeft 時才要求它,所以舊 DLL 照樣服務由左至右的語言,阿拉伯文則在建立引擎時以 EArgumentException 失敗、指名缺了哪個 export
為什麼比較新的 OCR 模型載不進來?
HotPDF 的 RapidOCR DLL 靜態連結 ONNX Runtime 1.14,讀不了以 ONNX IR 版本 10 存的模型,而 PP-OCRv5 模型這類新匯出可能要求比這更新的 runtime;這種模型會在建立引擎時以原生診斷失敗。語言包之所以釘在特定的 PP-OCRv3 與 PP-OCRv4 辨識器與字典配對上、而不是追「最新」,這條限制正是原因;上面那張表混著兩個世代也是:每組釘死的配對都能在那個 runtime 下載入並通過驗證
安裝腳本強制執行這個配對。它清單裡的每個檔案都帶 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 起