Bài viết kỹ thuật

RapidOCR in-process HotPDF: OCR DLL native trong Delphi

HotPDF làm các trang PDF scan trở nên có thể tìm kiếm bằng RapidOCR chạy trong tiến trình qua HPDFCreateRapidOCRDLLOCREngine, một factory thêm vào từ v2.774.0, nạp HotPDFRapidOCR.dll, giữ các model ONNX detection, angle classification và recognition thường trú trong bộ nhớ, rồi trả về một IHPDFOCREngine. Bạn đưa engine đó cho THotPDF.ApplyLoadedOCRTextLayer, thứ render từng trang, chạy CPU inference không cần Python hay tiến trình con, và ghi một text layer Unicode vô hình

Động lực là chi phí trên mỗi trang. Bộ adapter tiến trình RapidOCR phát hành trước đó, HPDFCreateRapidOCREngine, khởi động một worker Python cho mỗi lời gọi Recognize, và worker ấy import runtime cùng nạp các model ONNX của mình trước khi đọc một pixel nào. Trên một kho 500 trang, cái thuế khởi động ấy lặp lại 500 lần, và triển khai đồng nghĩa với việc đi kèm một môi trường Python cạnh tệp thực thi Delphi. DLL native chỉ nạp model một lần, khi bạn tạo engine, và phần triển khai thu gọn còn lại DLL, các tệp model của nó và một từ điển ký tự. Cái bạn đánh đổi là khả năng giết một recognizer bị kẹt, và phần lớn công sức kỹ thuật trong adapter này là về việc sống chung với điều đó một cách trung thực

Làm sao biến một PDF scan thành có thể tìm kiếm bằng DLL RapidOCR?

Tạo một PDF có thể tìm kiếm bằng DLL RapidOCR native chỉ cần một lời gọi factory cộng đúng lời gọi ApplyLoadedOCRTextLayer mà mọi OCR engine của HotPDF đều dùng. Factory nằm trong unit HPDFRapidOCRRecognition và kiểm tra ngay từ đầu: DLL cùng thư mục model phải tồn tại, mọi tệp model và từ điển phải phân giải được, phiên bản ABI phải là 1, và mọi export bắt buộc phải có mặt trước khi model nào được khởi tạo. Lỗi cấu hình raise EArgumentException; một model nạp thất bại raise EInvalidOperation mang theo văn bản chẩn đoán mà DLL đã viết

Chuỗi kiểm tra của factory DLL RapidOCR HotPDF cho HPDFCreateRapidOCRDLLOCREngine: đường dẫn và các tệp model phải tồn tại, HPDFRapidOCRAbiVersion phải trả về 1, các export bắt buộc phải phân giải được, và HPDFRapidOCRCreate phải khởi tạo model, với EArgumentException hay EInvalidOperation được raise ngay lập tức trước khi bất kỳ nhận diện nào chạy, cái sau mang theo văn bản chẩn đoán native
Việc kiểm tra là chủ động ngay từ đầu một cách có chủ đích: vấn đề cấu hình raise trước khi model nào khởi tạo, để một đường dẫn hay ABI sai không bao giờ chạm tới một deadline nhận diện
uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Các model nạp tại đây, ngoài mọi deadline nhận diện.
  // Tên model tương đối trong THPDFRapidOCRDLLOptions.Default được
  // phân giải theo thư mục model.
  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
    // 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,
      ' lines accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

THPDFRapidOCRDLLOptions.Default gọi tên ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx và ppocr_keys_v1.txt, với một CPU thread, giới hạn input 16.777.216 pixel, và deadline nhận diện 60.000 ms. Kể từ v2.775.0, THPDFRapidOCRDLLOptions.ForLanguage thay vào một model recognition cùng từ điển tương ứng cho chữ Hán phồn thể, tiếng Nga, tiếng Nhật, tiếng Ả Rập và các profile khác; vì sao model và từ điển phải đổi cùng nhau được bàn trong model đa ngôn ngữ RapidOCR và từ điển CTC trong HotPDF. Engine tự báo là RapidOCR (native DLL) trong Info.EngineName, giúp log đỡ lẫn lộn cạnh adapter OCR Tesseract chạy ngoài tiến trình và OCR engine khớp mẫu dựng sẵn

Vì sao ABI C chỉ nói int32_t và các byte UTF-8?

ABI của HotPDFRapidOCR.dll chỉ dùng số nguyên có bề rộng cố định, con trỏ thô và độ dài byte tường minh, vì Delphi, C++Builder và Free Pascal chẳng chia sẻ gì với MSVC ngoài quy ước gọi C. Một std::string, một std::vector hay một exception C++ có một layout và một mô hình unwinding thuộc về một compiler và một runtime library riêng. Để bất kỳ cái nào trong số đó vượt qua biên giới, thất bại sẽ là một stack hỏng hay một khối heap bị allocator sai giải phóng, chứ không phải một lỗi sạch sẽ

ABI version 1 vì thế bám một danh sách quy tắc ngắn. Mọi export là cdecl và trả về một status int32_t, trong đó 1 là thành công và 0 là thất bại. Mọi hàm có thể thất bại nhận một buffer chẩn đoán thuộc sở hữu người gọi cùng dung lượng của nó tính theo byte; DLL viết một thông báo UTF-8 kết thúc NUL, cắt ngắn cho vừa, và adapter decode nó với một ký tự kết thúc cứng ở byte cuối của buffer 4.096 byte của mình. Thân mỗi export được bọc trong try với cả catch (const std::exception &) lẫn catch (...), nên một lỗi ONNX Runtime, một assertion OpenCV hay một từ điển hỏng đều trở thành status 0 cộng văn bản, chứ không bao giờ là một exception chui vào code Pascal

ExportVai tròKhi nào adapter phân giải nó
HPDFRapidOCRAbiVersionTrả về 1; giá trị khác bị từ chốiĐầu tiên, trước bất cứ thứ gì
HPDFRapidOCRCreateNạp model detection, classification tùy chọn, recognition và từ điểnTrong factory
HPDFRapidOCRRecognizeChạy một bitmap và phát một callback cho mỗi dòng văn bảnTrong factory
HPDFRapidOCRDestroyGiải phóng instance modelTrong factory
HPDFRapidOCRSetReadingDirectionThứ tự hàng phải-sang-trái tùy chọn, thêm vào ở v2.775.0Chỉ khi RightToLeft được đặt

Export tùy chọn được phân giải lười một cách có chủ đích: một DLL v2.774.0 thiếu nó vẫn phục vụ được các yêu cầu trái-sang-phải. DLL được nạp bằng LoadLibraryEx với các cờ tìm kiếm phủ cả 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 phụ thuộc ONNX Runtime hay OpenCV đặt cạnh HotPDFRapidOCR.dll được tìm thấy mà không phải đụng tới PATH. Đường dẫn model và từ điển di chuyển dưới dạng UTF-8 và DLL chuyển chúng bằng MultiByteToWideChar ở chế độ nghiêm ngặt trước khi mở tệp qua các API wide-character, nên một thư mục model dưới một tên người dùng Trung Quốc hay Cyrillic hoạt động tốt thay vì bị mở rộng từng byte thành rác

Một quy tắc nằm trong bản build chứ không nằm trong header. DLL liên kết tĩnh ONNX Runtime và OpenCV, và cấu hình CMake mặc định dùng static release CRT (/MT). Các thư viện tĩnh build với /MD trộn vào một DLL /MT cho ra lỗi link trong trường hợp đẹp, và hai heap độc lập trong trường hợp xấu, nên các thư viện được cấp phải khớp với chế độ CRT mà DLL dùng

Chuyện gì xảy ra giữa một TBitmap và một dòng văn bản?

HotPDF trao cho DLL một bản chụp BGR top-down độc lập của trang đã render, và DLL trả lại một callback cho mỗi dòng văn bản được nhận diện, với văn bản UTF-8 dạng mượn mà adapter phải chép lại trước khi trả về

Trên Delphi, adapter gán bitmap trang vào một TBitmap riêng, ép pf24bit, và đọc các hàng bằng GetDIBits với một biHeight âm, thứ cho ra các hàng top-down được đệm tới căn chỉnh bốn byte; stride ấy được truyền tường minh. Trên FPC, nó đọc qua CreateIntfImage, vì việc ghi scanline của LCL có thể cập nhật ảnh thô mà không làm mới handle GDI. Bitmap của caller không bao giờ bị sửa, và ngân sách pixel (MaxPixels, mặc định 16.777.216 và cấu hình được tới 67.108.864) cùng giới hạn 32.767 pixel mỗi chiều được kiểm tra trước khi buffer chụp được cấp phát

Pipeline DLL RapidOCR của HotPDF từ bitmap tới text layer: adapter chụp trang thành pf24bit BGR top-down, DLL đệm, phát hiện, sắp thứ tự rồi nhận diện các crop, trao một callback cho mỗi dòng với văn bản UTF-8 mượn, box và confidence, còn adapter kiểm tra từng dòng trước khi commit text layer
Pixel vượt ABI đúng một lần dưới dạng bản chụp, các dòng quay về từng callback một, và chẳng gì chạm tới lớp có thể tìm kiếm cho tới khi mọi phép kiểm đều qua

Bên trong DLL, bản chụp được đệm thêm 50 pixel trắng, các vùng văn bản được phát hiện với cạnh dài tối đa 1.024 pixel, các box được xếp thành các hàng ngang, và mỗi crop được xoay tùy chọn bởi angle classifier trước khi nhận diện. Mỗi dòng văn bản sau đó đi qua một callback nhận một const char*, một số byte, một box dạng số nguyên theo pixel ảnh gốc, và confidence trung bình của ký tự. Con trỏ văn bản chỉ hợp lệ trong lúc callback, nên adapter chép nó ngay lập tức, và nó khắt khe về những gì được chấp nhận:

  • UTF-8 được decode với MB_ERR_INVALID_CHARS; một chuỗi dị dạng làm trang thất bại thay vì sinh ra các ký tự thay thế trong một lớp có thể tìm kiếm
  • Các ký tự điều khiển C0 và C1 bị từ chối, còn các dòng toàn dấu cách bị bỏ qua
  • Box phải nằm trong bitmap và confidence phải là một giá trị hữu hạn từ 0 tới 1
  • Văn bản được đếm vào MaxTextCodeUnits của yêu cầu với trần cứng 1.048.576 đơn vị UTF-16 mỗi lời gọi, và các ký tự bổ sung tốn hai đơn vị
  • Bất kỳ exception Pascal nào bên trong callback đều bị bắt tại đó, được lưu lại, và biến thành giá trị trả về 0, khiến DLL dừng và báo thất bại; thông báo đã lưu khi đó trở thành chẩn đoán

Hai hệ quả đáng nhớ khi tinh chỉnh. Thứ nhất, đơn vị đầu ra là một dòng, không phải một từ: mỗi dòng chiếm một slot MaxWords, Info.AcceptedWordCount và Info.DroppedWordCount đếm dòng, và phần highlight tìm kiếm trải theo box của dòng. Thứ hai, MinimumConfidence (mặc định 0.5) được so với confidence trung bình ký tự của dòng, nên một dòng có một ký tự không đọc được trong hai mươi ký tự sạch thường vẫn sống sót. DLL không cung cấp baseline, nên pipeline text layer tự suy ra một cái từ box. Một trang rỗng thành công với không dòng nào, và bất kỳ thất bại nào cũng xóa kết quả từng phần để commit nhiều trang giữ nguyên tính tất-cả-hoặc-không-cái-gì

Quyền sở hữu model và thread safety

Mỗi engine DLL RapidOCR sở hữu đúng một instance model trong suốt vòng đời của mình, và các lời gọi Recognize lên engine đó được tuần tự hóa bằng một critical section. Việc giữ interface IHPDFOCREngine là thứ giữ các model ấm nóng, nên mẫu đúng cho công việc theo lô là tạo engine một lần rồi dùng lại xuyên các tài liệu

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;    // scan đứng thẳng: không nạp model classifier
  Models.Threads := 4;                   // 1..64, bị chặn tại số logical processor
  Models.TimeoutMilliseconds := 120000;  // mỗi lời gọi Recognize, theo kiểu hợp tác
  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;  // tham chiếu cuối được thả: model bị hủy, rồi DLL được unload

Giá trị Threads đặt cả số thread intra-op lẫn inter-op của mỗi session ONNX, và DLL kẹp nó theo số processor đang hoạt động. Hai thread dùng chung một engine không chạy song song được; thread thứ hai chờ khóa. Lời chờ ấy không phải một EnterCriticalSection mù quáng: adapter gọi TryEnterCriticalSection mỗi 25 ms và kiểm tra cancellation token cùng deadline giữa các lần thử, nên một yêu cầu đang xếp hàng vẫn có thể bị hủy hoặc hết giờ. Nếu bạn cần song song thật, hãy tạo một engine mỗi worker và chấp nhận rằng mỗi engine giữ một bản sao model riêng trong bộ nhớ

Thứ tự dọn dẹp được cố định bởi destructor của engine: HPDFRapidOCRDestroy giải phóng instance model trước, rồi FreeLibrary unload DLL. Phía native, việc khởi tạo model cũng cẩn thận không kém; khi model recognition thất bại sau khi các session detector và classifier đã dựng xong, các session ấy được giải phóng trước khi lỗi được báo, và số lớp của từ điển được đối chiếu với output của model ngay lúc khởi tạo thay vì trên trang đầu tiên

Vì sao một lời gọi OCR native không thể bị giết giữa chừng inference?

Một lời gọi RapidOCR native không thể bị giết giữa chừng inference vì nó chạy trên thread của bạn, trong tiến trình của bạn, giữa một session ONNX Runtime không chấp nhận ngắt quãng. Việc hủy trong adapter DLL của HotPDF vì thế mang tính hợp tác: DLL gọi một abort callback trước và sau detection, sau classification, và sau mỗi dòng được nhận diện, và dừng ở checkpoint đầu tiên nơi callback trả về 0. Một Run ONNX đã bắt đầu thì sẽ chạy hết trước đã

Các phương án thay thế còn tệ hơn chờ đợi. TerminateThread sẽ để lại khóa heap của CRT, thread pool của ONNX Runtime, và mọi trạng thái OpenCV trong đúng tình trạng chúng đang vướng, đầu độc phần còn lại của tiến trình. FreeLibrary trong khi một lời gọi vẫn đang chạy sẽ unload code đang nằm trên stack. Chẳng cái nào làm an toàn được, nên adapter không bao giờ thử. Deadline trong TimeoutMilliseconds vì thế là một deadline hợp tác, và deadline hết hạn hiện ra như một engine error với chẩn đoán hết giờ, còn token bị hủy hiện ra như otlsCancelled:

// Token do caller tạo và chia sẻ với UI thread,
// thứ gọi Token.Cancel khi người dùng nhấn Stop
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
  case Info.Status of
    otlsCancelled:
      // trả về tại biên stage hay dòng kế; tài liệu không đổi
      Writeln('Cancelled');
    otlsEngineError:
      // gồm cả deadline hết hạn kiểu hợp tác và chẩn đoán native
      Writeln('Engine: ', string(Info.Diagnostic));
    otlsBudgetExceeded:
      Writeln('Budget: ', string(Info.Diagnostic));
  else
    Writeln(string(Info.Diagnostic));
  end;

Đây là sự đánh đổi cốt lõi giữa các adapter tiến trình của HotPDF và DLL in-process, và không bên nào thắng ở mọi dòng:

Các điểm đánh đổi của adapter OCR HotPDF: adapter tiến trình khởi động một worker và nạp model cho mỗi trang nhưng có thể bị giết và cô lập crash, trong khi DLL RapidOCR in-process nạp model một lần, chỉ dừng tại các checkpoint hợp tác, dùng chung không gian địa chỉ, và triển khai thành một DLL cùng model và từ điển của nó
Chọn theo loại công việc: ứng dụng desktop xử lý từng trang một hưởng lợi từ DLL ấm nóng, còn server nuốt các bản scan không đáng tin nên trả giá cho bức tường tiến trình
  • Chi phí khởi động: các adapter Tesseract và Python RapidOCR khởi chạy một tiến trình và nạp model cho từng trang; DLL chỉ nạp model một lần mỗi engine
  • Dừng lại: một tiến trình con có thể bị terminate thẳng tay, và worker Python chạy bên trong một Job Object kill-on-close nên toàn cây tiến trình của nó đi theo; DLL chỉ dừng được tại các biên stage và dòng
  • Cô lập lỗi: một crash trong tesseract.exe chỉ làm một trang thất bại; một access violation bên trong DLL hạ cả tiến trình của bạn
  • Triển khai: adapter tiến trình cần một chương trình cài sẵn hay một môi trường Python; DLL cần chính nó, các model và từ điển của nó, khớp với bitness của ứng dụng
  • Bộ nhớ: adapter tiến trình thả hết mọi thứ khi tiến trình con thoát; một engine DLL giữ các model thường trú cho tới khi tham chiếu interface cuối được thả

Với một ứng dụng desktop tương tác mà OCR từng trang một, tính phản hồi của DLL thường thắng. Với một server nuốt các bản scan không đáng tin suốt ngày đêm, biên tiến trình xứng đáng với chi phí khởi động của nó

Build và triển khai HotPDFRapidOCR.dll

HotPDFRapidOCR.dll được build từ các mã nguồn C++ trong Native/RapidOCR với MSVC, C++17, một Windows SDK, và CMake 3.20 trở lên, dùng một script trợ lý nhận các thư mục nguồn mạng native, ONNX Runtime, và OpenCV, cộng một nền tảng Win32 hay Win64. Hãy build cả hai nếu bạn giao cả hai, vì một ứng dụng Delphi 32-bit không thể nạp một DLL 64-bit, và các thư viện tĩnh bạn cấp phải khớp cả kiến trúc đích lẫn chế độ CRT

Phía model có những giới hạn tương thích riêng. Detector là một DB text detector; recognizer nhận các model CTC bố trí NCHW với chiều cao input cố định 32 hay 48, và dùng 48 cho các model có chiều cao động. ONNX Runtime tĩnh đóng gói sẵn không nạp được các model lưu với phiên bản IR mới hơn, nên các bản export PP-OCRv5 gần đây thất bại ngay lúc khởi tạo với một chẩn đoán thay vì nạp lửng. Từ điển phải là UTF-8 không BOM, đúng thứ tự ký tự của model, và số lớp của nó phải khớp output của model; kết thúc dòng CRLF được chấp nhận. Nhận diện là offline: DLL không bao giờ tải một model còn thiếu

Tra nhanh

  • Factory: HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options]) trong HPDFRapidOCRRecognition, có sẵn từ v2.774.0 trên Delphi, C++Builder và các bản build FPC/Lazarus Windows
  • Giữ IHPDFOCREngine trả về sống xuyên các trang và tài liệu; thả nó là hủy các model và unload DLL
  • Một engine chạy một nhận diện mỗi lúc; hãy tạo nhiều engine cho các worker song song và trù bị bộ nhớ cho từng bản sao model
  • Đầu ra là một entry mỗi dòng văn bản với confidence trung bình ký tự, được lọc bởi THPDFOCRTextLayerOptions.MinimumConfidence
  • Việc hủy và TimeoutMilliseconds mang tính hợp tác; một run ONNX đang chạy thì luôn chạy hết
  • Cho khớp bitness của DLL với ứng dụng và chế độ CRT của các thư viện tĩnh ONNX Runtime và OpenCV với DLL
  • Chọn một profile ngôn ngữ cho mỗi engine bằng THPDFRapidOCRDLLOptions.ForLanguage (v2.775.0); một engine không tự nhận diện ngôn ngữ

Adapter RapidOCR native, các adapter OCR dạng tiến trình, bộ render trang nuôi chúng, và trình ghi text layer Unicode vô hình đều đi cùng nhau trong HotPDF, một PDF component VCL native cho Delphi và C++Builder. Nếu ứng dụng trích xuất hay lưu trữ tài liệu của bạn cần đầu ra có thể tìm kiếm mà không cần một runtime Python trên máy đích, HotPDF Delphi PDF component cung cấp trọn pipeline, chỉ còn DLL cùng các model của nó để triển khai