PDF OCR — Python / Node.js / Go / C# / Rust (Tesseract 불필요)
내장 OCR로 스캔된 PDF에서 텍스트를 추출합니다. v0.3.27부터 Python, Node.js, Go, C#, Rust의 모든 바인딩에서 통합 FFI 계층(pdf_ocr_engine_create, pdf_ocr_page_needs_ocr, pdf_ocr_extract_text)을 통해 OCR을 노출합니다.
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("scanned.pdf")
text = doc.extract_text_ocr(0)
print(text)
Node.js
const { PdfDocument, OcrEngine } = require("pdf-oxide");
const doc = new PdfDocument("scanned.pdf");
const ocr = new OcrEngine();
if (ocr.pageNeedsOcr(doc, 0)) {
console.log(ocr.extractText(doc, 0));
}
ocr.close();
doc.close();
Go
import pdfoxide "github.com/yfedoseev/pdf_oxide/go"
doc, _ := pdfoxide.Open("scanned.pdf")
defer doc.Close()
ocr, _ := pdfoxide.NewOcrEngine()
defer ocr.Close()
if ocr.NeedsOcr(doc, 0) {
text, _ := ocr.ExtractTextWithOcr(doc, 0)
fmt.Println(text)
}
C#
using PdfOxide.Core;
using PdfOxide.Ocr;
using var doc = PdfDocument.Open("scanned.pdf");
using var ocr = new OcrEngine();
if (ocr.PageNeedsOcr(doc, 0))
{
Console.WriteLine(ocr.ExtractText(doc, 0));
}
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::ocr::{OcrEngine, OcrConfig, OcrExtractOptions, extract_text_with_ocr};
let mut doc = PdfDocument::open("scanned.pdf")?;
let config = OcrConfig::default();
let engine = OcrEngine::new("models/det.onnx", "models/rec.onnx", "models/dict.txt", config)?;
let options = OcrExtractOptions::default();
let text = extract_text_with_ocr(&mut doc, 0, Some(&engine), options)?;
println!("{text}");
PDF Oxide는 PaddleOCR을 ONNX Runtime으로 내장해 제공합니다. Tesseract 설치도, 시스템 의존성도, 서브프로세스 호출도 필요 없이 OCR 엔진이 프로세스 내에서 바로 동작합니다. PP-OCRv3, PP-OCRv4, PP-OCRv5 모델 패밀리를 지원합니다.
참고: OCR은 WebAssembly에서 사용할 수 없습니다(네이티브 ONNX Runtime이 필요). Go, Node.js, C#, Rust에서는
ocr기능을 켜고 빌드하세요. Python 휠은 기본적으로 OCR이 활성화된 상태로 배포됩니다.
Tesseract 없는 Python PDF OCR
대부분의 Python PDF OCR 솔루션은 Tesseract를 시스템 의존성으로 설치해야 하는데, 이는 OS와 CI 환경마다 달라 설정이 번거롭습니다. PDF Oxide는 PaddleOCR 모델을 Python 휠 안에 그대로 포함합니다.
- 시스템 의존성 없음 —
pip install pdf_oxide만으로 충분합니다 - 서브프로세스 호출 없음 — OCR이 ONNX Runtime으로 네이티브하게 동작합니다
- 세 가지 모델 패밀리 — PP-OCRv3, PP-OCRv4, PP-OCRv5
- 자동 페이지 판별 — 스캔 페이지인지 텍스트 기반 페이지인지 자동으로 구분합니다
비교: PDF Oxide OCR vs PyMuPDF + Tesseract
| PDF Oxide | PyMuPDF + Tesseract | |
|---|---|---|
| 설치 | pip install pdf_oxide |
pip install pymupdf + 시스템 Tesseract |
| OCR 엔진 | PaddleOCR (ONNX) | Tesseract (서브프로세스) |
| 설치 복잡도 | 한 줄 | OS별 Tesseract 설치 |
| CI/Docker | 추가 설정 없음 | apt-get install tesseract-ocr 필요 |
| 모델 포함 | 예(휠에 포함) | 아니오(별도 다운로드) |
설치
Python
pip install pdf_oxide
OCR 모델은 휠에 포함되어 있으며, 추가 다운로드가 필요 없습니다.
Rust
[dependencies]
pdf_oxide = { version = "0.3", features = ["ocr"] }
Go
go build -tags ocr ./...
Node.js
npm install pdf-oxide --build-from-source -- --features ocr
C#
NuGet 패키지는 Linux/macOS/Windows 기본 바이너리에서 OCR이 이미 활성화되어 있어 별도 설정이 필요 없습니다.
OCR이 필요한 경우
대부분의 PDF는 임베디드 텍스트를 갖고 있어 extract_text()로 페이지당 0.8ms에 처리됩니다. OCR은 다음의 경우에만 필요합니다.
- 스캔 문서 — 종이 문서를 PDF로 스캔한 경우
- 이미지 전용 PDF — 사진이나 스크린샷으로 만들어진 PDF
- 텍스트가 이미지로 들어간 PDF — 일부 생성기는 텍스트를 래스터화합니다
- 하이브리드 페이지 — 네이티브 텍스트와 스캔 이미지 영역이 함께 있는 페이지
PP-OCR 모델 버전
PDF Oxide는 PaddleOCR 3세대 모델을 지원합니다. 기본 설정은 PP-OCRv3와 PP-OCRv4에 맞추어져 있습니다. PP-OCRv5 서버 모델은 리사이즈 전략을 달리해야 합니다.
PP-OCRv3 / PP-OCRv4 (기본값)
모바일 최적화 모델로, 이미지를 최대 변 길이에 맞게 축소합니다. 일반적인 문서에 적합합니다.
- 검출 모델: DBNet++ (경량)
- 인식 모델: SVTR
- 리사이즈 전략:
MaxSide— 긴 변을 960px로 축소 - 적합 용도: 표준 문서, 모바일/엣지 배포
Python
from pdf_oxide import OcrConfig, OcrEngine
# 기본 설정은 v3/v4 모델에 맞춰져 있습니다
config = OcrConfig()
engine = OcrEngine("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)
Rust
use pdf_oxide::ocr::{OcrConfig, OcrEngine};
// 기본 설정: MaxSide { max_side: 960 }
let config = OcrConfig::default();
let engine = OcrEngine::new("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)?;
PP-OCRv5 (서버)
서버급 모델로 필요 시 이미지를 확대해 고해상도를 유지합니다. 조밀하거나 작은 글씨의 문서에서 정확도가 크게 향상됩니다.
- 검출 모델: DBNet++ (서버, 크기 큼)
- 인식 모델: SVTR-v5
- 리사이즈 전략:
MinSide— 짧은 변을 최소 64px로 보장하고 4000px를 상한으로 제한 - 적합 용도: 고정확도 추출, 서버 환경, 조밀한 텍스트
Python
from pdf_oxide import OcrConfig, OcrEngine
# v5 설정: 서버 모델용 고해상도 입력
config = OcrConfig(use_v5=True)
engine = OcrEngine("det_v5.onnx", "rec_v5.onnx", "dict_v5.txt", config)
Rust
use pdf_oxide::ocr::{OcrConfig, OcrEngine};
// v5 설정: MinSide { min_side: 64, max_side_limit: 4000 }
let config = OcrConfig::v5();
let engine = OcrEngine::new("det_v5.onnx", "rec_v5.onnx", "dict_v5.txt", config)?;
모델 비교
| 항목 | PP-OCRv3/v4 | PP-OCRv5 |
|---|---|---|
| 리사이즈 전략 | MaxSide (960px로 축소) |
MinSide (확대, 4000px 상한) |
| 입력 해상도 | 낮음(빠름) | 높음(정확) |
| 검출 모델 크기 | 약 3 MB | 약 12 MB |
| 인식 모델 크기 | 약 12 MB | 약 25 MB |
| 적합 용도 | 모바일, 엣지, 표준 문서 | 서버, 조밀한 텍스트, 작은 글씨 |
OcrConfig |
OcrConfig() / OcrConfig::default() |
OcrConfig(use_v5=True) / OcrConfig::v5() |
페이지 유형 감지
PDF Oxide는 OCR이 필요한지를 판단하기 위해 페이지를 자동 분류합니다. extract_text_ocr()가 내부에서 이를 처리하지만, 페이지 유형을 수동으로 감지할 수도 있습니다.
스캔 페이지 자동 감지
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("mixed.pdf")
for i in range(doc.page_count()):
text = doc.extract_text(i)
if len(text.strip()) < 50:
# 스캔 페이지로 추정 — OCR 사용
text = doc.extract_text_ocr(i)
print(f"Page {i + 1} (OCR): {text[:100]}...")
else:
print(f"Page {i + 1} (text): {text[:100]}...")
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::ocr::{detect_page_type, PageType, OcrEngine, OcrConfig, OcrExtractOptions, extract_text_with_ocr};
let mut doc = PdfDocument::open("mixed.pdf")?;
let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", OcrConfig::default())?;
for i in 0..doc.page_count() {
let page_type = detect_page_type(&mut doc, i)?;
match page_type {
PageType::NativeText => {
let text = doc.extract_text(i)?;
println!("Page {} (native): {}...", i + 1, &text[..100.min(text.len())]);
}
PageType::ScannedPage => {
let text = extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?;
println!("Page {} (OCR): {}...", i + 1, &text[..100.min(text.len())]);
}
PageType::HybridPage => {
// 네이티브 텍스트와 스캔 이미지가 공존 — 두 소스를 병합
let text = extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?;
println!("Page {} (hybrid): {}...", i + 1, &text[..100.min(text.len())]);
}
}
}
PageType 배리언트 (Rust)
| 배리언트 | 설명 |
|---|---|
NativeText |
페이지에 임베디드 텍스트가 있음 — OCR 불필요 |
ScannedPage |
페이지 전체가 스캔(큰 이미지, 텍스트 없음 또는 최소) — 전체 OCR |
HybridPage |
네이티브 텍스트와 큰 스캔 이미지가 함께 있음 — 네이티브 텍스트와 OCR 결과를 병합 |
헬퍼 needs_ocr()는 ScannedPage와 HybridPage 모두에 대해 true를 반환합니다.
use pdf_oxide::ocr::needs_ocr;
if needs_ocr(&mut doc, 0)? {
let text = extract_text_with_ocr(&mut doc, 0, Some(&engine), OcrExtractOptions::default())?;
}
동작 방식
- PDF Oxide가 페이지를 내부적으로 이미지(300 DPI)로 렌더링합니다
- 검출 전략(v3/v4는
MaxSide, v5는MinSide)에 따라 이미지를 리사이즈합니다 - DBNet++ 텍스트 검출기가 텍스트 영역을 사각형 바운딩 박스로 찾아냅니다
- SVTR 텍스트 인식기가 검출된 각 영역의 문자를 읽어냅니다
- 읽기 순서로 정렬해 결과를 텍스트로 조립합니다
- 하이브리드 페이지에서는 OCR 텍스트를 네이티브 텍스트와 병합합니다
전체 파이프라인이 ONNX Runtime으로 프로세스 내에서 실행됩니다. 외부 바이너리도, 서브프로세스 호출도, 임시 파일도 없습니다.
OCR 설정
Python
from pdf_oxide import OcrConfig, OcrEngine
# 기본값 (v3/v4)
config = OcrConfig()
# PP-OCRv5 서버 모델
config = OcrConfig(use_v5=True)
# 사용자 지정 임계값
config = OcrConfig(
det_threshold=0.5, # 검출 신뢰도 (0.0-1.0)
box_threshold=0.7, # 박스 신뢰도 (0.0-1.0)
rec_threshold=0.6, # 인식 신뢰도 (0.0-1.0)
num_threads=8, # ONNX Runtime 스레드 수
max_candidates=500, # 최대 텍스트 영역
)
# v5에 사용자 지정 임계값 적용
config = OcrConfig(use_v5=True, det_threshold=0.4, num_threads=8)
engine = OcrEngine("det.onnx", "rec.onnx", "dict.txt", config)
Rust
use pdf_oxide::ocr::{OcrConfig, OcrConfigBuilder, DetResizeStrategy};
// 기본값 (v3/v4): MaxSide { max_side: 960 }
let config = OcrConfig::default();
// PP-OCRv5: MinSide { min_side: 64, max_side_limit: 4000 }
let config = OcrConfig::v5();
// 사용자 빌더
let config = OcrConfig::builder()
.det_threshold(0.5)
.box_threshold(0.7)
.rec_threshold(0.6)
.num_threads(8)
.max_candidates(500)
.detect_styles(true) // OCR 지오메트리 기반 스타일 감지 활성화
.build();
// 사용자 정의 리사이즈 전략
let config = OcrConfig::builder()
.det_resize_strategy(DetResizeStrategy::MinSide {
min_side: 128,
max_side_limit: 6000,
})
.build();
DetResizeStrategy (Rust)
검출 모델을 실행하기 전에 입력 이미지를 어떻게 리사이즈할지 제어합니다.
| 배리언트 | 필드 | 설명 |
|---|---|---|
MaxSide |
max_side: u32 (기본값: 960) |
긴 변이 max_side에 맞도록 축소. PP-OCRv3/v4 기본값. |
MinSide |
min_side: u32 (기본값: 64), max_side_limit: u32 (기본값: 4000) |
짧은 변이 min_side 이상이 되도록 확대하고 max_side_limit으로 상한. PP-OCRv5 기본값. |
OcrConfig 필드
| 필드 | 타입 | 기본값 | 설명 |
|---|---|---|---|
det_threshold |
f32 |
0.3 |
검출 확률 임계값 |
box_threshold |
f32 |
0.6 |
박스 신뢰도 임계값 |
rec_threshold |
f32 |
0.5 |
인식 신뢰도 임계값 |
det_max_side |
u32 |
960 |
최대 이미지 변 길이 (v3/v4 호환) |
det_resize_strategy |
DetResizeStrategy |
MaxSide { 960 } |
이미지 리사이즈 전략 |
rec_target_height |
u32 |
48 |
인식용 크롭 목표 높이 |
num_threads |
usize |
4 |
ONNX Runtime 추론 스레드 수 |
unclip_ratio |
f32 |
1.5 |
박스 확장 비율 |
max_candidates |
usize |
1000 |
감지할 최대 텍스트 영역 수 |
detect_styles |
bool |
true |
OCR 지오메트리에서 폰트 스타일 감지 |
det_model_path |
Option<PathBuf> |
None |
사용자 검출 모델 경로 |
rec_model_path |
Option<PathBuf> |
None |
사용자 인식 모델 경로 |
dict_path |
Option<PathBuf> |
None |
사용자 문자 사전 경로 |
사용자 모델
내장 모델 대신 직접 준비한 ONNX 모델을 사용합니다.
Rust
use pdf_oxide::ocr::OcrConfig;
let config = OcrConfig::builder()
.det_model_path("models/custom_det.onnx")
.rec_model_path("models/custom_rec.onnx")
.dict_path("models/custom_dict.txt")
.build();
스타일 감지
detect_styles가 활성화되어 있으면(기본값) PDF Oxide가 OCR 지오메트리(문자 크기, 간격, 위치)에서 폰트 스타일(굵게, 제목 수준)을 추론합니다. 이는 스캔 페이지를 Markdown으로 변환할 때 품질을 개선합니다.
let config = OcrConfig::builder()
.detect_styles(true) // 텍스트 지오메트리로부터 스타일 추론
.build();
OCR vs Tesseract
| 항목 | PDF Oxide OCR | Tesseract (PyMuPDF 경유) |
|---|---|---|
| 설치 | pip install pdf_oxide |
시스템 패키지 + pytesseract |
| 시스템 의존성 | 없음 | Tesseract 바이너리 필요 |
| 런타임 | ONNX (프로세스 내) | 서브프로세스 호출 |
| 모델 버전 | PP-OCRv3, v4, v5 | Tesseract LSTM |
| 언어 | 다국어 지원 | 언어 팩 필요 |
| 설치 복잡도 | 없음 | 중간 |
| 검출 모델 | DBNet++ | Tesseract 내장 |
| 인식 모델 | SVTR / SVTR-v5 | Tesseract LSTM |
| 고해상도 지원 | MinSide 전략 (v5) |
DPI 설정 |
| 페이지 유형 감지 | 자동(네이티브/스캔/하이브리드) | 수동 |
사용자 DPI
OCR용으로 PDF 페이지를 이미지로 변환할 때의 렌더링 해상도를 제어합니다.
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("scanned.pdf")
# 기본값은 300 DPI — 정확도와 속도의 균형이 좋습니다
text = doc.extract_text_ocr(0)
# 작은 글씨에서 정확도를 높이려면 DPI를 더 올립니다
text = doc.extract_text_ocr(0) # Rust에서는 OcrExtractOptions로 DPI를 설정합니다
Rust
use pdf_oxide::ocr::OcrExtractOptions;
// DPI가 높을수록 정확도가 올라가지만 느려집니다
let options = OcrExtractOptions::default().with_dpi(300.0);
// DPI가 낮을수록 빠르지만 정확도가 떨어집니다
let options = OcrExtractOptions::default().with_dpi(150.0);
OCR 출력 구조 (Rust)
OcrEngine::ocr_image() 메서드는 스팬별 신뢰도 점수가 포함된 상세 결과를 반환합니다.
use pdf_oxide::ocr::OcrEngine;
let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", Default::default())?;
let output = engine.ocr_image(&image)?;
// 읽기 순서대로 정렬된 전체 텍스트
println!("{}", output.text_in_reading_order());
// 스팬별 상세 정보
for span in &output.spans {
println!("Text: '{}' (confidence: {:.2})", span.text, span.confidence);
println!(" 바운딩 박스: {:?}", span.bounding_rect());
println!(" 문자별 신뢰도: {:?}", span.char_confidences);
}
// 전체 신뢰도
println!("Total confidence: {:.2}", output.total_confidence);
OcrOutput 필드
| 필드 / 메서드 | 타입 | 설명 |
|---|---|---|
spans |
Vec<OcrSpan> |
인식된 모든 텍스트 영역 |
total_confidence |
f32 |
모든 스팬의 평균 신뢰도 |
text() |
String |
공백으로 연결한 전체 텍스트 |
text_in_reading_order() |
String |
위치 기준(위→아래, 좌→우)으로 정렬한 텍스트 |
OcrSpan 필드
| 필드 | 타입 | 설명 |
|---|---|---|
text |
String |
인식된 텍스트 |
polygon |
[[f32; 2]; 4] |
사각형 바운딩 박스 (4개 모서리) |
confidence |
f32 |
전체 신뢰도 (0.0–1.0) |
char_confidences |
Vec<f32> |
문자별 신뢰도 점수 |
일괄 OCR 처리
스캔된 PDF 디렉터리를 처리합니다.
Python
from pdf_oxide import PdfDocument, PdfError
from pathlib import Path
pdf_dir = Path("scans/")
output_dir = Path("text-output/")
output_dir.mkdir(exist_ok=True)
for pdf_path in pdf_dir.glob("*.pdf"):
try:
doc = PdfDocument(str(pdf_path))
pages = []
for i in range(doc.page_count()):
text = doc.extract_text(i)
if len(text.strip()) < 50:
text = doc.extract_text_ocr(i)
pages.append(text)
out_path = output_dir / pdf_path.with_suffix(".txt").name
out_path.write_text("\n\n".join(pages), encoding="utf-8")
except PdfError as e:
print(f"Error: {pdf_path.name}: {e}")
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::ocr::{OcrEngine, OcrConfig, OcrExtractOptions, extract_text_with_ocr, needs_ocr};
use std::fs;
use std::path::Path;
let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", OcrConfig::default())?;
let options = OcrExtractOptions::default();
for entry in fs::read_dir("scans/")? {
let path = entry?.path();
if path.extension().map_or(false, |e| e == "pdf") {
let mut doc = PdfDocument::open(path.to_str().unwrap())?;
let mut all_text = String::new();
for i in 0..doc.page_count() {
let text = if needs_ocr(&mut doc, i)? {
extract_text_with_ocr(&mut doc, i, Some(&engine), options.clone())?
} else {
doc.extract_text(i)?
};
all_text.push_str(&text);
all_text.push_str("\n\n");
}
let out_path = Path::new("text-output/")
.join(path.file_stem().unwrap())
.with_extension("txt");
fs::write(out_path, &all_text)?;
}
}
병렬 OCR (Python)
from pdf_oxide import PdfDocument
from multiprocessing import Pool
from pathlib import Path
def ocr_pdf(pdf_path: str) -> dict:
doc = PdfDocument(pdf_path)
text = ""
for i in range(doc.page_count()):
text += doc.extract_text_ocr(i) + "\n"
return {"file": pdf_path, "text": text}
pdf_files = [str(p) for p in Path("scans/").glob("*.pdf")]
with Pool(4) as pool:
results = pool.map(ocr_pdf, pdf_files)
OCR에서 Markdown으로
스캔 페이지를 Markdown으로 변환합니다.
Python
from pdf_oxide import PdfDocument
doc = PdfDocument("scanned-report.pdf")
for i in range(doc.page_count()):
md = doc.to_markdown(i, detect_headings=True)
if len(md.strip()) < 50:
# 스캔 페이지 — OCR 후 포맷
text = doc.extract_text_ocr(i)
md = text # OCR 출력은 일반 텍스트입니다
print(f"--- Page {i + 1} ---")
print(md)
Rust
use pdf_oxide::PdfDocument;
use pdf_oxide::ocr::{OcrEngine, OcrConfig, OcrExtractOptions, needs_ocr, extract_text_with_ocr};
let mut doc = PdfDocument::open("scanned-report.pdf")?;
let engine = OcrEngine::new("det.onnx", "rec.onnx", "dict.txt", OcrConfig::default())?;
for i in 0..doc.page_count() {
let text = if needs_ocr(&mut doc, i)? {
extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?
} else {
doc.to_markdown(i, &Default::default())?
};
println!("--- Page {} ---\n{}", i + 1, text);
}
성능 관련 고려 사항
OCR은 텍스트 추출보다 훨씬 느립니다.
| 작업 | 일반적인 속도 |
|---|---|
| 텍스트 추출 | 페이지당 0.8ms |
| OCR (v3/v4) | 페이지당 200–1,000ms |
| OCR (v5 서버) | 페이지당 500–2,000ms |
OCR 속도는 페이지 복잡도, 이미지 해상도, 텍스트 밀도, 모델 버전에 따라 달라집니다. PP-OCRv5는 더 느리지만 더 정확합니다. 대규모 배치에서는 병렬 처리를 고려하세요(위의 일괄 OCR 처리 참조).
바이트에서 모델 로드 (Rust)
use pdf_oxide::ocr::{OcrEngine, OcrConfig};
let det_bytes = std::fs::read("models/det.onnx")?;
let rec_bytes = std::fs::read("models/rec.onnx")?;
let dict = std::fs::read_to_string("models/dict.txt")?;
let engine = OcrEngine::from_bytes(&det_bytes, &rec_bytes, &dict, OcrConfig::default())?;
관련 페이지
- 텍스트 추출 — 표준 텍스트 추출
- Markdown 변환 — 제목 감지를 포함한 Markdown 변환
- 페이지 렌더링 — 페이지를 이미지로 렌더링(OCR이 내부에서 사용)
- 일괄 처리 — 병렬 처리 패턴
- PDF에서 텍스트 추출 — 텍스트 추출 가이드