HotPDF 讓 Tesseract 跑在您的 Delphi 行程裡,靠的是 v2.772.0 新增的工廠函式 HPDFCreateTesseractDLLOCREngine:動態載入 Tesseract 5 相容的 DLL,驅動它的 C API(TessBaseAPIInit2、TessBaseAPIRecognize 與結果迭代器),回傳一個 IHPDFOCREngine。THotPDF.ApplyLoadedOCRTextLayer 用這個引擎,為掃描 PDF 頁面加上隱形、可搜尋的 Unicode 文字層
同一個辨識器先前就能透過寫 BMP、解析 TSV 的外接 tesseract.exe 配接器用到。那條路能用,但每一頁都要付行程啟動、暫存點陣圖檔,以及一種沒有 baseline、也管不了頁面分割的文字格式。改呼叫 DLL,三筆帳一次清掉。但它同時也拆掉了行程那道牆——意味著 Pascal 繫結直接坐在 C 結構、C 布林值與 C 配置的字串上。這個配接器最值得知道的事,多半是這份繫結會在哪裡悄悄出錯
怎麼用 HotPDF 從 Delphi 行程內執行 Tesseract?
用 HotPDF 在行程內跑 Tesseract,只需要 HPDFTesseractRecognition 單元裡的一次工廠呼叫,加上每種 HotPDF OCR 引擎都在用的 ApplyLoadedOCRTextLayer。工廠函式驗證從早。DLL 檔與 tessdata 目錄必須存在;語言識別碼只能含 ASCII 字母、數字、_ 與 +;chi_sim+eng 這種組合裡的每個模型都必須有對應的 .traineddata 檔;全部 21 個必要 export 都解析得到,引擎才會回傳。設定錯誤丟 EArgumentException;載入失敗的 DLL 丟 EOSError,帶 Windows 錯誤碼與「檢查架構與相依」的提示
uses
SysUtils, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// Win64 應用程式需要 64 位元 DLL;相依 DLL 放在它旁邊
Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
'C:\OCR\tessdata', 'chi_sim+eng'); // THPDFTesseractOptions.Default
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,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
THPDFTesseractOptions.Default 把 PageSegMode 設為 tpsAuto、EngineMode 設為 temDefault、TimeoutMilliseconds 設為 60,000、MaxPixels 設為 16,777,216。像素預算比看起來更要緊。US Letter 頁面在預設 300 DPI 下渲染出 2,550 × 3,300 像素、約 840 萬,裝得下。同一頁開 600 DPI 就是 5,100 × 6,600、約 3,370 萬,配接器在 Tesseract 見到任何像素之前就拒收。調高 MaxPixels(上限 67,108,864),或把 DPI 維持原位;每個邊還另外以上限 32,767 像素為限
DLL 以 LoadLibraryEx 載入,搜尋旗標涵蓋 DLL 自身資料夾加預設安全目錄,所以 Tesseract 相依的影像程式庫可以擺在它旁邊,不必動 PATH 或目前目錄。HotPDF 不綑綁也不下載任何 OCR 執行環境或模型;兩者都由您備妥
跟 tesseract.exe 配接器相比,變的是什麼?
DLL 配接器拿行程隔離,換更豐富的輸出與更低的每頁開銷。兩種配接器插進同一條文字層管線,座標映射、信心值過濾與全有或全無的提交都一樣;差的是像素怎麼進去、詞怎麼出來
| 面向 | tesseract.exe 配接器 | Tesseract DLL 配接器 |
|---|---|---|
| 工廠函式 | HPDFCreateTesseractOCREngine | HPDFCreateTesseractDLLOCREngine |
| 像素進入 | 私有暫存目錄裡的 BMP 檔 | 記憶體中的 8-bit 灰階緩衝區 |
| 詞輸出 | 詞級 TSV,上限 64 MiB | 結果迭代器,每詞 UTF-8 |
| 基線 | 無法取得 | 從 TessPageIteratorBaseline 直接傳出 |
| 頁面分割與引擎模式 | 僅自動分割 | THPDFTesseractPageSegMode、THPDFTesseractEngineMode |
| 逾時 | 硬性:子行程被終止 | 合作式:Tesseract 必須自己注意到 |
| 崩潰與記憶體隔離 | 獨立行程 | 無,共享您的位址空間 |
有一筆成本不會消失。每次 Recognize 呼叫都建立自己的 API 實例、呼叫 TessBaseAPIInit2,語言模型因此是每頁初始化一次、而不是每顆引擎一次。作業系統的檔案快取會緩和重新載入,但對龐大的多語言模型集,它仍是每頁主要的一次性成本,而且計入辨識期限。行程內 RapidOCR DLL 引擎走相反的設計,ONNX 模型在引擎壽命內常駐;邊界問題(C ABI、借用的緩衝區、不可中斷的原生工作)則是同一家人
為什麼 Delphi 不能照抄 Tesseract 的 monitor 結構?
Delphi 無法安全地鏡射 Tesseract 的進度監視器,因為 ETEXT_DESC 含有隨版本而異的內部欄位,手工照抄的記錄在某些建置上會把取消回呼與期限放到錯的偏移。出事時沒有任何東西大聲失敗。Tesseract 只是從一個如今裝著別的東西的欄位讀您的回呼指標,或者根本看不到期限
所以 HotPDF 把 monitor 當不透明指標,只經匯出函式觸碰:TessMonitorCreate、TessMonitorSetCancelThis、TessMonitorSetCancelFunc、TessMonitorSetDeadlineMSecs 與 TessMonitorDelete。您若為別的目的自己繫結這個 C API,同一套模式照用。下面的素描是您自己的繫結程式碼、不是 HotPDF API,宣告則與 HotPDF 內部所用一致
type
// C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
TTessMonitorCreate = function: Pointer; cdecl; // ETEXT_DESC*,永不解參照
TTessMonitorDelete = procedure(Monitor: Pointer); cdecl;
TTessMonitorSetCancelFunc = procedure(Monitor: Pointer; Func: TTessCancelFunc); cdecl;
TTessMonitorSetCancelThis = procedure(Monitor, CancelThis: Pointer); cdecl;
TTessMonitorSetDeadlineMSecs = procedure(Monitor: Pointer; MSecs: Integer); cdecl;
TTessBaseAPIRecognize = function(Handle, Monitor: Pointer): Integer; cdecl;
TOCRJob = record
CancelRequested: Boolean;
DeadlineTick: UInt64;
end;
POCRJob = ^TOCRJob;
function ShouldCancel(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
begin
// 跑在 Tesseract 的堆疊上:只讀旗標與時鐘,絕不丟例外
Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
(GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;
// 用法,函式指標經 GetProcAddress 解析:
// Monitor := MonitorCreate();
// try
// MonitorSetCancelThis(Monitor, @Job);
// MonitorSetCancelFunc(Monitor, ShouldCancel);
// MonitorSetDeadlineMSecs(Monitor, RemainingMs);
// RC := BaseAPIRecognize(API, Monitor);
// finally
// MonitorDelete(Monitor);
// end;
素描裡有兩個細節是刻意的。回呼回傳 Boolean——在 Delphi 與 Free Pascal 裡都是一個位元組,正好對上 TessCancelFunc 裡的 C bool。四個位元組的 Windows BOOL 或 Delphi LongBool 看起來可以互換、其實不行:一邊寫一個位元組、另一邊讀四個時,回傳暫存器的高位元組是殘留的雜訊,false 可能以 true 的姿態抵達。同一個標頭檔還添了亂:TessPageIteratorBoundingBox 這類函式回傳 int,HotPDF 宣告成 Integer。每個回傳值都照 C 型別讀,別假設整個 API 是同一套慣例
第二個細節是回呼絕不丟例外。Delphi 例外穿過 Tesseract 的 C++ 堆疊框展開,是未定義行為,所以 HotPDF 的回呼只讀取消權杖與單調遞增的 GetTickCount64 值。TessBaseAPIRecognize 返回之後,配接器把結果轉成取消或逾時的診斷,而且不管原生回傳碼為何都會執行這項檢查
Delphi 這一側擁有哪些原生指標?
HotPDF 的 Tesseract DLL 配接器每個請求擁有三個原生物件——API 實例、monitor 與結果迭代器——其餘全是借的。每次 Recognize 呼叫建立自己的一套,並在 finally 區塊裡釋放:先 TessResultIteratorDelete、再 TessMonitorDelete、最後 TessBaseAPIDelete。釋放引擎介面即卸載程式庫
TessResultIteratorGetPageIterator回傳的是指進結果迭代器的借用視圖、不是新物件。HotPDF 用它跑TessPageIteratorBoundingBox與TessPageIteratorBaseline、從不釋放;單獨刪它會把同一塊記憶體釋放兩次TessResultIteratorGetUTF8Text回傳的字串由 DLL 自己的執行期配置。HotPDF 複製一份、在finally區塊裡經TessDeleteText歸還;Pascal 的FreeMem會把它還到錯的堆積上- 詞的文字以嚴格的 UTF-8 驗證解碼、轉換前先查長度。含控制字元、畸形 UTF-8、框跑出影像、矩形反轉、信心值落在 0–100 之外的詞,讓請求失敗,而不是悄悄修補
- 每次請求的文字總量以上限 1,048,576 個 UTF-16 code unit 為頂,詞數也必須裝得進
ApplyLoadedOCRTextLayer下發的請求預算
信心值以 0–100 抵達、縮放到 0–1,所以 THPDFOCRTextLayerOptions.MinimumConfidence 對每種引擎都是同一個意思。Tesseract 有報 baseline 時,兩個端點直接傳出;否則文字層管線後備到幾何估計——跟 TSV 輸入的做法一模一樣
為什麼 enum 要在抵達 DLL 之前驗證?
HotPDF 在範圍檢查之前,先把 PageSegMode 與 EngineMode 的原始序數複製進 Integer,因為編譯器可能假設 enum 變數永遠持有宣告過的值、把 Ord(X) > Ord(High(T)) 摺疊成常數 false。序數不是裝飾:THPDFTesseractPageSegMode 跟著 Tesseract 的頁面分割編號 0 到 13,THPDFTesseractEngineMode 跟著引擎模式編號 0 到 3,兩者都以普通整數送進 DLL。用 FillChar 清出、從串流填入、或從 C++Builder 帶著轉型整數傳來的選項記錄,都可能裝著 200 這樣的位元組。驗證複製出來的序數,讓它變成工廠階段的 EArgumentException,而不是原生程式碼裡的未定義模式。工廠函式另外拒收不產詞的 tpsOSDOnly 與 tpsAutoOnly,並要求 tpsAutoOSD 與 tpsSparseTextOSD 備妥 osd.traineddata
辨識逾時實際上保證什麼?
Tesseract DLL 的逾時是合作式的:HotPDF 能停自己的工作、請 Tesseract 停手,但沒辦法強迫原生程式碼返回。時鐘從 Recognize 開跑那一刻起算,所以點陣圖轉換與模型初始化跟辨識吃同一份預算。HotPDF 在灰階轉換期間、迭代結果時的詞與詞之間,檢查已耗時間與取消權杖,並在呼叫 TessBaseAPIRecognize 之前把剩餘毫秒數交給 TessMonitorSetDeadlineMSecs
缺口在原生呼叫的內部。Tesseract 的 monitor 在詞辨識期間被徵詢,TessBaseAPIInit2 或頁面版面分析期間沒有,所以慢的模型載入或病態的版面,可能在逾時被回報之前就跑過期限。像素與輸出預算也管不到原生程式庫自己的記憶體用量。需要殺得掉的 worker,就用行程配接器;這是誠實的取捨,不是缺功能
頁面分割是 DLL 配接器在難纏輸入上掙回本錢的地方。欄位四散的表單、標籤與掃描表格,用 tpsSparseText 往往比自動分割認得更好——後者會努力拼湊出不存在的欄與段
procedure OCRFormPages(Doc: THotPDF; const Pages: array of Integer);
var
Engine: IHPDFOCREngine;
TessOptions: THPDFTesseractOptions;
LayerOptions: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
TessOptions := THPDFTesseractOptions.Default;
TessOptions.PageSegMode := tpsSparseText; // 欄位四散,不拼欄
TessOptions.EngineMode := temLSTMOnly; // tessdata 裡要有 LSTM 模型
TessOptions.TimeoutMilliseconds := 20000; // 含模型初始化
Engine := HPDFCreateTesseractDLLOCREngine('C:\OCR\Win64\libtesseract-5.dll',
'C:\OCR\tessdata', 'eng+deu', TessOptions);
LayerOptions := THPDFOCRTextLayerOptions.Default;
LayerOptions.MinimumConfidence := 0.6;
if not Doc.ApplyLoadedOCRTextLayer(Pages, Engine, LayerOptions, Info) then
case Info.Status of
otlsCancelled:
Writeln('OCR cancelled, document unchanged');
otlsEngineError:
Writeln('Tesseract failed or timed out: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
end;
逾時以 otlsEngineError 浮現、診斷為 Tesseract DLL OCR timed out;取消的權杖則浮現為 otlsCancelled。兩種情況下,ApplyLoadedOCRTextLayer 都是在啟動提交交易之前,就把每個選中的頁面辨識完畢,所以 50 頁裡第 40 頁失敗,已載入的文件原封不動。注意 tpsSingleLine、tpsSingleBlock 與 tpsSparseText 只改分割;沒有任何一個能把歪斜的掃描扶正
Free Pascal 與 Lazarus:過期像素與消失的中文
經過兩個 FPC 專屬修正,v2.772.1 起兩個 Tesseract 工廠函式在 Windows 的 Free Pascal 與 Lazarus Win32/Win64 建置上都能用。先為目標架構重建 Lazarus 套件;整體移植的部分見 HotPDF 與 Free Pascal/Lazarus Win64
第一個修正關於像素。經 scanline 寫入的 LCL TBitmap 可能更新了原始影像、卻沒重整 Windows 點陣圖 handle,對那個 handle 跑 GetDIBits 拿回的是舊像素。症狀相當謎:直接畫到點陣圖上的文字認得,HotPDF 的 PDF 渲染器渲染出的頁面卻回傳空的詞清單。FPC 上配接器現在經 CreateIntfImage 讀取格式感知的快照,尊重原始影像的像素格式與列序。Delphi 建置維持在私有 24-bit 副本上走 GetDIBits。兩種建置都不修改呼叫方的點陣圖
第二個修正屬於 tesseract.exe 配接器。FPC 的 TStringList 存的是 ANSI 字串,把解碼後的 UTF-8 TSV 文字指派給 Lines.Text,系統 ANSI 字碼頁表示不了的中文或增補平面字元就悄悄消失。FPC 路徑現在把 TSV 按 UTF-8 位元組保留、在位元組層級剝掉 BOM、逐詞解碼成 UnicodeString。DLL 配接器從來沒這毛病,因為它直接從迭代器逐詞解碼
速查
- 工廠函式:
HPDFTesseractRecognition裡的HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]),v2.772.0 新增,FPC 支援自 v2.772.1 - 預設:
tpsAuto、temDefault、60,000 ms、16,777,216 像素;逾時範圍 1–3,600,000 ms,像素上限 67,108,864 - DLL 位元數對上應用程式,相依 DLL 放在 Tesseract DLL 旁邊
- monitor 當不透明對待;絕不把
ETEXT_DESC抄進 Pascal 記錄 - 取消回呼宣告成
cdecl、結果是一個位元組的Boolean,並讓例外絕不逃出它 - 迭代器的文字用
TessDeleteText釋放;從結果迭代器取得的 page iterator 絕不釋放 - 預期期限是合作式的:模型初始化與版面分析可能跑過頭
- 需要硬性終止或崩潰隔離時,用 tesseract.exe 配接器
Tesseract DLL 配接器、各種行程配接器與內建 OCR 引擎,都隨 HotPDF Delphi PDF 元件出貨,支援 Delphi、C++Builder 與 Free Pascal;版本與下載見 HotPDF 產品頁