HotPDF는 HPDFCreateTesseractDLLOCREngine을 통해 여러분의 Delphi 프로세스 안에서 Tesseract를 돌립니다. v2.772.0에서 추가된 이 팩토리는 Tesseract 5 호환 DLL을 동적으로 로드하고, 그 C API(TessBaseAPIInit2, TessBaseAPIRecognize, result iterator)를 몰아주고 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개 필수 익스포트 전부가 엔진이 반환되기 전에 해석되어야 합니다. 설정 실수는 EArgumentException을, 로드에 실패한 DLL은 아키텍처와 의존성 확인 힌트를 단 Windows 오류 코드와 함께 EOSError를 던집니다
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으로 설정합니다. 픽셀 예산은 보이는 것보다 중요합니다. 기본 300 DPI의 US Letter 페이지는 2,550 × 3,300 픽셀, 약 840만으로 들어갑니다. 같은 페이지가 600 DPI면 5,100 × 6,600, 약 3,370만이고 어댑터는 Tesseract가 픽셀 하나 보기 전에 거부합니다. MaxPixels를 올리거나(천장은 67,108,864) DPI를 지금 그대로 두세요. 각 변도 32,767픽셀로 상한이 있습니다
DLL은 DLL 자신의 폴더 더하기 기본 안전 디렉터리를 커버하는 검색 플래그로 LoadLibraryEx로 로드되므로, Tesseract가 의존하는 이미지 라이브러리들이 PATH나 현재 디렉터리를 건드리지 않고 그 곁에 살 수 있습니다. HotPDF는 OCR 런타임이나 모델을 묶거나 다운로드하지 않습니다. 둘 다 여러분이 공급합니다
tesseract.exe 어댑터와 비교하면 무엇이 바뀔까?
DLL 어댑터는 더 풍부한 출력과 더 낮은 페이지당 오버헤드를 위해 프로세스 격리를 맞바꿉니다. 두 어댑터 모두 같은 텍스트 레이어 파이프라인에 꽂히므로, 좌표 사상, 신뢰도 필터링, all-or-nothing 커밋은 동일합니다. 다른 것은 픽셀이 들어가고 워드가 나오는 방식입니다
| 측면 | tesseract.exe 어댑터 | Tesseract DLL 어댑터 |
|---|---|---|
| 팩토리 | HPDFCreateTesseractOCREngine | HPDFCreateTesseractDLLOCREngine |
| 픽셀 입력 | 사유 임시 디렉터리의 BMP 파일 | 메모리의 8비트 그레이스케일 버퍼 |
| 워드 출력 | 워드 수준 TSV, 64 MiB 상한 | result iterator, 워드별 UTF-8 |
| baseline | 없음 | TessPageIteratorBaseline에서 통과 |
| 페이지 분할과 엔진 모드 | 자동 분할만 | THPDFTesseractPageSegMode, THPDFTesseractEngineMode |
| 타임아웃 | 하드: 자식 프로세스가 종료됨 | 협력적: Tesseract가 알아차려야 함 |
| 크래시와 메모리 격리 | 별도 프로세스 | 없음, 여러분의 주소 공간 공유 |
사라지지 않는 비용이 하나 있습니다. Recognize 호출마다 자기 API 인스턴스를 만들고 TessBaseAPIInit2를 호출하므로, 언어 모델은 엔진당 한 번이 아니라 페이지마다 초기화됩니다. 운영체제 파일 캐시가 재로드를 누그러뜨리지만, 큰 다국어 모델 집합에서는 여전히 페이지당 고정 비용의 주벽이고 인식 마감에도 반영됩니다. 인프로세스 RapidOCR DLL 엔진은 반대 설계로 엔진 수명 동안 ONNX 모델을 상주시킵니다. 경계 문제(C ABI, 빌린 버퍼, 중단 불가능한 네이티브 작업)는 같은 계열입니다
Delphi는 Tesseract monitor 구조체를 왜 복사할 수 없을까?
Delphi는 Tesseract 진행 monitor를 안전하게 미러링할 수 없습니다. ETEXT_DESC가 버전 의존적인 내부 필드를 담고 있어, 손으로 복사한 레코드는 일부 빌드에서 cancel 콜백과 마감을 엉뚱한 오프셋에 놓기 때문입니다. 그럴 때 시끄럽게 실패하는 것도 없습니다. 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 양쪽에서 1바이트이고 TessCancelFunc의 C bool과 맞습니다. 4바이트 Windows BOOL이나 Delphi LongBool은 교환 가능해 보이지만 그렇지 않습니다. 한쪽은 바이트 하나를 쓰고 다른 쪽은 넷을 읽으면 반환 레지스터의 상위 바이트는 거기 남아 있던 것이고, false가 true로 도착할 수 있습니다. 같은 헤더가 일을 더 복잡하게 합니다. TessPageIteratorBoundingBox 같은 함수는 int를 반환하는데 HotPDF는 이를 Integer로 선언합니다. API 전체에 하나의 규약을 가정하는 대신 모든 반환 값의 C 타입을 읽으세요
둘째 세부는 콜백이 결코 예외를 던지지 않는다는 것입니다. Tesseract의 C++ 프레임을 통과해 언와인딩하는 Delphi 예외는 undefined behavior이므로, HotPDF 콜백은 취소 토큰과 단조 GetTickCount64 값만 읽습니다. 어댑터는 TessBaseAPIRecognize가 반환한 뒤 그 결과를 취소나 타임아웃 진단으로 바꾸고, 네이티브 반환 코드와 무관하게 그 검사를 수행합니다
Delphi 쪽은 어떤 네이티브 포인터를 소유할까?
HotPDF Tesseract DLL 어댑터는 요청당 세 네이티브 오브젝트, 즉 API 인스턴스, monitor, result iterator를 소유하고 나머지는 모두 빌립니다. Recognize 호출마다 자기 집합을 만들어 finally 블록에서 해제합니다. TessResultIteratorDelete, 이어서 TessMonitorDelete, 이어서 TessBaseAPIDelete 순입니다. 엔진 인터페이스를 해제하면 라이브러리가 언로드됩니다
TessResultIteratorGetPageIterator는 새 오브젝트가 아니라 result iterator 안을 들여다보는 빌린 보기를 반환합니다. HotPDF는 그것을TessPageIteratorBoundingBox와TessPageIteratorBaseline에 쓰고 결코 해제하지 않습니다. 따로 삭제하면 같은 메모리를 두 번 해제합니다TessResultIteratorGetUTF8Text는 DLL 자신의 런타임이 할당한 문자열을 반환합니다. HotPDF는 복사한 뒤finally블록에서TessDeleteText로 돌려줍니다. PascalFreeMem은 엉뚱한 힙에서 해제했을 겁니다- 워드 텍스트는 엄격한 UTF-8 검증으로 디코딩되고 변환 전 길이 검사를 받습니다. 제어 문자가 있거나, malformed UTF-8이거나, 이미지 밖의 박스, 뒤집힌 사각형, 0–100 밖의 신뢰도를 가진 워드는 조용히 수선되는 대신 요청을 실패시킵니다
- 요청당 총 텍스트는 1,048,576 UTF-16 code unit으로 제한되고, 워드 수는
ApplyLoadedOCRTextLayer가 내려온 요청 예산에 들어야 합니다
신뢰도는 0–100으로 도착해 0–1로 조정되므로, THPDFOCRTextLayerOptions.MinimumConfidence는 모든 엔진에서 같은 뜻입니다. Tesseract가 baseline을 보고하면 두 끝점이 통과되고, 그렇지 않으면 텍스트 레이어 파이프라인이 TSV 입력에서와 정확히 같게 기하 추정으로 폴백합니다
DLL에 닿기 전에 열거형을 왜 검증할까?
HotPDF는 범위 검사 전에 PageSegMode와 EngineMode의 raw 서수를 Integer에 복사합니다. 컴파일러가 열거 변수가 항상 선언된 값을 담는다고 가정해 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나 페이지 레이아웃 분석 중에는 그렇지 않으므로, 느린 모델 로드나 병적인 레이아웃은 타임아웃이 보고되기 전에 마감을 넘어 달릴 수 있습니다. 픽셀과 출력 예산도 네이티브 라이브러리 자신의 메모리 사용을 제한하지 않습니다. 죽일 수 있는 워커가 필요하면 프로세스 어댑터를 쓰세요. 그것은 정직한 트레이드오프이지 빠진 기능이 아닙니다
페이지 분할은 어려운 입력에서 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;
타임아웃은 진단 Tesseract DLL OCR timed out을 단 otlsEngineError로 표면화하고, 취소된 토큰은 otlsCancelled로 표면화합니다. 두 경우 모두 ApplyLoadedOCRTextLayer는 커밋 트랜잭션을 시작하기 전에 선택된 모든 페이지를 인식했으므로, 50페이지 중 40페이지의 실패는 불러온 문서를 그대로 둡니다. tpsSingleLine, tpsSingleBlock, tpsSparseText는 분할만 바꾼다는 점에 유의하세요. 어느 것도 기울어진 스캔을 바로세우지 않습니다
Free Pascal과 Lazarus: 낡은 픽셀과 잃어버린 중국어
두 Tesseract 팩토리는 FPC 전용 수정 두 건을 거쳐 v2.772.1부터 Windows Free Pascal과 Lazarus Win32, Win64 빌드에서 동작합니다. 먼저 타깃 아키텍처로 Lazarus 패키지를 다시 빌드하세요. 전반적인 포팅은 Free Pascal과 Lazarus Win64의 HotPDF가 다룹니다
첫 수정은 픽셀에 관한 것입니다. scanline을 통해 쓰인 LCL TBitmap은 Windows 비트맵 핸들을 새로 고치지 않고 raw 이미지를 갱신할 수 있어서, 그 핸들에 대한 GetDIBits는 옛 픽셀을 돌려줍니다. 증상은 애매한 것이었습니다. 비트맵에 직접 그린 텍스트는 인식되는데, HotPDF의 PDF 렌더러가 그린 페이지는 빈 워드 목록을 내놓았죠. FPC에서 어댑터는 이제 CreateIntfImage로 포맷을 인식하는 스냅샷을 읽는데, 이것은 raw 이미지의 픽셀 포맷과 행 순서를 존중합니다. Delphi 빌드는 사유 24비트 복사본 위에서 GetDIBits 경로를 유지합니다. 어느 빌드도 호출자의 비트맵을 수정하지 않습니다
둘째 수정은 tesseract.exe 어댑터에 속합니다. FPC의 TStringList는 ANSI 문자열을 저장하므로, 디코딩된 UTF-8 TSV 텍스트를 Lines.Text에 대입하면 시스템 ANSI 코드 페이지가 표현할 수 없는 모든 중국어나 supplementary plane 문자가 조용히 떨어져 나갔습니다. 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 레코드에 절대 복사하지 말 것 - cancel 콜백은 1바이트
Boolean결과를 단cdecl로 선언하고 예외가 빠져나가게 하지 말 것 - 반복자 텍스트는
TessDeleteText로 해제할 것. result iterator에서 얻은 페이지 반복자를 절대 해제하지 말 것 - 마감은 협력적이라고 예상할 것. 모델 초기화와 레이아웃 분석이 넘어설 수 있음
- 하드 종료나 크래시 격리가 필요하면 tesseract.exe 어댑터를 쓸 것
Tesseract DLL 어댑터, 프로세스 어댑터들, 내장 OCR 엔진은 모두 Delphi, C++Builder, Free Pascal용 HotPDF Delphi PDF 컴포넌트에 실려 나갑니다. 에디션과 다운로드는 HotPDF 제품 페이지를 보세요