HotPDF는 자체 네이티브 RapidOCR DLL 어댑터로 Delphi에서 중국어와 다국어 OCR을 수행합니다. THPDFRapidOCRDLLOptions.ForLanguage는 'zh-CN', 'zh-TW', 'ru', 'ar' 같은 언어 태그를 맞는 인식 모델과 문자 사전으로 사상하고, THotPDF.ApplyLoadedOCRTextLayer는 인식된 행들을 스캔 PDF 페이지 위의 보이지 않고 검색 가능한 Unicode 텍스트 레이어로 바꿉니다
라틴 문자 데모를 돌아가게 하는 건 쉬운 편입니다. 재미있는 실패는 번체 중국어나 러시아어로 바꿨더니 출력이 자신만만하고 형태가 제대로 된 엉터리가 되거나, 모든 행이 조용히 마지막 문자를 잃거나, 아랍어 페이지가 텍스트 박스 순서가 엉킨 채 돌아올 때 시작됩니다. 이들 중 하나도 저절로 예외를 던지지 않습니다. HotPDF v2.775.0에서 추가된 언어 프리셋은 대개 그 틈을 메우려고 존재하고, 아래의 네 함정은 네이티브 코드를 한 번도 만지지 않더라도 이해할 가치가 있습니다. 각각이 다른 식이면 하루를 쫓아다니게 될 증상을 설명해 주기 때문입니다
ForLanguage는 모델과 사전을 어떻게 고르나?
THPDFRapidOCRDLLOptions.ForLanguage는 태그를 아홉 프로파일 중 하나로 해석해 모델 디렉터리 아래의 <profile>/recognition.onnx와 <profile>/dictionary.txt를 가리키는 옵션을 반환하되, 공유 탐지기와 선택적 각도 분류기, 그리고 THPDFRapidOCRDLLOptions.Default의 스레드, 픽셀, 타임아웃 기본값은 유지합니다. 이 메서드는 태그를 소문자로 바꾸고 밑줄을 하이픈으로 바꾸며 주변 공백을 자르므로, 'zh_TW', 'ZH-tw', ' zh-tw '가 모두 같은 프로파일에 착지합니다. 별칭은 접두 매치가 아니라 명시적 목록입니다. 'zh-Hant-TW'는 목록에 있으니 받아들여지고, 목록에 없는 임의의 지역 변형은 어떤 모델도 로드되기 전에 EArgumentException을 던집니다
| 프로파일 | 언어 | 예시 태그 | 고정 모델 |
|---|---|---|---|
ch | 간체 중국어와 영어 | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | 번체 중국어 | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | 영어 | en, en-US, en-GB, eng | PP-OCRv4 |
latin | 프랑스어, 독일어, 스페인어, 포르투갈어, 이탈리아어, 네덜란드어, 튀르키예어 | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | 일본어 | ja, ja-JP, jpn | PP-OCRv4 |
korean | 한국어 | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | 러시아어, 우크라이나어, 불가리아어, 벨라루스어 | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | 아랍어, 페르시아어, 우르두어 | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | 힌디어, 마라티어, 네팔어 | hi, mr, ne | PP-OCRv4 |
어댑터 자체는 아무것도 다운로드하지 않습니다. 파일은 함께 실려 나가는 헬퍼로 한 번만 공급하면 됩니다. 예컨대 tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic(아홉 프로파일 전부는 -Language All)처럼요. 헬퍼는 공유 탐지기와 분류기를 Default가 기대하는 루트 파일 이름에 놓습니다. 그 뒤로 간체 중국어 스캔은 몇 줄로 검색 가능해집니다. 엔진 배관은 인프로세스 RapidOCR DLL과 그 ABI 경계에 관한 글이 기술한 같은 IHPDFOCREngine 이음새이므로, 이 글은 언어에 집중합니다
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
procedure MakeChineseScanSearchable(const SourceFile, TargetFile: string);
var
Doc: THotPDF;
Engine: IHPDFOCREngine;
Models: THPDFRapidOCRDLLOptions;
Layer: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
// ch/recognition.onnx + ch/dictionary.txt, 공유 탐지기와 분류기
Models := THPDFRapidOCRDLLOptions.ForLanguage('zh-CN');
Engine := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile(SourceFile) < 1 then
raise Exception.Create('Cannot load ' + SourceFile);
Layer := THPDFOCRTextLayerOptions.Default; // 300 DPI, MinimumConfidence 0.5
// 빈 페이지 목록은 모든 페이지를 뜻함, 이미 텍스트가 있는 페이지는 건너뜀
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
' lines, ', Info.UniqueScalarCount, ' distinct characters');
Doc.SaveLoadedDocument(TargetFile);
finally
Doc.Free;
end;
end;
그 출력의 세부 두 가지는 짚을 가치가 있습니다. 네이티브 파이프라인은 단어가 아니라 탐지된 텍스트 행마다 결과 하나를 반환하므로, 여기서 AcceptedWordCount는 행을 세고, MinimumConfidence는 행 전체의 평균 문자 신뢰도와 비교됩니다. 평균 0.45짜리 행은 통째로 버려지죠. UniqueScalarCount는 텍스트 레이어가 폰트와 ToUnicode 테이블에 사상해야 한 서로 다른 Unicode 스칼라가 몇 개였는지 보고하는데, CJK 텍스트가 소수의 라틴 폴백 대신 실제로 도착했는지 확인하는 유용한 점검입니다. 엔진 인터페이스는 문서들에 걸쳐 살려 두세요. 모델 초기화는 팩토리에서 일어나는 비싼 단계입니다
인식 모델만 바꾸면 왜 엉터리가 나올까?
CTC 인식 모델은 결코 문자를 출력하지 않고 클래스 인덱스만 출력하며, 인덱스 1,204를 글리프로 바꾸는 유일한 존재가 사전입니다. ch/recognition.onnx를 cyrillic/recognition.onnx로 바꾸되 중국어 사전을 그대로 두면, 모델은 합법적인 키릴 인덱스를 쏟아내고 옛 사전은 그것을 무작위 한자로 번역합니다. 결과는 텍스트처럼 보이고, UTF-8 검증을 통과하고, 검색하면 정확히 아무것도 안 나옵니다. 그래서 ForLanguage는 RecognitionModel과 CharacterDictionary를 항상 함께 설정하고, 손으로 만든 옵션도 하나만 바꿔서는 안 됩니다
사전 크기를 모델 출력 폭과 비교하는 자명한 안전 검사는 필요하지만 충분하지 않습니다. 두 사전은 서로 다른 순서로 같은 개수의 엔트리를 가질 수 있고, 순서에서 한 칸 어긋나면 모든 문자가 한 code point씩 밀립니다. 그래서 HotPDF는 팩토리가 모델을 초기화할 때 두 단계로 검사합니다. 첫째, 출력 클래스 수는 사전 엔트리 더하기 2와 같아야 합니다. 둘째, ONNX 파일이 character 메타데이터 목록을 내장하고 있으면 모든 사전 엔트리를 순서대로 그것과 비교하고, 어긋나면 그럴듯한 엉터리를 나중에 만드는 대신 EInvalidOperation과 네이티브 진단으로 초기화가 실패합니다
"더하기 2"는 클래스 배치에서 나옵니다. 클래스 0은 CTC blank이고, 클래스 1부터 N까지는 파일 순서의 사전 행들이며, 마지막 클래스는 공백입니다. 일부 사전은 자체 공백 엔트리도 갖고 있는데, 그 행은 있는 그대로 유지돼야 합니다. 선의의 Trim이 실제 피해를 입히는 지점이 바로 여기입니다. 공백 하나짜리 엔트리를 빈 문자열로 만들어 테이블을 밀거나 부수죠. 안전한 정규화는 뒤따르는 캐리지 리턴 제거뿐이므로, CRLF 행 끝으로 저장된 사전은 올바르게 로드되고, UTF-8 BOM이나 빈 행, 탭을 담은 엔트리는 거부됩니다. 아래 스케치가 Pascal로 배치를 보여 줍니다. 설명용 코드지 HotPDF API가 아닙니다
// 설명용일 뿐: CTC 인식기가 기대하는 클래스 테이블
uses
SysUtils, IOUtils;
function BuildCTCClassTable(const FileName: string): TArray<string>;
var
Text, Entry: string;
Lines: TArray<string>;
I, Last: Integer;
begin
Text := TEncoding.UTF8.GetString(TFile.ReadAllBytes(FileName));
if (Text <> '') and (Text[1] = #$FEFF) then
raise EArgumentException.Create('Dictionary must be UTF-8 without a BOM');
Lines := Text.Split([#10]);
Last := High(Lines);
if (Last >= 0) and (Lines[Last] = '') then
Dec(Last); // 파일 끝의 개행
SetLength(Result, Last + 3);
Result[0] := ''; // 클래스 0: CTC blank
for I := 0 to Last do
begin
Entry := Lines[I];
if (Entry <> '') and (Entry[Length(Entry)] = #13) then
SetLength(Entry, Length(Entry) - 1); // CRLF: CR만 제거
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // 절대 Trim 금지: ' '는 클래스
end;
Result[Last + 2] := ' '; // 마지막 클래스: 공백
// Length(Result)는 모델 출력 클래스 수와 같아야 함
end;
greedy CTC 디코딩은 실제로 무엇을 할까?
greedy CTC 디코딩은 매 시간 단계에서 가장 점수 높은 클래스를 고르고, 연속된 반복을 한 문자로 접으며, blank 클래스는 버립니다. blank가 진짜 겹친 글자를 살려 주는 존재죠. 인식 모델은 텍스트 행을 좁은 세로 조각들의 나열로 보고, 각 조각, 즉 시간 단계마다 모든 클래스의 확률을 출력합니다. AA中을 담은 행은 argmax 시퀀스 A A blank A 中 space를 내놓을 수 있습니다. 앞의 두 A 단계를 접으면 A 하나가 되고, blank가 그것을 다음 A와 갈라 놓으며, 결과는 뒤따르는 공백까지 온전한 AA中 입니다. blank 규칙이 없다면 book과 bok을 구분할 수 없었겠죠
디코더가 십여 줄밖에 안 되므로 경계를 틀리기 쉽고, 실패는 소리 없습니다. 안쪽 argmax 루프가 한 클래스 전에 멈추면 공백 클래스는 이길 수 없고 모든 행은 단어 간격 없이 돌아오는데, 이는 영어와 라틴 페이지의 구 검색을 망가뜨립니다. 바깥 루프가 한 시간 단계 전에 멈추면 모든 행의 마지막 문자가 사라지는데, 짧은 행에서는 텍스트의 3분의 1이 될 수 있습니다. 반복 가드가 blank로 리셋되지 않으면 ll 같은 겹친 문자나 谢谢 같은 중국어 중첩이 하나로 접힙니다. HotPDF 디코더는 마지막 클래스와 마지막 시간 단계를 포함하고, blank로 갈라진 반복을 유지하며, 추가로 유한하지 않거나 0부터 1 밖인 점수와 사전과 맞지 않는 모든 클래스 수를 거부합니다. 같은 로직을 Pascal 설명으로 보여 드립니다
// 설명용일 뿐: 올바른 경계를 가진 greedy CTC 디코딩.
// Scores는 Steps * Classes 개의 확률을 담고 시간 단계마다 한 행
function GreedyCTCDecode(const Scores: array of Single;
Steps, Classes: Integer; const Characters: array of string): string;
var
Step, C, Best, Previous: Integer;
BestScore: Single;
begin
if (Classes < 3) or (Length(Characters) <> Classes) or
(Length(Scores) <> Steps * Classes) then
raise EArgumentException.Create('Model output does not match the dictionary');
Result := '';
Previous := 0; // 클래스 0은 CTC blank
for Step := 0 to Steps - 1 do // 마지막 시간 단계 포함
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // 마지막 클래스(공백) 포함
if Scores[Step * Classes + C] > BestScore then
begin
Best := C;
BestScore := Scores[Step * Classes + C];
end;
if (Best <> 0) and (Best <> Previous) then
Result := Result + Characters[Best];
Previous := Best; // blank가 반복 가드를 리셋
end;
end;
greedy 디코딩은 사용할 수 있는 가장 정확한 CTC 전략이 아닙니다. 언어 모델을 갖춘 beam search가 모호한 조각 몇을 고칠 수 있죠. 300 DPI 인쇄 문서에서는 greedy 결과가 보통 모델이 낼 수 있는 전부이고, 디코더는 모델 약점을 보상할 자리가 아닙니다. 라틴 PP-OCRv3 모델은 예컨대 깨끗한 입력에서도 ñ를 n으로 읽을 수 있습니다. HotPDF는 후처리 문자 치환으로 그것을 덮지 않습니다. 스페인어를 고치는 치환 테이블은 다른 것을 부수고, 검색 가능한 레이어의 틀린 문자는 정직한 미스보다 나쁘기 때문입니다
HotPDF는 오른쪽에서 왼쪽 아랍어를 포함해 텍스트 행을 어떻게 정렬하나
HotPDF는 탐지된 텍스트 박스를 위에서 아래로 정렬하고, 작은 박스 높이의 절반 이상 수직으로 겹치면 박스들을 한 행으로 묶으며, 각 행을 왼쪽에서 오른쪽으로, RightToLeft가 켜져 있으면 오른쪽에서 왼쪽으로 정렬합니다. 인식된 각 행 안의 문자들은 결코 뒤집히지 않습니다. 묶음이 중요한 이유는 탐지기가 하나의 시각적 행을 여러 박스로 자르는 일이 잦기 때문입니다. 넓은 간격으로 갈라진 레이블과 값 같은 것이죠. 순수한 top 좌표 정렬은 꼭대기가 한두 픽셀 어긋날 때마다 그것들을 이웃 행과 뒤섞었을 겁니다
아랍어 프리셋은 RightToLeft := True를 설정해 DLL에게 각 행의 박스를 오른쪽 모서리 기준으로 오른쪽 여백에서 안쪽으로 정렬하게 합니다. 효과는 그게 전부입니다. 모델이 행에 대해 반환하는 텍스트는 이미 Unicode 논리 순서, 아랍어 독자가 읽고 타이핑하는 순서이고, 그것이 PDF 텍스트 추출과 검색이 기대하는 순서이기도 합니다. 디버거에서 "제대로 보이게" 하려고 문자열을 기계적으로 뒤집으면 검색, 복사 붙여넣기, 스크린 리더가 깨집니다. 양방향 표시와 글리프 셰이핑은 뷰어의 소관입니다
하나의 엔진이 하나의 언어 프로파일을 맡습니다. 자동 문자 체계 탐지는 없으므로, 문자 체계를 섞은 문서는 프로파일마다 엔진 하나가 필요하고 그것을 쓰는 페이지에 적용됩니다. ApplyLoadedOCRTextLayer는 명시적 페이지 목록을 받고 각 호출을 자기만의 all-or-nothing 트랜잭션으로 커밋하므로, 그건 단순한 일입니다
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// 모르는 태그에 대해 모델이 로드되기 전에 EArgumentException을 던짐
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // 300 DPI A3 페이지를 위한 여유
Result := HPDFCreateRapidOCRDLLOCREngine(
'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
end;
procedure OCRMixedArchive(Doc: THotPDF);
var
Chinese, Arabic: IHPDFOCREngine;
Layer: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
Chinese := CreateRapidEngine('zh-TW'); // chinese_cht 프로파일
Arabic := CreateRapidEngine('ar-SA'); // arabic 프로파일, RightToLeft = True
Layer := THPDFOCRTextLayerOptions.Default;
if not Doc.ApplyLoadedOCRTextLayer([0, 1, 2], Chinese, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
if not Doc.ApplyLoadedOCRTextLayer([3], Arabic, Layer, Info) then
raise Exception.Create(string(Info.Diagnostic));
end;
MaxPixels 행에는 이유가 있습니다. DLL 옵션은 요청당 16,777,216픽셀이 기본이라 300 DPI의 A4와 US Letter는 넉넉히 커버하지만, 300 DPI의 A3 페이지는 약 3508 x 4961 픽셀, 대략 1,740만이고 요청은 예산 초과로 거부됩니다. 큰 포맷에는 MaxPixels를 올리거나(천장은 67,108,864) THPDFOCRTextLayerOptions.DPI를 내리세요. 오른쪽에서 왼쪽 정렬은 ABI 버전 1의 선택적 HPDFRapidOCRSetReadingDirection 익스포트를 씁니다. 어댑터는 RightToLeft가 설정됐을 때만 그것을 요구하므로, 오래된 DLL은 왼쪽에서 오른쪽 언어는 계속 서빙하고 아랍어에는 빠진 익스포트 이름을 밝히는 EArgumentException으로 엔진 생성에서 실패합니다
더 새 OCR 모델은 왜 로드에 실패할까?
HotPDF RapidOCR DLL은 정적 ONNX Runtime 1.14를 링크하는데, 이것은 ONNX IR 버전 10으로 저장된 모델을 읽지 못하고, PP-OCRv5 모델 같은 더 새 익스포트는 그보다 새 런타임을 요구할 수 있습니다. 그런 모델은 네이티브 진단과 함께 엔진 생성에서 실패합니다. 그 제약이 언어 팩이 "latest"가 아니라 특정 PP-OCRv3와 PP-OCRv4 인식기-사전 쌍에 고정된 이유이고, 위 표가 두 세대를 섞는 이유입니다. 고정된 쌍 하나하나가 그 런타임 아래에서 로드되고 검증되는 것들이니까요
인스톨러가 쌍을 강제합니다. 매니페스트의 모든 파일은 SHA256 해시를 달고, 해시가 다른 기존 파일은 덮어쓰이는 대신 설치를 멈추게 하며, 각 다운로드는 임시 이름 아래 착지해 해시가 맞은 뒤에만 제자리로 옮겨집니다. 그것은 사전 문제의 조용한 버전을 막습니다. 누군가 손으로 더 새 recognition.onnx를 프로파일 폴더에 떨어뜨리고, 클래스 수가 우연히 맞고, 고객이 눈에 뻔히 보이는 단어를 검색이 못 찾는다고 보고할 때까지 아무것도 실패하지 않는 경우죠. 런타임에서 어댑터는 오프라인으로 머무르고 빠진 모델을 결코 가져오지 않습니다. 인식기는 로드 시점에 모델 모양도 검증해, 높이가 32나 48픽셀로 고정되거나 동적인 NCHW 입력을 받아들이며, 동적인 것은 48로 돌립니다
아홉 프로파일 어느 것도 커버하지 않는 문자 체계가 필요하다면, RecognitionModel과 CharacterDictionary를 여러분의 파일에 가리키면 됩니다. 같은 검사가 적용되는데, 그게 포인트입니다. 어긋난 쌍은 초기화에서 실패하지 고객의 아카이브에서 실패하지 않습니다. RapidOCR 프로파일 어느 쪽도 맞지 않는 페이지에는 검색 가능한 PDF용 Tesseract 어댑터가 같은 ApplyLoadedOCRTextLayer 호출에 꽂히고, 기계 인쇄된 ASCII 폼에는 내장 템플릿 매칭 OCR 엔진이 모델을 전혀 필요로 하지 않습니다
빠른 참조: 다국어 RapidOCR 체크리스트
- 옵션은
THPDFRapidOCRDLLOptions.ForLanguage로 만들고EArgumentException은 런타임 결함이 아니라 지원되지 않는 태그로 취급 RecognitionModel과CharacterDictionary는 함께 바꿀 것, 하나만 결코 아님. 같은 클래스 수는 같은 문자 순서를 증명하지 않음- 사전은 BOM 없는 UTF-8로 유지하고 엔트리를 절대 자르지 말며, 모델이 N + 2개 클래스(blank, N 엔트리, 공백)를 갖는다고 예상
- 커스텀 CTC 디코더는 마지막 클래스와 마지막 시간 단계를 커버하고 blank로 갈라진 반복을 유지해야 함
- 언어 프로파일마다 엔진 하나를 쓰고 문자 체계가 섞인 문서에는 명시적 페이지 목록을 넘길 것
RightToLeft는 박스 순서만 바꿈. 인식된 텍스트는 Unicode 논리 순서로 유지- 모델은
Install-RapidOCRModels.ps1로 설치해 SHA256 고정이 모델과 사전 쌍을 지키게 할 것.-SkipClassifier로 설치했다면UseAngleClassifier := False로 설정 - 300 DPI A3 이상 페이지를 돌리기 전에
MaxPixels를 16,777,216 기본값 위로 올릴 것
RapidOCR 언어 프리셋, 네이티브 DLL 어댑터, OCR 텍스트 레이어 파이프라인은 Delphi, C++Builder, Windows FPC/Lazarus용 HotPDF Delphi PDF Component의 일부이며, 다국어 프로파일은 v2.775.0부터 들어 있습니다