OCR이 스캔 PDF를 읽는 방법, 그리고 이 도구가 Markdown을 반환하는 이유
스캔 PDF는 결국 텍스트를 찍은 그림입니다. 이 OCR 엔드포인트는 숨은 텍스트 레이어 PDF가 아니라 Markdown을 반환합니다. Auto는 기존 텍스트를 쓰고 Force는 모든 페이지를 다시 읽습니다.

스캔 PDF는 페이지를 래스터 이미지로 저장합니다. 텍스트를 찍은 사진이지 선택할 수 있는 문자 데이터가 아닙니다. OCR(optical character recognition, 광학 문자 인식)은 그 픽셀을 보고 어떤 모양이 문자인지 추정해 실제 텍스트를 내놓습니다. PDF123에서는 무료 브라우저 도구로도, POST /api/v1/misc/ocr-pdf로도 실행되며, 응답은 다시 쓴 PDF가 아니라 Markdown입니다.
스캔본은 여전히 그림입니다
OCR은 비트맵을 정리하거나 선명하게 하거나 교체하지 않습니다. OCR이 잘못 읽은 선명한 스캔본도 보기에는 여전히 선명합니다. 이미지가 품질 병목이었던 적은 없습니다. 인식이 병목이었습니다. 같은 스캔 묶음에서 인식 엔진만 바꿔도 정확도가 크게 달라질 수 있는데, 픽셀 자체는 전혀 바뀌지 않았습니다.
고전적인 PDF OCR은 이미지 아래에 눈에 보이지 않는 두 번째 텍스트 레이어를 정렬해 써 넣어, 선택과 검색이 올바른 단어에 닿게 합니다. 그림이 보여 주는 방식이고, 지금도 많은 데스크톱 도구가 이렇게 동작합니다.
이 엔드포인트가 실제로 반환하는 것
OCR 경로는 /api/v1/misc/ocr-pdf를 호출합니다. 이 엔드포인트는 독립된 Rust crate인 pdf-inspector(ocr 기능을 켜서 빌드) 위에서 동작하며, PDF 전체를 인식 모델에 통째로 넘기지 않습니다. pdf-inspector는 먼저 각 페이지를 이미 추출 가능한 네이티브 텍스트가 있는 페이지인지, 이미지만 있는 페이지인지로 분류합니다. 네이티브 텍스트가 있는 페이지는 그 텍스트를 그대로 쓰고 인식 모델을 전혀 거치지 않습니다. 이미지만 있는 페이지는 인식 파이프라인으로 넘어가는데, PDFium(Chrome이 내부적으로 쓰는 것과 같은 오픈소스 렌더러)이 페이지를 래스터화하고, ONNX Runtime이 PP-OCRv6 Small 모델을 돌려 읽어냅니다. 두 종류의 페이지 결과는 순서대로 하나의 Markdown 문서로 이어붙여집니다. 응답이 text/markdown이고 다운로드 파일이 .md로 끝나는 이유입니다.
이 구조는 이번 재작성에서 새로 생긴 것입니다. 이 프로젝트가 예전에 쓰던 Java/Spring Boot 백엔드는 OCRmyPDF를 호출했습니다. "이미지에 숨은 텍스트 레이어를 얹는" 전통적인 방식으로, 출력도 PDF였습니다. Rust로 재작성하면서 이 경로 전체를 pdf-inspector의 선택적 OCR로 바꿨고, 그에 따라 출력도 PDF에서 Markdown으로 바뀌었습니다. 예전 Java 백엔드는 저장소에서 완전히 제거되어, 다시 되돌릴 수 있는 토글이 아닙니다.
검색 가능한 PDF(이미지와 텍스트 레이어)가 필요하다면 이 엔드포인트는 그것을 줄 수 없습니다. 에이전트나 데이터 파이프라인이 바로 소비할 수 있는 텍스트를 줄 뿐입니다.
Auto와 Force OCR의 차이
폼에는 ocrType 필드가 있습니다.
- Normal(
force-ocr가 아닌 모든 값)은 Auto로 매핑됩니다. 파일에 이미 있는 텍스트는 그대로 쓰고, 이미지만 있는 페이지에만 OCR을 돌립니다. 깨끗한 전자 문서 PDF에서는 Auto가 OCR 런타임을 로드하지 않습니다. - Force OCR(
ocrType=force-ocr)은 이미 텍스트 레이어가 있는 페이지까지 모든 페이지의 인식을 다시 돌립니다. 순수 이미지 페이지에서는 시간을 더 치를 뿐 정확도가 오르지 않습니다. 대신 망가졌거나 일부만 남은 기존 레이어를 무시하는 한 번의 인식을 얻습니다.
포털 기본값은 Force OCR입니다. 언어 옵션은 없습니다. 예전 Java 경로에서 남은 언어 파라미터는 서버가 무시합니다.
Auto와 Force의 구분은 요청 안에서만 일어나며, 포털도 API의 기능 탐지(capability probe)도 미리 이를 확인해 주지 않습니다. GET /api/v1/settings/get-endpoints-status는 ocr-pdf를 항상 "enabled"로 보고할 뿐, PDFium과 ONNX Runtime, PP-OCRv6 모델이 실제로 설치돼 있는지는 확인하지 않습니다. 진짜 확인은 요청이 인식을 실행시키는 그 순간에야 일어납니다. 그중 하나라도 빠져 있으면 엔드포인트는 네이티브 텍스트만으로 조용히 물러나는 반쪽짜리 Markdown이 아니라 503을 반환합니다. PP-OCRv6 Small 모델 자체는 약 31 MB로, 배포 시 로컬 캐시에 미리 심어 두지 않았다면 실제로 인식이 필요한 첫 페이지에서 네트워크로 내려받습니다. 오프라인이면서 모델을 미리 심어 두지 않은 머신은 느리게 도는 게 아니라 첫 Force 요청에서 곧바로 실패할 가능성이 큽니다.
인식은 확률적입니다
인식 엔진은 픽셀 패턴을 글자와 단어 모델에 맞춰 봅니다. 깨끗하고 대비가 높은 표준 폰트 인쇄물에서는 잘 맞히고, 다음 경우에는 나빠집니다.
- 저해상도이거나 대비가 낮은 스캔(300 DPI 원본에 비해 팩스 품질인 경우)
- 손글씨, 장식용 폰트, 특이한 레이아웃(다단 표, 회전된 텍스트, 빽빽한 서식)
- 기울어진 페이지가 글자 모양을 살짝 왜곡해 매칭을 방해하는 경우
Auto든 Force든 흐릿한 팩스는 깨끗한 스캔처럼 인식되지 않습니다. 이는 모델 자체의 한계이지 파라미터로 고칠 수 있는 문제가 아닙니다.
OCR이 하지 않는 일
OCR은 텍스트를 줍니다. 문단, 제목, 표를 재구성하거나 다시 흐를 수 있는 Word 문서를 만들지는 않습니다. 편집 가능한 레이아웃은 인식 오류 위에 얹히는 변환 문제이며, 스캔본을 읽는 일과 같은 작업이 아닙니다.
PDF OCR은 계정 없이 서버에서 실행됩니다. 다운로드는 Markdown입니다. 기존 텍스트 레이어가 잘못돼 보이면 Force OCR을 써서 Auto가 그 레이어를 신뢰하지 않게 하세요. 어떤 머신에 PDFium과 모델이 실제로 설치돼 있는지 알고 싶다면, 기능 탐지 상태를 보는 것보다 실제 요청을 한 번 보내는 편이 더 확실합니다. 탐지는 엔드포인트가 존재한다는 것만 확인할 뿐, 런타임이 준비됐는지는 확인해 주지 않기 때문입니다. 자동화 키와 OpenAPI는 개발자를 보세요.