PDFium Component는 Delphi에서 ApplyOcrSearchLayer를 통해 스캔된 PDF 페이지에 검색 가능한 텍스트 레이어를 추가합니다. 선택된 각 페이지를 렌더링하고, 그 픽셀을 여러분이 제공한 OCR 프로바이더에 넘긴 다음, 인식된 단어를 스캔 속 단어 위에 보이지 않는 텍스트 객체로 다시 써 넣습니다. 원본 페이지 이미지는 전혀 디코드되거나 다시 인코드되거나 교체되지 않으므로, 시각적 결과는 시작할 때와 바이트 단위로 동일한 페이지입니다
인식 엔진은 의도적으로 이 라이브러리의 일부가 아닙니다. PDFium은 페이지 렌더링, 좌표 매핑, 폰트 로딩, 텍스트 객체 생성, 보이지 않는 렌더 모드를 제공하지만 OCR 엔진은 담고 있지 않습니다. 그렇지 않은 척하는 것은 누군가의 인식 제품을 PDF 컴포넌트 안에 끼워 넣는 셈이 될 것입니다. 대신 인식은 IPdfOcrProvider 인터페이스 뒤에 자리합니다. 라이브러리는 고정 레이아웃의 top-origin BGRA 픽셀을 넘기고, 프로바이더는 유니코드 텍스트와 신뢰도 값, 단어 사각형을 반환합니다
검색 가능한 텍스트 레이어란 정확히 무엇일까?
스캔된 PDF는 문서의 사진입니다. 페이지 콘텐츠는 커다란 이미지 하나뿐이며, 선택하거나 검색하거나 복사하거나 색인할 대상이 아무것도 없습니다. 검색 가능한 텍스트 레이어는 렌더 모드를 보이지 않음으로 설정한 실제 텍스트 객체를 그 이미지 위에 얹는데, 그러면 뷰어는 아무것도 그리지 않으면서도 선택, 검색, 추출은 단어가 나타나는 바로 그 자리에서 찾아냅니다
위치 지정이 이 기능의 전부입니다. 보이지 않는 텍스트가 몇 포인트라도 어긋나 있으면 선택 강조 표시는 단어 옆에 놓이지 실제 단어 위에 놓이지 않으며, 단락을 복사하면 순서가 뒤바뀐 텍스트가 나옵니다. 그래서 지오메트리는 대략적인 추측이 아니라 PDFium이 페이지를 렌더링할 때 쓰는 것과 같은 변환에서 나와야 합니다
프로바이더 구현하기
프로바이더 계약은 메서드 하나입니다. 크기, 스트라이드, DPI, 픽셀 포맷, 픽셀 바이트 자체를 담은 페이지 이미지 레코드와 취소 토큰을 받아, 단어 목록이나 오류 메시지를 반환합니다:
uses
PDFium;
type
TMyOcrProvider = class(TInterfacedObject, IPdfOcrProvider)
public
function RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
end;
function TMyOcrProvider.RecognizePage(const Image: TPdfOcrImage;
const CancellationToken: IPdfCancellationToken;
out Words: TPdfOcrWords; out ErrorMessage: string): Boolean;
var
I: Integer;
begin
// Image.Pixels holds top-origin BGRA rows of Image.Stride bytes.
// Hand them to your engine, then fill one entry per recognised word
SetLength(Words, RecognisedCount);
for I := 0 to RecognisedCount - 1 do
begin
Words[I].Text := EngineWordText(I);
Words[I].Confidence := EngineWordConfidence(I); // 0..1
Words[I].Quad := TPdfOcrQuad.FromRectangle(
EngineLeft(I), EngineTop(I), EngineRight(I), EngineBottom(I));
end;
ErrorMessage := '';
Result := True;
end;
사각형이 아니라 사변형을 쓰는 이유는 스캔이 페이지와 정확히 직각을 이루는 경우가 드물기 때문입니다. 살짝 회전된 페이지 위의 단어는 평행사변형 모양을 차지하며, TPdfOcrQuad는 네 꼭짓점을 담아 기울거나 회전된 단어도 정확한 선택 영역을 유지하게 해 줍니다. 축에 정렬된 사각형만 보고하는 엔진은 퇴화된 사변형을 만들어 주는 FromRectangle을 쓰면 됩니다
단어 위치는 왜 비례적으로 스케일링할 수 없을까?
픽셀 좌표를 렌더링 너비로 나눈 뒤 페이지 너비를 곱해 페이지 좌표로 변환하고 싶은 유혹이 듭니다. 이는 회전이 없고 CropBox가 MediaBox와 동일하며 원점이 0인 페이지에서만 통하는데, 스캔된 문서 상당수는 이 조건 중 적어도 하나를 만족하지 못합니다
PDFium Component는 렌더러가 픽셀을 만들 때 쓴 것과 같은 매핑인 FPDF_DeviceToPage로 사변형의 네 꼭짓점을 각각 매핑하므로, /Rotate 항목과 오프셋이 있는 크롭 박스는 구조적으로 처리됩니다. 텍스트 객체의 아핀 행렬은 그런 다음 매핑된 세 점, 즉 왼쪽 아래, 오른쪽 아래, 왼쪽 위 꼭짓점으로부터 만들어지며, 이는 위치, 스케일, 회전, 기울임을 표현하는 데 정확히 충분합니다
텍스트 객체 자체는 실제 폰트 경계를 측정할 수 있도록 단위 폰트 크기로 만들어지며, 측정된 객체 경계는 그런 다음 목표 사변형에 매핑됩니다. 추측한 포인트 크기가 스캔된 단어와 맞아떨어지길 바라는 방식이었다면 폰트 대체가 있을 때마다 어긋났을 텐데, 먼저 측정하면 어떤 폰트로 레이어를 만들든 맞춤이 흔들리지 않습니다
문서 전체에 실행하기
옵션 레코드는 해상도, 필터링, 모든 예산을 제어합니다. 신뢰도 필터링은 보기보다 훨씬 중요합니다. 낮은 신뢰도의 쓰레기 단어는 검색 결과를 영구히 오염시키며, 잘못된 렌더링과 달리 검색이 헛소리를 반환할 때까지 아무도 눈치채지 못합니다:
var
Pdf: TPdf;
Options: TPdfOcrOptions;
Report: TPdfOcrReport;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'scanned-contract.pdf';
Pdf.LoadDocument;
Options := TPdfOcrOptions.Default;
Options.Dpi := 300; // recognition resolution
Options.MinConfidence := 0.60; // drop uncertain words
Options.SkipPagesWithText := True; // leave born-digital pages alone
Options.ContinueOnError := True; // one bad page must not stop the job
Options.MaxPixelsPerPage := 40 * 1000 * 1000;
if Pdf.ApplyOcrSearchLayer(TMyOcrProvider.Create, Options, Report) then
Pdf.SaveAs('scanned-contract-searchable.pdf');
for I := 0 to High(Report.Pages) do
if Report.Pages[I].Status = popsFailed then
Writeln(Format('page %d failed: %s',
[Report.Pages[I].PageNumber, Report.Pages[I].ErrorMessage]));
Writeln(Format('%d word(s) inserted, %d rejected, %d page(s) skipped',
[Report.InsertedWordCount, Report.RejectedWordCount,
Report.SkippedPageCount]));
finally
Pdf.Free;
end;
end;
SkipPagesWithText는 혼합된 아카이브에서 특히 강조할 만합니다. 원래 디지털로 만들어졌든 이전에 처리된 적이 있든 이미 실제 텍스트를 담고 있는 PDF에 OCR을 그냥 돌리면 두 번째 텍스트 레이어가 생기고, 중복 때문에 추출할 때 모든 단어가 두 번씩 나옵니다. 페이지별 상태 popsSkippedExistingText는 어느 페이지를 건드리지 않고 놔두었는지 정확히 알려줍니다
예산, 취소, 실패 격리
악의적이거나 단순히 거대한 문서가 부풀릴 수 있는 모든 수량에는 상한이 있습니다. 페이지당 및 전체 픽셀, 페이지당 및 전체 단어, 단어당 문자입니다. 이 모두는 페이지가 쓰이기 전에 확인되며, 픽셀 추정치는 비트맵이 할당되기 전에 페이지 크기와 DPI로부터 계산됩니다. DPI를 150에서 300으로 올리면 페이지당 메모리가 네 배가 되므로, 큰 포맷에서 배치 작업이 실패하기 시작할 때 가장 먼저 조정할 매개변수는 페이지당 상한입니다
취소 토큰은 전체 경로를 관통합니다. 순차적 렌더링, 프로바이더 호출, 단어별 삽입 루프까지입니다. 즉 400페이지짜리 파일을 인식하는 도중 취소한 사용자는 문서 끝까지가 아니라 한 페이지 안에서 멈추게 되며, 취소 가능한 순차적 렌더링에서 설명한 이 컴포넌트 다른 곳에서 쓰는 것과 같은 토큰 패턴이 여기서도 그대로 적용됩니다
실패 격리는 페이지 단위입니다. 라이브러리는 한 페이지에 삽입한 객체 핸들을 모아두었다가 모든 단어를 배치한 후 FPDFPage_GenerateContent를 한 번 호출합니다. 프로바이더 오류든 폰트 문제든 중간에 무언가 실패하면, 그 페이지에 삽입된 객체들은 역순으로 제거되고 페이지 콘텐츠가 다시 생성되므로, 실패한 페이지는 절반짜리 텍스트 레이어를 남기는 대신 원래 상태로 되돌아갑니다. 그런 다음 문서 루프는 ContinueOnError에 따라 계속되거나 멈추며, 활성 페이지는 항상 복원됩니다
이미지가 정말 그대로였는지 확인하기
가장 강력한 확인은 가장 단순하기도 합니다. 레이어를 적용하기 전과 후에 같은 크기로 페이지를 렌더링해 비트맵을 비교하는 것입니다. 보이지 않는 텍스트는 아무것도 그리지 않고 이미지 스트림도 전혀 디코드된 적이 없으므로 비트맵은 바이트 단위로 동일해야 합니다. 차이가 있다면 텍스트 레이어가 아닌 다른 무언가가 페이지를 바꿨다는 뜻입니다
그다음에는 처리된 파일에서 텍스트를 추출해 단어 위치가 스캔 위에 정확히 맞아떨어지는지 확인해 텍스트 쪽을 검증하십시오. 추출 경로는 PDF 문서에서 텍스트 추출하기에서 설명한 것과 같으며, 정렬을 빠르게 시각적으로 확인하려면 PDF 페이지를 JPEG로 변환하기처럼 페이지를 이미지로 렌더링하면 단어 상자를 스캔 위에 겹쳐볼 수 있습니다
OCR 레이어링, 렌더링, 추출, 편집은 모두 Delphi, C++Builder, Lazarus에서 같은 문서 객체 위에서 실행됩니다. 전체 API는 Delphi용 PDFium Component 페이지에서 설명합니다