Skip to content

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()ScannedPageHybridPage 모두에 대해 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())?;
}

동작 방식

  1. PDF Oxide가 페이지를 내부적으로 이미지(300 DPI)로 렌더링합니다
  2. 검출 전략(v3/v4는 MaxSide, v5는 MinSide)에 따라 이미지를 리사이즈합니다
  3. DBNet++ 텍스트 검출기가 텍스트 영역을 사각형 바운딩 박스로 찾아냅니다
  4. SVTR 텍스트 인식기가 검출된 각 영역의 문자를 읽어냅니다
  5. 읽기 순서로 정렬해 결과를 텍스트로 조립합니다
  6. 하이브리드 페이지에서는 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())?;

관련 페이지