HotPDF 用行程內 RapidOCR 讓掃描 PDF 頁面變得可搜尋,靠的是 v2.774.0 新增的工廠函式 HPDFCreateRapidOCRDLLOCREngine:它載入 HotPDFRapidOCR.dll,讓 ONNX 的偵測、角度分類與辨識模型常駐記憶體,回傳一個 IHPDFOCREngine。把這個引擎交給 THotPDF.ApplyLoadedOCRTextLayer,它就渲染每一頁、不用 Python 也不開子行程地跑 CPU 推論,然後寫入一層隱形的 Unicode 文字層
動機是每頁成本。早先出貨的 RapidOCR 行程配接器 HPDFCreateRapidOCREngine,每次 Recognize 都要起一個 Python worker,而那個 worker 在讀到第一個像素之前,得先匯入執行環境、載入 ONNX 模型。500 頁的檔案庫,這筆啟動稅就交 500 次,部署也變成要在 Delphi 執行檔旁邊擺一套 Python 環境。原生 DLL 在您建立引擎時把模型載一次,部署縮成 DLL、模型檔與一份字元字典。換來的代價是放棄「把卡死的辨識器殺掉」的能力,而這個配接器的工程工夫,大半都花在誠實地與這件事共處
怎麼用 RapidOCR DLL 把掃描 PDF 變成可搜尋?
用原生 RapidOCR DLL 做出可搜尋 PDF,只要一次工廠呼叫,加上每種 HotPDF OCR 引擎都在用的那個 ApplyLoadedOCRTextLayer。工廠函式住在 HPDFRapidOCRRecognition 單元裡,而且驗證從嚴、寧早勿晚:DLL 與模型目錄必須存在、每個模型與字典檔必須解析得到、ABI 版本必須是 1、所有必要的 export 必須齊備,然後才輪到任何模型初始化。設定錯誤丟 EArgumentException;模型載入失敗丟 EInvalidOperation,帶著 DLL 寫下的診斷文字
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// 模型在這裡載入,不在任何辨識期限的壓力之下。
// THPDFRapidOCRDLLOptions.Default 裡的相對模型名稱
// 以模型目錄為基準解析。
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models');
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Options := THPDFOCRTextLayerOptions.Default; // 300 DPI、MinimumConfidence 0.5
// 空頁面清單代表每一頁;已有文字的頁面會跳過
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
raise Exception.Create(string(Info.Diagnostic));
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' lines accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
THPDFRapidOCRDLLOptions.Default 指名 ch_PP-OCRv3_det_infer.onnx、ch_PP-OCRv3_rec_infer.onnx、ch_ppocr_mobile_v2.0_cls_infer.onnx 與 ppocr_keys_v1.txt,單一 CPU 執行緒、1,600 萬像素輸入上限、60,000 ms 辨識期限。從 v2.775.0 起,THPDFRapidOCRDLLOptions.ForLanguage 可以換進繁體中文、俄文、日文、阿拉伯文等其他語言設定檔的辨識模型與字典;模型與字典為什麼必須一起換,見 HotPDF 裡的 RapidOCR 多語言模型與 CTC 字典。引擎在 Info.EngineName 裡自報家門為 RapidOCR (native DLL),跟 外接 Tesseract OCR 行程配接器、內建範本比對 OCR 引擎並列時,日誌不會含糊
為什麼這個 C ABI 只講 int32_t 與 UTF-8 位元組?
HotPDFRapidOCR.dll 的 ABI 只用固定寬度整數、裸指標與明確的位元組長度,因為 Delphi、C++Builder 與 Free Pascal 跟 MSVC 之間,除了 C 呼叫慣例之外什麼都不共用。std::string、std::vector 或 C++ 例外的記憶體版面與堆疊展開模型,屬於某一個編譯器與某一個執行期程式庫。讓任何一個越過邊界,下場就是堆疊損壞、或堆積區塊被錯的配置器釋放——而不是乾淨的錯誤
所以 ABI 版本 1 遵守一份很短的清單。每個 export 都是 cdecl、回傳 int32_t 狀態碼,1 是成功、0 是失敗。每個可能失敗的函式都收一個歸呼叫方所有的診斷緩衝區與其位元組容量;DLL 寫入 NUL 結尾的 UTF-8 訊息、超長就截斷塞滿,配接器解碼時在自己的 4,096 位元組緩衝區最後一個位元組硬放終結符。每個 export 本體都包在 try 裡,同時掛 catch (const std::exception &) 與 catch (...),於是 ONNX Runtime 錯誤、OpenCV 斷言或壞字典都變成狀態 0 加文字,絕不會有例外逃進 Pascal 程式碼
| Export | 角色 | 配接器何時解析它 |
|---|---|---|
HPDFRapidOCRAbiVersion | 回傳 1;其他任何值都拒收 | 最先,先於一切 |
HPDFRapidOCRCreate | 載入偵測、選用的分類、辨識模型與字典 | 在工廠函式裡 |
HPDFRapidOCRRecognize | 跑一張點陣圖,每行文字發一次回呼 | 在工廠函式裡 |
HPDFRapidOCRDestroy | 釋放模型實例 | 在工廠函式裡 |
HPDFRapidOCRSetReadingDirection | 選用的由右至左列序,v2.775.0 新增 | 只有設了 RightToLeft 時 |
那個選用 export 刻意延後解析:缺了它的 v2.774.0 DLL 照樣服務由左至右的請求。DLL 以 LoadLibraryEx 載入,搜尋旗標涵蓋 DLL 自身資料夾加預設安全目錄,所以擺在 HotPDFRapidOCR.dll 旁的 ONNX Runtime 或 OpenCV 相依不必動 PATH 就找得到。模型與字典路徑以 UTF-8 傳遞,DLL 在經寬字元 API 開檔之前,先用嚴格模式的 MultiByteToWideChar 轉換,所以中文或西里爾使用者名稱底下的模型目錄照樣可用,而不是被逐位元組拉寬成一串亂碼
有一條規則活在建置裡、不在標頭檔裡。DLL 靜態連結 ONNX Runtime 與 OpenCV,預設的 CMake 設定用靜態 release CRT(/MT)。以 /MD 編譯的靜態程式庫混進 /MT 的 DLL,輕則連結錯誤、重則兩座互不相干的堆積,所以備妥的程式庫必須匹配 DLL 用的那種 CRT 模式
從 TBitmap 到一行文字之間發生了什麼?
HotPDF 把渲染頁面的獨立副本——top-down BGR 快照——交給 DLL;DLL 對每行辨識出的文字回呼一次,帶著借用來的 UTF-8 文字,配接器必須在回呼返回前複製走
在 Delphi 上,配接器把頁面點陣圖指派給私有的 TBitmap、強制 pf24bit、用負的 biHeight 走 GetDIBits 讀列——這樣得到的是 top-down、補齊四位元組對齊的列;stride 明確傳遞。在 FPC 上則經 CreateIntfImage 讀,因為 LCL 的 scanline 寫入可能更新了原始影像卻沒重整 GDI handle。呼叫方的點陣圖絕不被修改,而像素預算(MaxPixels,預設 16,777,216、可設定到 67,108,864)與每維 32,767 像素上限,都在快照緩衝區配置之前檢查
DLL 內部,快照先補上 50 個白色像素,文字區域以最長邊 1,024 像素偵測,框被排成水平列,每個裁切區在辨識前可選擇性地由角度分類器旋轉。每一行文字隨後經過一次回呼,回呼收到 const char*、位元組數、以原始影像像素計的整數框,以及字元平均信心值。文字指標只在回呼期間有效,所以配接器立刻複製,而且驗收標準很嚴:
- UTF-8 以
MB_ERR_INVALID_CHARS解碼;畸形序列讓該頁失敗,而不是在可搜尋層裡生出替換字元 - C0 與 C1 控制字元一律拒收,純空白行直接跳過
- 框必須落在點陣圖之內,信心值必須是 0 到 1 之間的有限值
- 文字對著請求的
MaxTextCodeUnits計數,每次呼叫硬上限 1,048,576 個 UTF-16 單位,增補平面字元一個算兩個單位 - 回呼裡丟出的任何 Pascal 例外都會在原地被接住、存起來、轉成回傳 0,DLL 於是停下來回報失敗;存下的訊息隨後成為診斷文字
對調校而言有兩個後果要緊。第一,輸出單位是「行」、不是「詞」:每行吃掉一個 MaxWords 名額,Info.AcceptedWordCount 與 Info.DroppedWordCount 數的是行,搜尋高亮跨的是行的框。第二,MinimumConfidence(預設 0.5)對比的是行的字元平均信心值,所以二十個乾淨字元裡夾一個認不得的,那行通常還活著。DLL 不提供 baseline,文字層管線自己從框裡估一個。空頁面以零行成功,任何失敗都清掉部分結果,多頁提交因此維持全有或全無
模型所有權與執行緒安全
每個 RapidOCR DLL 引擎終其一生恰好擁有一個模型實例,對該引擎的 Recognize 呼叫由 critical section 串行化。握住 IHPDFOCREngine 介面,模型才保持熱著,所以批次工作的正確姿勢是引擎建一次、跨文件重複用
procedure OcrBatch(const Files: TStrings; const OutputDir: string);
var
Models: THPDFRapidOCRDLLOptions;
Engine: IHPDFOCREngine;
Doc: THotPDF;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
I: Integer;
begin
Models := THPDFRapidOCRDLLOptions.Default;
Models.UseAngleClassifier := False; // 正向掃描:不載入分類器模型
Models.Threads := 4; // 1..64,上限為邏輯處理器數
Models.TimeoutMilliseconds := 120000; // 每次 Recognize 呼叫,合作式
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
Options := THPDFOCRTextLayerOptions.Default;
for I := 0 to Files.Count - 1 do
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if (Doc.LoadFromFile(Files[I]) > 0) and
Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
Doc.SaveLoadedDocument(IncludeTrailingPathDelimiter(OutputDir) +
ExtractFileName(Files[I]))
else
Writeln(Files[I], ': ', string(Info.Diagnostic));
finally
Doc.Free;
end;
end;
end; // 最後一個參照釋放:模型銷毀,接著 DLL 卸載
Threads 同時設定每個 ONNX session 的 intra-op 與 inter-op 執行緒數,DLL 會把它夾到作用中處理器數為止。兩個執行緒共用一個引擎並不平行執行;第二個等鎖。那個等待不是閉著眼的 EnterCriticalSection:配接器每 25 ms 呼叫一次 TryEnterCriticalSection,每次嘗試之間檢查取消權杖與期限,所以排隊中的請求照樣可以取消或逾時。真要平行,就每個 worker 一顆引擎,並接受每顆引擎都在記憶體裡自持一份模型
拆除順序由引擎解構子定死:HPDFRapidOCRDestroy 先釋放模型實例,FreeLibrary 隨後卸載 DLL。原生那一側的模型初始化同樣細心:偵測器與分類器 session 都建好之後辨識模型才失敗的話,那些 session 會先釋放再回報錯誤;字典的類別數在初始化時就對著模型輸出檢查,而不是拖到第一頁
原生 OCR 呼叫為什麼無法在推論中途被殺掉?
原生 RapidOCR 呼叫無法在推論中途被殺,因為它跑在您的執行緒上、您的行程裡、一個不接受中斷的 ONNX Runtime session 中間。HotPDF 的 DLL 配接器於是把取消做成合作式:DLL 在偵測前後、分類之後、每行辨識之後呼叫一次中止回呼,在回呼回傳 0 的第一個檢查點停手。已經開跑的那一次 ONNX Run 會先跑完
替代方案比等更糟。TerminateThread 會把 CRT 堆積鎖、ONNX Runtime 的執行緒池與任何 OpenCV 狀態留在它們當下碰巧所在的狀態,毒化行程的其餘部分。呼叫還在執行時 FreeLibrary,等於卸載還在堆疊上的程式碼。兩者都做不安全,所以配接器從不嘗試。TimeoutMilliseconds 裡的期限因此是合作式期限:逾時以引擎錯誤、帶逾時診斷的姿態浮現,取消的權杖則浮現為 otlsCancelled:
// 權杖由呼叫方建立,與 UI 執行緒共用,
// 使用者按下 Stop 時由 UI 執行緒呼叫 Token.Cancel
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
case Info.Status of
otlsCancelled:
// 在下一個階段或行邊界返回;文件不變
Writeln('Cancelled');
otlsEngineError:
// 含合作式期限逾期與原生診斷
Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded:
Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
這就是 HotPDF 行程配接器與行程內 DLL 之間的核心取捨,哪一邊都沒有全勝:
- 啟動成本:Tesseract 與 Python RapidOCR 配接器每頁起一個行程、載一次模型;DLL 每顆引擎只載一次
- 停手:子行程可以直接終止,Python worker 還跑在關閉即殺的 Job Object 裡、整棵行程樹一起走;DLL 只能在階段與行邊界停
- 故障隔離:
tesseract.exe崩潰只賠一頁;DLL 裡的存取違規把您的整個行程帶走 - 部署:行程配接器要一套裝好的程式或 Python 環境;DLL 只要它自己、它的模型與字典,且位元數要跟應用程式匹配
- 記憶體:行程配接器在子行程退出時全部歸零;DLL 引擎的模型常駐到最後一個介面參照釋放
一次 OCR 一頁的互動式桌面應用,DLL 的回應速度通常贏。全天候吞不可信掃描的伺服器,行程邊界值得它的啟動成本
建置與部署 HotPDFRapidOCR.dll
HotPDFRapidOCR.dll 用 MSVC、C++17、一個 Windows SDK 與 CMake 3.20 以上,從 Native/RapidOCR 的 C++ 原始碼建置,輔助腳本吃原生網路來源、ONNX Runtime 與 OpenCV 目錄,外加 Win32 或 Win64 平台。兩種都要出貨就兩種都建,因為 32 位元的 Delphi 應用載不動 64 位元 DLL,而備妥的靜態程式庫也得同時對上目標架構與 CRT 模式
模型那一側有自己的相容性界線。偵測器是 DB 文字偵測器;辨識器收 NCHW 版面、固定輸入高度 32 或 48 的 CTC 模型,動態高度的模型用 48。綁在一起出貨的靜態 ONNX Runtime 載不動以更新 IR 版本存的模型,所以新近的 PP-OCRv5 匯出會帶著診斷文字初始化失敗,而不是載一半。字典必須是無 BOM 的 UTF-8、字元順序與模型完全一致,類別數必須匹配模型輸出;CRLF 行尾可以接受。辨識是離線的:DLL 絕不會下載缺的模型
速查
- 工廠函式:
HPDFRapidOCRRecognition裡的HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options]),v2.774.0 起,Delphi、C++Builder 與 Windows FPC/Lazarus 建置皆可用 - 讓回傳的
IHPDFOCREngine跨頁、跨文件保持存活;釋放它就銷毀模型、卸載 DLL - 一顆引擎一次跑一個辨識;要平行 worker 就建多顆引擎,並為每份模型副本編列記憶體
- 輸出每行文字一個條目、帶字元平均信心值,經
THPDFOCRTextLayerOptions.MinimumConfidence過濾 - 取消與
TimeoutMilliseconds都是合作式;進行中的 ONNX run 一定跑完 - DLL 位元數對上應用程式,靜態 ONNX Runtime 與 OpenCV 程式庫的 CRT 模式對上 DLL
- 每顆引擎用
THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0)選一種語言設定檔;引擎自己不會偵測語言
原生 RapidOCR 配接器、以行程為本的 OCR 配接器、餵它們的頁面渲染器,以及寫隱形 Unicode 文字層的寫入器,全部一起隨 HotPDF——Delphi 與 C++Builder 的原生 VCL PDF 元件——出貨。您的文件擷取或歸檔應用需要在目標機器上不裝 Python 執行環境就能產出可搜尋結果的話,HotPDF Delphi PDF component 提供整條管線,剩下要部署的只有 DLL 與它的模型