기술 문서

HotPDF 인프로세스 RapidOCR: Delphi 네이티브 DLL OCR

HotPDF는 HPDFCreateRapidOCRDLLOCREngine을 통해 인프로세스 RapidOCR로 스캔 PDF 페이지를 검색 가능하게 만듭니다. v2.774.0에서 추가된 이 팩토리는 HotPDFRapidOCR.dll을 로드하고, ONNX 탐지, 각도 분류, 인식 모델을 메모리에 상주시킨 뒤 IHPDFOCREngine을 반환합니다. 그 엔진을 THotPDF.ApplyLoadedOCRTextLayer에 넘기면 각 페이지를 렌더링하고, Python도 자식 프로세스도 없이 CPU 추론을 돌려 보이지 않는 Unicode 텍스트 레이어를 커밋합니다

동기는 페이지당 비용입니다. 먼저 나온 RapidOCR 프로세스 어댑터 HPDFCreateRapidOCREngine는 Recognize 호출마다 Python 워커를 띄우고, 그 워커는 픽셀 하나 읽기 전에 런타임을 임포트하고 ONNX 모델을 로드합니다. 500페이지 아카이브에서 그 시작세는 500번 반복되고, 배포는 Delphi 실행 파일 옆에 Python 환경을 실어 보내는 일을 뜻합니다. 네이티브 DLL은 엔진을 만들 때 모델을 한 번만 로드하고, 배포는 DLL, 모델 파일, 문자 사전으로 줄어듭니다. 그 대가로 포기하는 것은 멈춘 인식기를 죽일 수 있는 능력이고, 이 어댑터 엔지니어링의 대부분은 그 사실과 정직하게 공존하는 일입니다

RapidOCR DLL로 스캔 PDF를 어떻게 검색 가능하게 만들까?

네이티브 RapidOCR DLL로 검색 가능한 PDF를 만드는 데는 팩토리 호출 한 번과 모든 HotPDF OCR 엔진이 쓰는 같은 ApplyLoadedOCRTextLayer 호출이 필요합니다. 팩토리는 HPDFRapidOCRRecognition 유닛에 살며 미리 검증합니다. DLL과 모델 디렉터리가 존재해야 하고, 모든 모델과 사전 파일이 해석되어야 하며, ABI 버전은 1이어야 하고, 모든 필수 익스포트가 어떤 모델도 초기화되기 전에 있어야 합니다. 설정 실수는 EArgumentException을, 로드에 실패한 모델은 DLL이 쓴 진단 텍스트를 실은 EInvalidOperation을 던집니다

HPDFCreateRapidOCRDLLOCREngine용 HotPDF RapidOCR DLL 팩토리 검증 순서: 경로와 모델 파일이 존재해야 하고, HPDFRapidOCRAbiVersion이 1을 반환해야 하며, 필수 익스포트가 해석되어야 하고, HPDFRapidOCRCreate가 모델을 초기화해야 합니다. EArgumentException이나 EInvalidOperation은 인식이 시작되기 전에 미리 던져지고, 후자는 네이티브 진단 텍스트를 실어 나릅니다
검증은 일부러 미리 돕니다. 설정 문제는 어떤 모델도 초기화되기 전에 던져지므로, 엉뚱한 경로나 ABI가 인식 마감에 닿는 일이 없습니다
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는 CPU 스레드 하나, 16,777,216픽셀 입력 한도, 60,000 ms 인식 마감과 함께 ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx, ppocr_keys_v1.txt를 가리킵니다. 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는 고정폭 정수, raw 포인터, 명시적 바이트 길이만 씁니다. Delphi, C++Builder, Free Pascal은 C 호출 규약 너머로 MSVC와 공유하는 것이 아무것도 없기 때문입니다. std::string, std::vector, C++ 예외는 하나의 컴파일러와 하나의 런타임 라이브러리에 속하는 레이아웃과 언와인딩 모델을 갖습니다. 그중 무엇이든 경계를 넘게 하면 실패는 깨진 스택이나 엉뚱한 할당자가 해제한 힙 블록이지 깔끔한 오류가 아닙니다

그래서 ABI 버전 1은 짧은 규칙 목록을 따릅니다. 모든 익스포트는 cdecl이고 int32_t 상태를 반환하며, 1은 성공, 0은 실패를 뜻합니다. 실패할 수 있는 모든 함수는 호출자 소유의 진단 버퍼와 바이트 단위 용량을 받습니다. DLL은 크기에 맞게 잘린 NUL 종료 UTF-8 메시지를 쓰고, 어댑터는 자기 4,096바이트 버퍼의 마지막 바이트에 강제 종료자를 놓고 디코딩합니다. 각 익스포트 본문은 catch (const std::exception &)와 catch (...) 둘 다를 갖춘 try로 감싸져 있으므로, ONNX Runtime 오류, OpenCV 어서션, 잘못된 사전은 상태 0 더하기 텍스트가 되지 Pascal 코드로 탈출하는 예외가 되지 않습니다

익스포트역할어댑터가 해석하는 시점
HPDFRapidOCRAbiVersion1을 반환, 다른 값은 거부됨가장 먼저, 그 무엇보다도
HPDFRapidOCRCreate탐지, 선택적 분류, 인식 모델과 사전을 로드팩토리에서
HPDFRapidOCRRecognize하나의 비트맵을 돌리고 텍스트 행마다 콜백 하나를 내보냄팩토리에서
HPDFRapidOCRDestroy모델 인스턴스를 해제팩토리에서
HPDFRapidOCRSetReadingDirection선택적 오른쪽에서 왼쪽 행 순서, v2.775.0에서 추가RightToLeft가 설정된 경우만

선택적 익스포트는 일부러 느긋하게 해석됩니다. 그것이 없는 v2.774.0 DLL도 왼쪽에서 오른쪽 요청은 여전히 서빙합니다. DLL은 DLL 자신의 폴더 더하기 기본 안전 디렉터리를 커버하는 검색 플래그로 LoadLibraryEx로 로드되므로, HotPDFRapidOCR.dll 곁에 둔 ONNX Runtime이나 OpenCV 의존성이 PATH를 건드리지 않고 발견됩니다. 모델과 사전 경로는 UTF-8로 이동하고 DLL은 wide-character API로 파일을 열기 전에 엄격 모드 MultiByteToWideChar로 변환하므로, 중국어나 키릴 사용자 이름 아래의 모델 디렉터리도 바이트별로 넓혀져 엉터리가 되는 대신 동작합니다

한 규칙은 헤더가 아니라 빌드에 삽니다. DLL은 ONNX Runtime과 OpenCV를 정적 링크하고, 기본 CMake 구성은 정적 릴리스 CRT(/MT)를 씁니다. /MD로 컴파일된 정적 라이브러리가 /MT DLL에 섞이면 최선은 링크 오류, 최악은 독립된 두 힙입니다. 그러니 공급하는 라이브러리는 DLL이 쓰는 CRT 모드와 일치해야 합니다

TBitmap과 텍스트 행 사이에서 무슨 일이 벌어질까?

HotPDF는 렌더링된 페이지의 독립적인 top-down BGR 스냅샷을 DLL에 넘기고, DLL은 인식된 텍스트 행마다 콜백 하나를 어댑터가 반환 전에 복사해야 하는 빌린 UTF-8 텍스트와 함께 돌려줍니다

Delphi에서 어댑터는 페이지 비트맵을 사유 TBitmap에 대입하고, pf24bit을 강제하고, 음수 biHeight로 GetDIBits를 써 행을 읽어 4바이트 정렬로 패딩된 top-down 행을 얻습니다. 그 stride는 명시적으로 넘겨집니다. FPC에서는 CreateIntfImage를 통해 읽는데, LCL scanline 쓰기가 GDI 핸들을 새로 고치지 않고 raw 이미지를 갱신할 수 있기 때문입니다. 호출자의 비트맵은 결코 수정되지 않고, 픽셀 예산(MaxPixels, 기본 16,777,216, 최대 67,108,864까지 설정 가능)과 치수당 32,767픽셀 한도는 스냅샷 버퍼가 할당되기 전에 검사됩니다

비트맵에서 텍스트 레이어까지의 HotPDF RapidOCR DLL 파이프라인: 어댑터가 페이지를 top-down pf24bit BGR로 스냅샷하고, DLL이 패딩, 탐지, 정렬, 크롭 인식을 하며, 빌린 UTF-8 텍스트, 박스, 신뢰도와 함께 행마다 콜백 하나를 전달하고, 어댑터가 텍스트 레이어 커밋 전에 각 행을 검증합니다
픽셀은 스냅샷으로 ABI를 한 번 건너고, 행은 콜백으로 한 번에 하나씩 돌아오며, 모든 검사를 통과하기 전에는 아무것도 검색 가능한 레이어에 닿지 않습니다

DLL 안에서 스냅샷은 흰 픽셀 50개로 패딩되고, 텍스트 영역은 최대 변 1,024픽셀로 탐지되며, 박스는 가로 행들로 정렬되고, 각 크롭은 인식 전에 각도 분류기가 선택적으로 회전시킵니다. 그런 다음 각 텍스트 행은 const char*와 바이트 수, 원본 이미지 픽셀 기준 정수 박스, 평균 문자 신뢰도를 받는 콜백을 통과합니다. 텍스트 포인터는 콜백 동안만 유효하므로 어댑터는 즉시 복사하고, 받아들이는 것에 엄격합니다:

  • UTF-8은 MB_ERR_INVALID_CHARS로 디코딩됩니다. malformed 시퀀스는 검색 가능한 레이어에 치환 문자를 만드는 대신 페이지를 실패시킵니다
  • C0과 C1 제어 문자는 거부되고, 공백뿐인 행은 건너뜁니다
  • 박스는 비트맵 안에 있어야 하고 신뢰도는 0부터 1 사이의 유한 값이어야 합니다
  • 텍스트는 요청의 MaxTextCodeUnits에 대조해 세며 호출당 1,048,576 UTF-16 유닛의 하드 천장이 있고, supplementary plane 문자는 두 유닛을 먹습니다
  • 콜백 안의 Pascal 예외는 그 자리에서 잡혀 저장되고 0 반환으로 바뀌며, 이는 DLL이 멈춰 실패를 보고하게 합니다. 저장된 메시지가 이어서 진단이 됩니다

튜닝에 중요한 결과가 둘 있습니다. 첫째, 출력의 단위는 단어가 아니라 행입니다. 각 행은 MaxWords 슬롯 하나를 소비하고, Info.AcceptedWordCount와 Info.DroppedWordCount는 행을 세며, 검색 하이라이트는 행 박스를 덮습니다. 둘째, MinimumConfidence(기본 0.5)는 행의 평균 문자 신뢰도와 비교되므로, 깨끗한 스무 자 사이에 읽을 수 없는 한 자가 있는 행은 보통 살아남습니다. DLL은 baseline을 공급하지 않으므로 텍스트 레이어 파이프라인이 박스에서 추정합니다. 빈 페이지는 0행으로 성공하고, 어떤 실패든 부분 결과를 지워 다중 페이지 커밋이 all-or-nothing으로 유지됩니다

모델 소유와 thread safety

각 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 세션의 intra-op와 inter-op 스레드 수를 모두 정하고, DLL은 그것을 활성 프로세서 수로 클램프합니다. 하나의 엔진을 공유하는 두 스레드는 병렬로 돌지 않습니다. 둘째가 락을 기다립니다. 그 대기는 눈먼 EnterCriticalSection이 아닙니다. 어댑터는 25 ms마다 TryEnterCriticalSection을 호출하고 시도 사이에 취소 토큰과 마감을 검사하므로, 줄 선 요청은 여전히 취소되거나 시간 초과될 수 있습니다. 진짜 병렬성이 필요하면 워커마다 엔진을 하나 만들고 각 엔진이 메모리에 자기 모델 사본을 들고 있다는 걸 받아들이세요

해체 순서는 엔진 소멸자가 정합니다. HPDFRapidOCRDestroy가 먼저 모델 인스턴스를 해제하고, 이어서 FreeLibrary가 DLL을 언로드합니다. 네이티브 쪽에서 모델 초기화도 똑같이 신중합니다. 탐지기와 분류기 세션이 이미 만들어진 뒤 인식 모델이 실패하면 오류를 보고하기 전에 그 세션들을 해제하고, 사전 클래스 개수는 첫 페이지가 아니라 초기화 중에 모델 출력과 대조해 검사합니다

네이티브 OCR 호출은 왜 추론 중간에 죽일 수 없을까?

네이티브 RapidOCR 호출은 추론 중간에 죽일 수 없습니다. 여러분의 스레드 위, 여러분의 프로세스 안, 중단을 받아들이지 않는 ONNX Runtime 세션 한가운데에서 돌기 때문입니다. 그래서 HotPDF DLL 어댑터의 취소는 협력적입니다. DLL은 탐지 전후, 분류 후, 인식된 각 행 후에 abort 콜백을 호출하고, 콜백이 0을 반환하는 첫 체크포인트에서 멈춥니다. 시작된 단일 ONNX Run은 먼저 끝을 봅니다

대안들은 기다림보다 나쁩니다. TerminateThread는 CRT 힙 락, ONNX Runtime의 thread pool, 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 사이의 핵심 트레이드오프이고, 어느 쪽도 모든 행에서 이기지 않습니다:

HotPDF OCR 어댑터 트레이드오프: 프로세스 어댑터는 페이지마다 워커를 띄우고 모델을 로드하지만 죽일 수 있고 크래시를 가둡니다. 반면 인프로세스 RapidOCR DLL은 모델을 한 번 로드하고 협력적 체크포인트에서만 멈추며 주소 공간을 공유하고 모델과 사전을 갖춘 DLL로 배포됩니다
워크로드별로 고르세요. 한 번에 한 페이지씩 처리하는 데스크톱 앱은 따뜻한 DLL의 이점을 보고, 신뢰할 수 없는 스캔을 받아들이는 서버는 프로세스 벽의 비용을 치러야 합니다
  • 시작 비용: Tesseract와 Python RapidOCR 어댑터는 페이지마다 프로세스를 띄우고 모델을 로드합니다. DLL은 엔진당 모델을 한 번 로드
  • 멈추기: 자식 프로세스는 통째로 종료할 수 있고 Python 워커는 kill-on-close Job Object 안에서 돌아 프로세스 트리 전체가 함께 갑니다. DLL은 단계와 행 경계에서만 멈출 수 있음
  • 결함 격리: tesseract.exe의 크래시는 한 페이지를 실패시킵니다. DLL 안의 access violation은 여러분의 프로세스를 끌어내림
  • 배포: 프로세스 어댑터는 설치된 프로그램이나 Python 환경이 필요합니다. DLL은 자기 자신, 모델, 사전이 필요하고 애플리케이션 비트니스와 맞아야 함
  • 메모리: 프로세스 어댑터는 자식이 끝나면 모든 것을 해제합니다. DLL 엔진은 마지막 인터페이스 참조가 해제될 때까지 모델을 상주시킴

한 번에 한 페이지씩 OCR하는 인터랙티브 데스크톱 애플리케이션에는 보통 DLL의 응답성이 이깁니다. 신뢰할 수 없는 스캔을 밤낮으로 받아들이는 서버에는 프로세스 경계가 그 시작 비용을 할 만합니다

HotPDFRapidOCR.dll 빌드와 배포

HotPDFRapidOCR.dll은 Native/RapidOCR의 C++ 소스에서 MSVC, C++17, Windows SDK, CMake 3.20 이상으로 빌드되며, 네이티브 네트워크 소스와 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이 언로드됨
  • 하나의 엔진은 한 번에 하나의 인식만 돌림. 병렬 워커에는 엔진을 여러 개 만들고 모델 사본마다 메모리를 예산화
  • 출력은 평균 문자 신뢰도를 단 텍스트 행마다 엔트리 하나이며 THPDFOCRTextLayerOptions.MinimumConfidence로 필터링됨
  • 취소와 TimeoutMilliseconds는 협력적임. 진행 중인 ONNX run은 항상 완료됨
  • DLL 비트니스는 애플리케이션과, 정적 ONNX Runtime과 OpenCV 라이브러리의 CRT 모드는 DLL과 맞출 것
  • THPDFRapidOCRDLLOptions.ForLanguage(v2.775.0)로 엔진마다 언어 프로파일을 고를 것. 하나의 엔진이 스스로 언어를 탐지하지 않음

네이티브 RapidOCR 어댑터, 프로세스 기반 OCR 어댑터들, 그것들을 먹여 주는 페이지 렌더러, 보이지 않는 Unicode 텍스트 레이어 라이터는 Delphi와 C++Builder용 네이티브 VCL PDF 컴포넌트인 HotPDF에 함께 실려 나갑니다. 문서 캡처나 아카이빙 애플리케이션이 대상 머신에 Python 런타임 없이 검색 가능한 출력을 필요로 한다면, HotPDF Delphi PDF 컴포넌트가 DLL과 모델만 배포하면 되는 전체 파이프라인을 제공합니다