HotPDF 用 Tesseract 把掃描的 PDF 頁面變成可搜尋 PDF,入口是 HPDFCreateTesseractOCREngine,這個工廠把本機安裝的 Tesseract 執行檔包成 IHPDFOCREngine。您把引擎交給 ApplyLoadedOCRTextLayer,它渲染每一頁、每頁跑一次 Tesseract、剖析字級 TSV 輸出,然後以單一交易為所有請求頁面提交一層隱形 Unicode 文字層——要嘛全部,要嘛全不
這個轉接器存在的原因是範圍。內建模板比對 OCR 引擎刻意窄:機打的 ASCII 字母與數字,僅此而已。帶重音符姓名的發票、中文合約、多語言檔案庫,需要帶訓練語言模型的真正辨識器,而 Tesseract 是顯而易見的候選,它是命令列程式,可以部署在應用程式旁邊。從文件函式庫裡呼叫外部程式,聽起來輕鬆。並不輕鬆,轉接器裡多數有趣的程式碼,都在處理程式不乖、卡死、被取消、或繼承了不該看見的東西時會發生什麼
HotPDF 怎麼從 Delphi 應用驅動 Tesseract?
HotPDF 讓 Tesseract 以隱藏子行程的方式每頁跑一次,餵它一張渲染點陣圖、收回一份 TSV 檔,並透過內建引擎用的同一個 IHPDFOCREngine 接縫暴露結果。下游什麼都不變:座標映射、旋轉處理、Unicode 驗證、信心度過濾與原子提交,都是您已有的文字層管線。工廠住在 HPDFTesseractRecognition 單元裡,而且驗證得很早:執行檔必須存在,tessdata 目錄必須存在,逾時必須落在 1 到 3,600,000 毫秒之間,語言識別碼只能含 ASCII 字母、數字、_ 與 +。最後那個檢查要緊,因為語言字串最終會落在命令列上,eng+chi_sim 是合法的 Tesseract 值,帶引號或空格的東西都不是
uses
SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;
procedure MakeSearchable(const SourceFile, TargetFile: string;
Token: THPDFCancellationToken);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// 執行檔缺失、tessdata 缺失、語言識別碼不合法、
// 或逾時超出 1..3600000 ms 時,丟 EArgumentException
Engine := HPDFCreateTesseractOCREngine(
'C:\OCR\Tesseract\tesseract.exe',
'C:\OCR\Tesseract\tessdata',
'eng+chi_sim', // 多個模型以 '+' 串接
120000); // 每頁上限,預設 60000
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
Options.CancellationToken := Token;
// 空頁面清單代表每一頁;已有文字的頁面預設跳過
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
begin
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' words accepted, ', Info.DroppedWordCount, ' dropped');
Doc.SaveLoadedDocument(TargetFile);
end
else
case Info.Status of
otlsCancelled: Writeln('Cancelled, document unchanged');
otlsEngineError: Writeln('Engine: ', string(Info.Diagnostic));
otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
else
Writeln(string(Info.Diagnostic));
end;
finally
Doc.Free;
end;
end;
每一頁,Recognize 在暫存路徑下建一個名為 HotPDF-OCR-{GUID} 的私有目錄,把渲染點陣圖存成 input.bmp,然後啟動 tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1,每個路徑參數都按 Windows 命令列對反斜線與內嵌引號的跳脫規則加引號。--dpi 值取自 THPDFOCRTextLayerOptions.DPI 的渲染 DPI,所以 Tesseract 不必從影像中繼資料猜解析度;--psm 3 要求全自動版面分割。引擎自我回報為 Tesseract (local CLI),這就是落進 Info.EngineName 的字串。Tesseract 與它的語言模型不隨 HotPDF 附帶;安裝是應用程式自己的事
TSV 解析器為什麼這麼嚴?
HotPDF 的 TSV 解析器對任何畸形列都讓整頁失敗,因為剖析到一半的字詞清單,會產出與影像悄悄不一致的文字層。Tesseract 的 TSV 輸出有固定的十二欄表頭,從 level 到 text,HotPDF 剝掉可選的位元組順序標記後,把第一行與那個確切表頭比對。之後每一列都必須切出恰好十二個欄位,而切分在第十一個 tab 之後就停,讓辨識文字裡的 tab 留在字詞裡、不會生出第十三欄。只有 level 5 的列是字詞;level 1 到 4 描述頁、區塊、段落與行,一律跳過。level 5 裡文字為空或純空白的列也跳過,因為空白字詞有框、卻沒有東西可定位或搜尋。其餘全部硬檢:整數幾何、以不變 en-US 格式剖析的信心度(德語 locale 才不會把 93.5 讀成垃圾)、完全落在點陣圖內的框、以及 0 到 100 之間的信心度。單一失敗就丟例外,引擎回傳 False,字詞陣列清空。迴歸測試裡恰好有這個案例:一個合法字詞後面跟著一行壞列,必須得到零個字詞,而不是一個
// 摘自 HPDFLocalTSVRecognition 的 level-5 迴圈
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue; // 頁/區塊/段落/行列
WordText := Fields[11];
if Trim(WordText) = '' then Continue; // 空白字詞沒有位置
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
not TryStrToFloat(Fields[10], Confidence, Settings) then
raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
(Int64(X) + W > Request.Bitmap.Width) or
(Int64(Y) + H > Request.Bitmap.Height) or
not ((Confidence >= 0) and (Confidence <= 100)) then
raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100; // 管線期待 0..1
最後那行與一個您可能料不到的預設值互動。Tesseract 的信心度從 0 到 100,管線用 0 到 1,而 THPDFOCRTextLayerOptions.MinimumConfidence 預設 0.5,所以任何低於 50 的 Tesseract 字詞都會計入 Info.DroppedWordCount,永遠到不了頁面。乾淨的 300 DPI 掃描,這是合理的地板。雜訊多的傳真,它可能丟掉頁面上多得驚人的一部分,正確的動作是先看丟棄計數、再考慮調低門檻,因為低信心度的字詞恰恰是最可能錯的那批
Tesseract 子行程繼承了什麼?
Tesseract 子行程從 HotPDF 手裡恰好繼承兩個 handle:標準輸入與輸出用的一個 NUL handle,以及標準錯誤用的一個檔案 handle。這個精確度就是重點。CreateProcess 配 bInheritHandles = True 是把標準 handle 傳給子行程的方式,但它同時把宿主行程裡每一個可繼承 handle 都傳了過去,包括應用程式裡不相關程式碼打開的檔案、pipe 與事件。子行程於是把那些物件攥在手裡直到退出,Tesseract 磨一頁的功夫裡,檔案一直鎖著、pipe 永遠等不到端。HotPDF 用擴充啟動記錄補上這個洞:STARTUPINFOEX、一個帶 PROC_THREAD_ATTRIBUTE_HANDLE_LIST 的屬性清單,以及 EXTENDED_STARTUPINFO_PRESENT 建立旗標。handle 清單就位後,bInheritHandles 仍須為 True,但只有列出的 handle 跨過邊界。同一套圈圍思路也驅動著把 PDF 影像編解碼器隔離在工作者行程裡,那裡的子行程是不可信程式碼;這裡的子行程可信,但宿主也不是自己 handle 表的唯一主人
// 常數以名稱顯示;原始碼傳它們的數值
// 兩個 handle 都以 bInheritHandle = True 建立
InheritedHandles[0] := NullHandle; // stdin 與 stdout
InheritedHandles[1] := ErrorHandle; // 私有目錄裡的 stderr.txt
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
@InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
True, // handle 清單的要求
CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);
被取消的 OCR 為什麼看起來像引擎失敗?
因為 IHPDFOCREngine.Recognize 只回傳一個布林值,False 同時意味著「Tesseract 失敗」與「使用者按了取消」。子行程跑著的時候,轉接器每 25 毫秒輪詢一次取消權杖與逾時,權杖觸發時,它在 Recognize 裡丟例外、抓住自己的例外、清理現場,然後帶著診斷回傳 False。如果管線把它當引擎錯誤,呼叫端會為一個使用者主動停掉的工作看到 otlsEngineError。所以 ApplyLoadedOCRTextLayer 在 Recognize 回傳 False 時先查權杖,只有權杖沒設時才把結果轉成引擎失敗。這個順序保住了多頁契約:辨識、驗證、預算記帳與內容建構對每個請求頁面都先跑完,圖形交易才開,所以 50 頁裡第 40 頁的取消回報 otlsCancelled,文件連同前 39 頁原封不動。之後不必解釋一個半可搜尋的檔案,其餘失敗處理也是同一套有界風格:
- 逾時以每次
Recognize呼叫計、從呼叫開始量,所以預設 60,000 ms 作用在每頁、而不是整份文件 - 逾時或取消時仍在跑的子行程被終止、最多等 5 秒,其私有目錄在
finally區塊裡刪除 output.tsv上限 64 MiB、stderr.txt上限 1 MiB,子行程跑著時與退出後都檢查- 字詞數與 UTF-16 碼元按頁受剩餘的
MaxWordsPerPage、MaxTotalWords與MaxTextCodeUnits預算封頂,超了就讓整輪失敗,而不是截斷字詞清單 - 標準輸出導向
NUL,因為 Tesseract 寫的是output.tsv;標準錯誤進檔案,非零結束碼會附上引擎自己的抱怨、最多 4,096 字元——通常是發現.traineddata檔缺失最快的路
辨識出的字詞怎麼變成隱形文字層
HotPDF 用文字渲染模式 3 把 Tesseract 字詞寫成隱形文字,ISO 32000-1 §9.3.6 定義的既不填充也不描邊模式,頁面上照樣顯示掃描影像,而搜尋與複製作用在辨識出的字詞上。內容串流以 3 Tr 開 BT,每個字詞拿到基線上的一個 Tm 矩陣、一個由框高像素按渲染 DPI 推出的字號、以及一個把字元串拉到實測框寬的 Tz 水平縮放,所以搜尋高亮落在影像裡的字詞上,而不是飄過它
Tesseract 的 TSV 有框、沒有基線,所以轉接器回報每個字詞都無基線,管線把基線估在框底邊上方五分之一框高處。文字本身走一個共用的未內嵌 Type0 字型、Identity-H 編碼加生成的 ToUnicode CMap,整輪裡每個相異 Unicode 純量一個 CID,中文、帶重音的拉丁字母與輔助平面字元因此在複製與搜尋裡都活下來。這個設計有兩個值得先講明的極限:一次至多帶 65,535 個相異純量;未內嵌字型不滿足 ISO 19005 的字型內嵌要求,PDF/A 輸出需要另外內嵌合規字型。驗證結果很簡單、值得自動化:存檔、重載、跑從已載入 PDF 擷取文字的普通已載入文件文字路徑;字詞從預期的頁面回來,文字層就是真的
同一 TSV 協定上的 RapidOCR 與其他引擎
HotPDF 用同一套行程執行器與 TSV 解析器跑 RapidOCR,入口 HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds),對簡體中文掃描是更實用的選擇。命令列一模一樣,只是 bridge 腳本路徑插在 Python 執行檔之後,語言固定 chi_sim。HotPDF 以 tools/OCR/rapidocr_tsv.py 附帶 bridge;它要求 rapidocr 與 onnxruntime 套件加三個本地 ONNX 模型,停用自動模型下載,寫出 Tesseract 形狀的 TSV,讓 Delphi 側不需要第二個解析器。Info.EngineName 回報的引擎名是 RapidOCR (local ONNX)。這個形狀暗示了通用配方:任何能用小腳本包起來、接受 Tesseract 式參數清單、發出十二欄 TSV 的辨識器,都免費繼承 handle 隔離、逾時、取消、輸出預算與全有或全無的提交。這些轉接器只在 Windows 上、一次同步跑一頁,也不在渲染器輸出之外做去偏斜或前處理,所以進去的影像品質仍然是出來的東西的天花板
Tesseract 與 RapidOCR 轉接器、隱形文字層寫入器、餵它們的頁面渲染器,以及驗證結果的文字擷取,全都隨同一個原生 VCL 元件出貨,支援 Delphi 與 C++Builder。要為文件擷取或封存應用加 OCR,HotPDF Delphi PDF component 給您整條管線,只剩 OCR 引擎本身待安裝