Bài viết kỹ thuật

OCR Tesseract DLL của HotPDF: gọi C API từ Delphi

HotPDF chạy Tesseract ngay trong tiến trình Delphi của bạn qua HPDFCreateTesseractDLLOCREngine, một factory thêm vào ở v2.772.0, nạp động một DLL tương thích Tesseract 5, lái C API của nó (TessBaseAPIInit2, TessBaseAPIRecognize, result iterator) và trả về một IHPDFOCREngine. THotPDF.ApplyLoadedOCRTextLayer dùng engine đó để thêm một text layer Unicode vô hình, có thể tìm kiếm, lên các trang PDF đã scan

Cùng recognizer ấy vốn đã với tới được qua adapter tesseract.exe ngoài, thứ ghi một tệp BMP rồi parse TSV. Đường đó hoạt động, nhưng mỗi trang phải trả cho một lần khởi chạy tiến trình, một tệp bitmap tạm, và một định dạng văn bản không có baseline cũng chẳng có tùy chỉnh page segmentation. Gọi DLL xóa sổ cả ba. Nó đồng thời xóa luôn bức tường tiến trình, nghĩa là một binding Pascal nằm ngay trên các cấu trúc C, các boolean C và các chuỗi cấp phát bởi C. Phần lớn điều đáng biết về adapter này là chỗ nào binding có thể sai một cách êm thầm

Chạy Tesseract in-process từ Delphi với HotPDF thế nào?

Chạy Tesseract in-process với HotPDF chỉ cần một lời gọi factory trong unit HPDFTesseractRecognition cộng đúng lời gọi ApplyLoadedOCRTextLayer mà mọi OCR engine của HotPDF đều dùng. Factory kiểm tra ngay từ đầu. Tệp DLL và thư mục tessdata phải tồn tại, mã định danh ngôn ngữ chỉ được chứa chữ cái ASCII, chữ số, _ và +, mọi model trong một tổ hợp như chi_sim+eng phải có một tệp .traineddata khớp, và cả 21 export bắt buộc phải phân giải được trước khi engine được trả về. Lỗi cấu hình raise EArgumentException; một DLL nạp thất bại raise EOSError kèm mã lỗi Windows và gợi ý kiểm tra kiến trúc cùng các phụ thuộc

uses
  SysUtils, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Một ứng dụng Win64 cần DLL 64-bit; các DLL phụ thuộc đặt cạnh nó
  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
    // Danh sách trang rỗng nghĩa là mọi trang; trang đã có văn bản bị bỏ qua
    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 đặt PageSegMode là tpsAuto, EngineMode là temDefault, TimeoutMilliseconds là 60.000 và MaxPixels là 16.777.216. Ngân sách pixel quan trọng hơn bề ngoài của nó. Một trang US Letter ở 300 DPI mặc định render thành 2.550 × 3.300 pixel, khoảng 8,4 triệu — vừa khít. Cùng trang đó ở 600 DPI là 5.100 × 6.600, khoảng 33,7 triệu, và adapter từ chối trước khi Tesseract kịp thấy một pixel nào. Hãy nâng MaxPixels (trần là 67.108.864) hay giữ DPI nguyên chỗ; mỗi cạnh cũng bị chặn ở 32.767 pixel

DLL được nạp bằng LoadLibraryEx với các cờ tìm kiếm phủ thư mục riêng của DLL cộng các thư mục an toàn mặc định, nên các thư viện ảnh mà Tesseract dựa vào có thể nằm cạnh nó mà không phải đụng tới PATH hay thư mục hiện hành. HotPDF không đóng gói hay tải xuống bất kỳ runtime hay model OCR nào; cả hai do bạn cấp

Thứ gì đổi so với adapter tesseract.exe?

Adapter DLL đổi lấy sự cô lập tiến trình để nhận đầu ra phong phú hơn và chi phí mỗi trang thấp hơn. Cả hai adapter cắm vào cùng pipeline text layer, nên ánh xạ tọa độ, lọc confidence và commit tất-cả-hoặc-không-cái-gì là như nhau; khác nhau nằm ở cách pixel đi vào và từ ngữ đi ra

Khía cạnhtesseract.exe adapterTesseract DLL adapter
FactoryHPDFCreateTesseractOCREngineHPDFCreateTesseractDLLOCREngine
Pixel vàoTệp BMP trong thư mục tạm riêngBuffer grayscale 8-bit trong bộ nhớ
Từ raTSV cấp từ, chặn ở 64 MiBResult iterator, UTF-8 mỗi từ
BaselineKhông cóĐưa thẳng qua từ TessPageIteratorBaseline
Page segmentation và engine modeChỉ segmentation tự độngTHPDFTesseractPageSegMode, THPDFTesseractEngineMode
TimeoutCứng: tiến trình con bị terminateHợp tác: Tesseract phải tự nhận ra
Cô lập crash và bộ nhớTiến trình riêngKhông, dùng chung không gian địa chỉ của bạn

Một chi phí không biến mất. Mỗi lời gọi Recognize tạo instance API riêng và gọi TessBaseAPIInit2, nên các model ngôn ngữ được khởi tạo mỗi trang thay vì một lần mỗi engine. File cache của hệ điều hành dịu bớt phần nạp lại, nhưng trên các bộ model đa ngôn ngữ lớn, nó vẫn là chi phí cố định trội mỗi trang, và nó đếm vào deadline nhận diện. Engine DLL RapidOCR in-process chọn thiết kế ngược lại và giữ các model ONNX thường trú suốt vòng đời engine; các vấn đề biên giới (C ABI, buffer mượn, công việc native không thể ngắt) thuộc cùng một họ

Vì sao Delphi không thể chép struct monitor của Tesseract?

Delphi không thể phản chiếu monitor tiến trình của Tesseract một cách an toàn vì ETEXT_DESC chứa các trường nội bộ tùy phiên bản, nên một record chép tay đặt cancel callback và deadline sai offset trên vài bản build. Chẳng gì hỏng ầm ầm khi chuyện đó xảy ra. Tesseract chỉ đơn giản đọc con trỏ callback của bạn từ một trường giờ đang giữ thứ khác, hoặc không bao giờ thấy deadline

HotPDF vì thế coi monitor như một con trỏ mờ và chỉ đụng tới nó qua các hàm export: TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc, TessMonitorSetDeadlineMSecs và TessMonitorDelete. Nếu bạn tự bind C API cho mục đích khác, cùng mẫu này áp dụng. Đoạn phác dưới đây là code binding của riêng bạn, không phải API HotPDF, và phản chiếu các khai báo mà HotPDF dùng nội bộ

Cách xử lý monitor của DLL Tesseract trong HotPDF: chép record ETEXT_DESC tùy phiên bản đặt cancel callback và deadline sai offset rồi thất bại âm thầm, trong khi HotPDF coi monitor như opaque, lái TessMonitorCreate, TessMonitorSetCancelThis, TessMonitorSetCancelFunc và TessMonitorSetDeadlineMSecs, và giữ callback cdecl không dính exception
Một con trỏ mờ cộng năm export là toàn bộ hợp đồng; callback giữ nguyên là một Boolean một byte chỉ đọc một cờ và một đồng hồ
type
  // C: typedef bool (*TessCancelFunc)(void *cancel_this, int words);
  TTessCancelFunc = function(CancelThis: Pointer; Words: Integer): Boolean; cdecl;
  TTessMonitorCreate = function: Pointer; cdecl;   // ETEXT_DESC*, không bao giờ dereference
  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
  // Chạy trên stack của Tesseract: đọc cờ và đồng hồ, đừng bao giờ raise
  Result := (CancelThis = nil) or POCRJob(CancelThis)^.CancelRequested or
    (GetTickCount64 >= POCRJob(CancelThis)^.DeadlineTick);
end;

// Cách dùng, với các con trỏ hàm được phân giải bằng GetProcAddress:
//   Monitor := MonitorCreate();
//   try
//     MonitorSetCancelThis(Monitor, @Job);
//     MonitorSetCancelFunc(Monitor, ShouldCancel);
//     MonitorSetDeadlineMSecs(Monitor, RemainingMs);
//     RC := BaseAPIRecognize(API, Monitor);
//   finally
//     MonitorDelete(Monitor);
//   end;

Hai chi tiết trong đoạn phác ấy là có chủ đích. Callback trả về Boolean, thứ chiếm một byte trong cả Delphi lẫn Free Pascal, khớp với bool của C trong TessCancelFunc. BOOL bốn byte của Windows hay LongBool của Delphi nhìn qua tưởng thay thế được mà không: khi một bên ghi một byte và bên kia đọc bốn, các byte cao của thanh ghi trả về là thứ còn sót lại, và một false có thể tới nơi với dáng vẻ true. Cùng header đó còn gây thêm rắc rối, vì các hàm như TessPageIteratorBoundingBox trả về một int, mà HotPDF khai báo là Integer. Hãy đọc kiểu C của từng giá trị trả về thay vì giả định một quy ước cho cả API

Chi tiết thứ hai là callback không bao giờ raise. Một exception Delphi unwinding xuyên qua các frame C++ của Tesseract là hành vi không xác định, nên callback của HotPDF chỉ đọc cancellation token và một giá trị GetTickCount64 đơn điệu. Adapter biến kết quả thành một chẩn đoán hủy hay hết giờ sau khi TessBaseAPIRecognize trả về, và nó thực hiện phép kiểm đó bất kể mã trả về native

Phía Delphi sở hữu những con trỏ native nào?

Adapter DLL Tesseract của HotPDF sở hữu ba đối tượng native mỗi yêu cầu — instance API, monitor và result iterator — và mượn tất cả phần còn lại. Mỗi lời gọi Recognize tạo bộ của riêng nó và thả trong một khối finally: TessResultIteratorDelete, rồi TessMonitorDelete, rồi TessBaseAPIDelete. Thả interface engine là unload thư viện

Quyền sở hữu đối tượng native của DLL Tesseract HotPDF cho mỗi lời gọi Recognize: result iterator, monitor và instance API được sở hữu và giải phóng theo đúng thứ tự ấy trong finally, page iterator từ TessResultIteratorGetPageIterator là một view mượn không bao giờ được giải phóng, còn các chuỗi GetUTF8Text được chép rồi trả về qua TessDeleteText
Ba đối tượng được sở hữu, mọi thứ còn lại là mượn: hãy giải phóng theo đúng thứ tự cố định, đừng bao giờ double-free page iterator, và đừng bao giờ trộn các allocator
  • TessResultIteratorGetPageIterator trả về một view mượn vào result iterator, không phải một đối tượng mới. HotPDF dùng nó cho TessPageIteratorBoundingBox và TessPageIteratorBaseline và không bao giờ giải phóng nó; xóa nó riêng lẻ là giải phóng cùng một vùng nhớ hai lần
  • TessResultIteratorGetUTF8Text trả về một chuỗi cấp phát bởi runtime riêng của DLL. HotPDF chép nó rồi trả lại qua TessDeleteText trong một khối finally; FreeMem của Pascal sẽ thả nó trên heap sai
  • Văn bản của từ được decode với kiểm tra UTF-8 nghiêm ngặt và kiểm độ dài trước khi chuyển đổi. Các từ chứa ký tự điều khiển, UTF-8 dị dạng, box nằm ngoài ảnh, hình chữ nhật bị lật, hay confidence ngoài 0–100 làm yêu cầu thất bại thay vì bị vá âm thầm
  • Tổng văn bản mỗi yêu cầu bị chặn ở 1.048.576 code unit UTF-16, và số từ phải vừa ngân sách yêu cầu được ApplyLoadedOCRTextLayer truyền xuống

Confidence tới dưới dạng 0–100 và được scale về 0–1, nên THPDFOCRTextLayerOptions.MinimumConfidence mang cùng một ý nghĩa với mọi engine. Khi Tesseract báo một baseline, cả hai điểm đầu-cuối được đưa thẳng qua; nếu không, pipeline text layer lùi về ước lượng hình học của mình, đúng như nó làm với input TSV

Vì sao phải kiểm chứng enum trước khi nó tới DLL?

HotPDF chép giá trị ordinal thô của PageSegMode và EngineMode vào một Integer trước khi kiểm khoảng, vì compiler có thể giả định một biến enum luôn giữ một giá trị đã khai báo và gộp Ord(X) > Ord(High(T)) thành hằng false. Các ordinal không phải trang trí: THPDFTesseractPageSegMode bám cách đánh số page segmentation của Tesseract từ 0 tới 13, THPDFTesseractEngineMode bám cách đánh số engine mode từ 0 tới 3, và cả hai đều tới DLL dưới dạng số nguyên thường. Một record options dựng bằng FillChar, điền từ một stream, hay truyền từ C++Builder với một số nguyên ép kiểu có thể mang một byte kiểu 200. Kiểm chứng ordinal đã chép biến nó thành một EArgumentException ngay lúc factory thay vì một mode không xác định bên trong code native. Factory còn từ chối tpsOSDOnly và tpsAutoOnly — những mode không sinh từ nào — và đòi osd.traineddata cho tpsAutoOSD cùng tpsSparseTextOSD

Timeout nhận diện thực chất bảo đảm điều gì?

Timeout của DLL Tesseract mang tính hợp tác: HotPDF có thể dừng công việc của mình và nhờ Tesseract dừng, nhưng không thể ép code native trả về. Đồng hồ bắt đầu khi Recognize khởi động, nên chuyển đổi bitmap và khởi tạo model tiêu cùng một ngân sách với phần nhận diện. HotPDF kiểm thời gian đã trôi và cancellation token trong lúc chuyển grayscale và giữa các từ khi duyệt kết quả, và truyền số mili giây còn lại cho TessMonitorSetDeadlineMSecs trước khi gọi TessBaseAPIRecognize

Khe hở nằm bên trong lời gọi native. Monitor của Tesseract chỉ được hỏi trong lúc nhận diện từ, không phải trong TessBaseAPIInit2 hay phân tích bố cục trang, nên một lần nạp model chậm hay một bố cục bệnh hoạn có thể chạy vượt deadline trước khi timeout được báo. Các ngân sách pixel và đầu ra cũng không chặn việc dùng bộ nhớ của riêng thư viện native. Nếu bạn cần một worker có thể giết được, hãy dùng adapter tiến trình; đó là sự đánh đổi trung thực, không phải một tính năng bị bỏ sót

Giải phẫu timeout hợp tác của DLL Tesseract trong HotPDF: đồng hồ bắt đầu khi Recognize khởi động và phủ cả chuyển grayscale, TessBaseAPIInit2 và phân tích bố cục, nhưng monitor chỉ được hỏi trong lúc nhận diện từ, nên việc nạp model và phân tích bố cục có thể vượt trước khi HotPDF báo otlsEngineError hay otlsCancelled
Một deadline ở đây là một lời đề nghị, không phải một bảo chứng: init và phân tích bố cục có thể chạy dài, còn một worker mà bạn thật sự giết được thì cần adapter tiến trình

Page segmentation là chỗ adapter DLL kiếm được miếng cơm của mình với input khó. Các biểu mẫu, nhãn và bảng scan có các trường rải rác thường nhận diện tốt hơn với tpsSparseText so với segmentation tự động, thứ cố gắng lắp ghép các cột và đoạn văn vốn không tồn tại

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;  // các trường rải rác, không ghép cột
  TessOptions.EngineMode := temLSTMOnly;     // cần các model LSTM trong tessdata
  TessOptions.TimeoutMilliseconds := 20000;  // gồm cả việc khởi tạo model
  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;

Một timeout hiện ra như otlsEngineError với chẩn đoán Tesseract DLL OCR timed out, còn một token bị hủy hiện ra như otlsCancelled. Trong cả hai trường hợp, ApplyLoadedOCRTextLayer đã nhận diện xong mọi trang được chọn trước khi bắt đầu giao dịch commit, nên một thất bại ở trang 40 trên 50 để nguyên tài liệu đã nạp y như cũ. Lưu ý tpsSingleLine, tpsSingleBlock và tpsSparseText chỉ đổi segmentation; chẳng cái nào làm thẳng một bản scan bị nghiêng

Free Pascal và Lazarus: pixel cũ kỹ và tiếng Trung mất tích

Cả hai factory Tesseract hoạt động trên Free Pascal và Lazarus Windows bản Win32 và Win64 kể từ v2.772.1, sau hai bản sửa riêng cho FPC. Hãy build lại package Lazarus cho kiến trúc đích trước đã; phần port chung được nói trong HotPDF trên Free Pascal và Lazarus Win64

Bản sửa đầu tiên liên quan tới pixel. Một TBitmap của LCL được ghi qua scanline có thể cập nhật ảnh thô mà không làm mới handle bitmap Windows, nên GetDIBits trên handle ấy trả về các pixel cũ. Triệu chứng khiến người ta ngơ ngác: văn bản vẽ trực tiếp lên một bitmap thì được nhận diện, còn một trang do PDF renderer của HotPDF render lại cho ra danh sách từ trống rỗng. Trên FPC, adapter giờ đọc một bản chụp biết định dạng qua CreateIntfImage, thứ tôn trọng pixel format và thứ tự hàng của ảnh thô. Bản build Delphi giữ đường GetDIBits trên một bản sao 24-bit riêng. Cả hai bản build đều không sửa bitmap của caller

Bản sửa thứ hai thuộc về adapter tesseract.exe. TStringList của FPC lưu chuỗi ANSI, nên việc gán văn bản TSV UTF-8 đã decode vào Lines.Text lặng lẽ bỏ mất mọi ký tự tiếng Trung hay mặt phẳng bổ sung mà ANSI code page hệ thống không biểu diễn được. Đường FPC giờ giữ TSV dưới dạng byte UTF-8, strip BOM ở mức byte và decode từng từ riêng lẻ thành UnicodeString. Adapter DLL chưa bao giờ dính vấn đề này vì nó decode từng từ trực tiếp từ iterator

Tra nhanh

  • Factory: HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language[, Options]) trong HPDFTesseractRecognition, thêm vào ở v2.772.0, hỗ trợ FPC ở v2.772.1
  • Mặc định: tpsAuto, temDefault, 60.000 ms, 16.777.216 pixel; dải timeout 1–3.600.000 ms, trần pixel 67.108.864
  • Cho khớp bitness của DLL với ứng dụng và đặt các DLL phụ thuộc cạnh DLL Tesseract
  • Coi monitor như opaque; đừng bao giờ chép ETEXT_DESC vào một record Pascal
  • Khai báo cancel callback là cdecl với kết quả Boolean một byte, và đừng bao giờ để một exception thoát ra khỏi nó
  • Giải phóng văn bản iterator bằng TessDeleteText; đừng bao giờ giải phóng page iterator lấy từ result iterator
  • Hãy mong đợi deadline mang tính hợp tác: khởi tạo model và phân tích bố cục có thể vượt qua nó
  • Dùng adapter tesseract.exe khi bạn cần terminate cứng hay cô lập crash

Adapter DLL Tesseract, các adapter tiến trình và OCR engine dựng sẵn đều đi kèm HotPDF Delphi PDF component cho Delphi, C++Builder và Free Pascal; xem trang sản phẩm HotPDF để biết các phiên bản và tải về