오프라인 OCR 모델 관리
PDF Oxide의 OCR은 로컬 캐시 디렉터리에 저장된 ONNX 감지 모델과 인식 모델을 사용합니다. Docker 빌드, CI, 에어갭/오프라인 배포 환경에서는 첫 번째 OCR 호출 전에 모델이 준비되어 있어야 합니다. 요청 시점에 다운로드하는 방식은 피해야 합니다. PDF Oxide는 이를 위한 세 가지 기본 기능을 제공합니다.
prefetch_models— 공유 감지기와 언어별 인식 모델 및 사전을 모델 캐시 디렉터리에 다운로드합니다(빌드 시 프로비저닝).model_manifest— 네트워크 없이 모든 모델 파일과 소스 URL을 나열한 JSON 매니페스트로, 에어갭 호스트에서 미러링 및 검증에 사용합니다.prefetch_available— 현재 빌드가 실제로 다운로드 가능한지(ocr피처를 포함해 컴파일되었는지) 여부를 반환합니다.
캐시 디렉터리는 $PDF_OXIDE_MODEL_DIR이 설정된 경우 해당 경로를, 그렇지 않으면 플랫폼 캐시(Linux에서는 ~/.cache/pdf_oxide/models)를 사용합니다. 파일이 준비되면 OCR이 완전 오프라인으로 동작합니다.
바인딩 지원 범위. 모델 프로비저닝은 Rust, Go, C#, Swift에서 제공됩니다.
model_manifest와prefetch_available은 WASM/JavaScript에서도 제공됩니다(WASM에서prefetchAvailable()은 항상false를 반환합니다. WASM에는 네트워크 페처가 없으므로 매니페스트를 사용해 호스트 측에서 파일을 준비해야 합니다). Python 및 Node N-API 바인딩은 v0.3.69에서 이 기능을 제공하지 않습니다.
오프라인용 OCR 모델을 프리페치하는 방법
prefetch_models는 쉼표로 구분된 언어 코드를 받아(빈 값이면 영어 기본값), 공유 감지기와 각 언어의 인식 모델 및 사전을 캐시 디렉터리에 다운로드하고 해당 디렉터리 경로를 반환합니다. 멱등성이 있어서 이미 존재하는 파일은 건너뜁니다.
Rust
use pdf_oxide::extractors::auto::{AutoExtractor, OcrLanguage};
fn main() -> pdf_oxide::Result<()> {
// AutoExtractor::prefetch_models(langs: &[OcrLanguage])
// -> Result<std::path::PathBuf>
let dir = AutoExtractor::prefetch_models(&[
OcrLanguage::English,
OcrLanguage::Chinese,
OcrLanguage::Arabic,
])?;
println!("models cached in {}", dir.display());
// One-shot English (the common case):
let _ = AutoExtractor::prefetch_models_default()?;
Ok(())
}
Go
package main
import (
"fmt"
"log"
pdfoxide "github.com/yfedoseev/pdf_oxide/go"
)
func main() {
if !pdfoxide.PrefetchAvailable() {
log.Fatal("this build cannot download models (built without the ocr feature)")
}
// func PrefetchModels(langs ...string) (string, error)
dir, err := pdfoxide.PrefetchModels("english", "chinese", "arabic")
if err != nil {
log.Fatal(err)
}
fmt.Println("models cached in", dir)
}
C#
using System;
using PdfOxide.Core;
if (!PdfDocument.PrefetchAvailable())
throw new InvalidOperationException("built without the ocr feature; cannot download models");
// static string PdfDocument.PrefetchModels(params string[] languages)
string dir = PdfDocument.PrefetchModels("english", "chinese", "arabic");
Console.WriteLine($"models cached in {dir}");
Swift
import PdfOxide
guard PdfOxide.prefetchAvailable() == 1 else {
fatalError("built without the ocr feature; cannot download models")
}
// static func prefetchModels(languagesCsv: String) throws -> String
let dir = try PdfOxide.prefetchModels(languagesCsv: "english,chinese,arabic")
print("models cached in \(dir)")
PHP
use PdfOxide\Pdf;
if (!Pdf::prefetchAvailable()) {
throw new RuntimeException('built without the ocr feature; cannot download models');
}
// static Pdf::prefetchModels(array $languages): string
$dir = Pdf::prefetchModels(['english', 'chinese', 'arabic']);
echo "models cached in {$dir}\n";
Ruby
require 'pdf_oxide'
unless PdfOxide::Pdf.prefetch_available?
raise 'built without the ocr feature; cannot download models'
end
# PdfOxide::Pdf.prefetch_models(languages) -> String (cache dir)
dir = PdfOxide::Pdf.prefetch_models(%w[english chinese arabic])
puts "models cached in #{dir}"
C++
#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>
if (pdf_oxide::prefetch_available() == 0)
throw std::runtime_error("built without the ocr feature; cannot download models");
// std::string pdf_oxide::prefetch_models(const std::string& languages_csv)
auto dir = pdf_oxide::prefetch_models("english,chinese,arabic");
std::cout << "models cached in " << dir << "\n";
Dart
import 'package:pdf_oxide/pdf_oxide.dart' as pdf_oxide;
if (pdf_oxide.prefetchAvailable() == 0) {
throw StateError('built without the ocr feature; cannot download models');
}
// String prefetchModels(String languagesCsv)
final dir = pdf_oxide.prefetchModels('english,chinese,arabic');
print('models cached in $dir');
R
library(pdfoxide)
if (pdf_prefetch_available() == 0)
stop("built without the ocr feature; cannot download models")
# pdf_prefetch_models(languages_csv = NULL) -> cache directory path
dir <- pdf_prefetch_models("english,chinese,arabic")
cat("models cached in", dir, "\n")
Julia
using PdfOxide
prefetch_available() != 0 || error("built without the ocr feature; cannot download models")
# prefetch_models(languages_csv::AbstractString) -> cache directory path
dir = prefetch_models("english,chinese,arabic")
println("models cached in ", dir)
Zig
const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;
if (pdf_oxide.prefetchAvailable() == 0) return error.OcrFeatureMissing;
// prefetchModels(alloc, languages_csv) -> []u8 (cache dir; caller frees)
const dir = try pdf_oxide.prefetchModels(a, "english,chinese,arabic");
defer a.free(dir);
std.debug.print("models cached in {s}\n", .{dir});
Objective-C
#import "POXPdfOxide.h"
NSError *err = nil;
if ([POXModels prefetchAvailable] <= 0) {
@throw [NSException exceptionWithName:@"PdfOxide" reason:@"no ocr feature" userInfo:nil];
}
// + prefetchModels:error: returns a status JSON (nil on error)
NSString *status = [POXModels prefetchModels:@"english,chinese,arabic" error:&err];
NSLog(@"prefetch status: %@", status);
Elixir
unless PdfOxide.prefetch_available() != 0 do
raise "built without the ocr feature; cannot download models"
end
# prefetch_models(languages_csv \\ "") -> JSON status string
status = PdfOxide.prefetch_models("english,chinese,arabic")
IO.puts("prefetch status: #{status}")
언어 코드
prefetch_models에서 사용할 수 있는 코드 목록입니다(알 수 없는 코드는 건너뜁니다; 빈 입력은 영어로 기본 설정됩니다).
english, chinese, chinese_cht, japan, korean, arabic, cyrillic, latin, devanagari, ta(타밀어), te(텔루구어), ka(칸나다어)
에어갭 호스트에서 모델을 프로비저닝하는 방법
인터넷 연결이 없는 머신(또는 페처가 없는 WASM 대상)에서는 prefetch_models를 호출할 수 없습니다. 대신 model_manifest를 사용하세요. 이는 모든 모델 파일과 업스트림 URL을 네트워크 없이 정적으로 나열한 JSON입니다. 아티팩트 저장소를 통해 URL을 미러링한 후 $PDF_OXIDE_MODEL_DIR에 파일을 배치하세요.
Rust
use pdf_oxide::extractors::auto::AutoExtractor;
fn main() {
// AutoExtractor::model_manifest() -> String (JSON, never errors)
let manifest = AutoExtractor::model_manifest();
println!("{manifest}");
}
Go
// func ModelManifest() string (JSON, never errors)
fmt.Println(pdfoxide.ModelManifest())
C#
// static string PdfDocument.ModelManifest()
Console.WriteLine(PdfDocument.ModelManifest());
Swift
// static func modelManifest() -> String (JSON)
print(PdfOxide.modelManifest())
JavaScript (WASM)
import init, { modelManifest, prefetchAvailable } from "pdf-oxide-wasm";
await init();
// prefetchAvailable() is always false in WASM — provision host-side.
console.log("can download here?", prefetchAvailable()); // false
console.log(modelManifest()); // JSON manifest
PHP
use PdfOxide\FFI\FunctionBindings;
// (new FunctionBindings())->pdfOxideModelManifest(): string (JSON, never errors)
$manifest = (new FunctionBindings())->pdfOxideModelManifest();
echo $manifest, "\n";
C++
#include <pdf_oxide/pdf_oxide.hpp>
#include <iostream>
// std::string pdf_oxide::model_manifest() (JSON, never errors)
std::cout << pdf_oxide::model_manifest() << "\n";
Dart
import 'package:pdf_oxide/pdf_oxide.dart' as pdf_oxide;
// String modelManifest() (JSON)
print(pdf_oxide.modelManifest());
R
library(pdfoxide)
# pdf_model_manifest() -> JSON string
cat(pdf_model_manifest(), "\n")
Julia
using PdfOxide
# model_manifest() -> JSON String
println(model_manifest())
Zig
const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;
// modelManifest(alloc) -> []u8 (JSON; caller frees)
const manifest = try pdf_oxide.modelManifest(a);
defer a.free(manifest);
std.debug.print("{s}\n", .{manifest});
Objective-C
#import "POXPdfOxide.h"
NSError *err = nil;
// + manifestWithError: -> JSON string (nil on error)
NSString *manifest = [POXModels manifestWithError:&err];
NSLog(@"%@", manifest);
Elixir
# model_manifest() -> JSON string
IO.puts(PdfOxide.model_manifest())
매니페스트 구조
{
"detector": {
"file": "det.onnx",
"url": "https://.../det.onnx"
},
"languages": [
{
"language": "english",
"rec_file": "rec.onnx",
"dict_file": "en_dict.txt",
"rec_url": "https://.../rec.onnx",
"dict_url": "https://.../en_dict.txt"
}
],
"note": "Hebrew has no upstream PaddleOCR recognition model; the loader is ready if one is provided."
}
detector.url과 각 언어의 rec_url / dict_url을 미러링한 후, det.onnx와 각 rec_file / dict_file을 PDF_OXIDE_MODEL_DIR에 배치하세요. 그 후 OCR은 네트워크 접근 없이 동작합니다.
이 빌드에서 모델 다운로드가 가능한가요?
prefetch_available은 네이티브 라이브러리가 ocr 피처(HTTP 페처 포함)와 함께 컴파일되었는지 여부를 반환합니다. false를 반환하면 prefetch_models는 캐시 디렉터리만 생성하고 다운로드는 수행하지 않습니다. 다운로드에 의존하기 전에 반드시 확인하세요.
Rust
use pdf_oxide::extractors::auto::AutoExtractor;
// AutoExtractor::prefetch_available() -> bool
if AutoExtractor::prefetch_available() {
let _ = AutoExtractor::prefetch_models_default();
} else {
eprintln!("OCR feature not compiled in — provision via model_manifest()");
}
Go — pdfoxide.PrefetchAvailable() bool
C# — PdfDocument.PrefetchAvailable() -> bool
Swift — PdfOxide.prefetchAvailable() -> Int32 (1 == yes)
Dockerfile 예시
빌드 시점에 모델을 이미지에 내장하여 실행 중인 컨테이너가 네트워크에 접근하지 않도록 합니다.
FROM rust:1 AS models
WORKDIR /app
COPY . .
# Build the CLI / your binary with the `ocr` feature, then prefetch.
ENV PDF_OXIDE_MODEL_DIR=/models
RUN cargo run --features ocr --bin prefetch -- english chinese
FROM debian:stable-slim
ENV PDF_OXIDE_MODEL_DIR=/models
COPY --from=models /models /models
# OCR now runs fully offline against /models
전역 엔진 설정
추출 엔진을 조정하는 프로세스 전역 설정 항목이 두 개 있습니다. 두 항목 모두 C-ABI 바인딩 전체에 노출되며, 이전 값을 반환하고 오류 채널이 없습니다(실패하지 않습니다). 프로세스 전역이므로 한 스레드에서 설정하면 모든 병렬 추출 작업에 영향을 미칩니다.
콘텐츠 스트림 연산자 상한을 높이는 방법
PDF Oxide는 악의적인 입력의 처리 비용을 제한하기 위해 스트림당 콘텐츠 스트림 연산자 수에 상한을 설정합니다(기본값 1,000,000). 대형 합법적 기술 PDF(교재, ISO 표준 등)는 이 상한을 초과할 수 있습니다. set_max_ops_per_stream으로 상한을 올리거나 내릴 수 있으며, 이전 값을 반환합니다.
Rust
// pdf_oxide::content::parser::set_max_ops_per_stream(limit: Option<usize>)
// -> Option<usize> (None restores the 1,000,000 default)
use pdf_oxide::content::parser::set_max_ops_per_stream;
let prev = set_max_ops_per_stream(Some(5_000_000));
// ... extract a huge trusted PDF ...
set_max_ops_per_stream(prev); // restore
Go
// func SetMaxOpsPerStream(limit int64) int64 (returns previous cap)
prev := pdfoxide.SetMaxOpsPerStream(5_000_000)
defer pdfoxide.SetMaxOpsPerStream(prev)
C#
// static long CAbi.SetMaxOpsPerStream(long limit) (returns previous cap)
long prev = PdfOxide.Core.CAbi.SetMaxOpsPerStream(5_000_000);
try { /* extract huge trusted PDF */ }
finally { PdfOxide.Core.CAbi.SetMaxOpsPerStream(prev); }
Swift
// static func setMaxOpsPerStream(_ limit: Int64) -> Int64
let prev = PdfOxide.setMaxOpsPerStream(5_000_000)
defer { _ = PdfOxide.setMaxOpsPerStream(prev) }
PHP
use PdfOxide\FFI\FunctionBindings;
$bindings = new FunctionBindings();
// pdfOxideSetMaxOpsPerStream(int $limit): int (returns previous cap; -1 = default was active)
$prev = $bindings->pdfOxideSetMaxOpsPerStream(5_000_000);
try { /* extract huge trusted PDF */ }
finally { $bindings->pdfOxideSetMaxOpsPerStream($prev); }
Ruby
require 'pdf_oxide'
# PdfOxide.set_max_ops_per_stream(limit) -> previous cap (-1 = default was active)
prev = PdfOxide.set_max_ops_per_stream(5_000_000)
begin
# ... extract a huge trusted PDF ...
ensure
PdfOxide.set_max_ops_per_stream(prev)
end
C++
#include <pdf_oxide/pdf_oxide.hpp>
// std::int64_t pdf_oxide::set_max_ops_per_stream(std::int64_t limit) -> previous cap
auto prev = pdf_oxide::set_max_ops_per_stream(5'000'000);
// ... extract a huge trusted PDF ...
pdf_oxide::set_max_ops_per_stream(prev); // restore
Dart
import 'package:pdf_oxide/pdf_oxide.dart' as pdf_oxide;
// int setMaxOpsPerStream(int limit) -> previous cap
final prev = pdf_oxide.setMaxOpsPerStream(5000000);
// ... extract a huge trusted PDF ...
pdf_oxide.setMaxOpsPerStream(prev); // restore
R
library(pdfoxide)
# pdf_set_max_ops_per_stream(limit) -> previous cap (negative limit restores default)
prev <- pdf_set_max_ops_per_stream(5000000)
# ... extract a huge trusted PDF ...
pdf_set_max_ops_per_stream(prev) # restore
Julia
using PdfOxide
# set_max_ops_per_stream(limit::Integer) -> previous cap
prev = set_max_ops_per_stream(5_000_000)
# ... extract a huge trusted PDF ...
set_max_ops_per_stream(prev) # restore
Zig
const pdf_oxide = @import("pdf_oxide");
// setMaxOpsPerStream(limit: i64) i64 (returns previous cap)
const prev = pdf_oxide.setMaxOpsPerStream(5_000_000);
// ... extract a huge trusted PDF ...
_ = pdf_oxide.setMaxOpsPerStream(prev); // restore
Objective-C
#import "POXPdfOxide.h"
// + setMaxOpsPerStream: -> previous cap
int64_t prev = [POXConfig setMaxOpsPerStream:5000000];
// ... extract a huge trusted PDF ...
[POXConfig setMaxOpsPerStream:prev]; // restore
Elixir
# set_max_ops_per_stream(limit) -> previous cap (-1 = default was active)
prev = PdfOxide.set_max_ops_per_stream(5_000_000)
# ... extract a huge trusted PDF ...
PdfOxide.set_max_ops_per_stream(prev)
C ABI에서 pdf_oxide_set_max_ops_per_stream(limit)은 음수 limit를 "기본값 복원"으로 처리하며, 이전에 기본값이 활성화되어 있었다면 -1을 반환합니다.
매핑되지 않은 글리프(U+FFFD)를 보존하는 방법
기본적으로 고수준 접근자(extract_text / extract_words / extract_spans)는 Unicode 매핑이 없는 글리프를 필터링합니다(이 글리프들은 U+FFFD �로 표시됩니다). 페이지의 모든 가시 글리프가 U+FFFD에 매핑되는 경우—예를 들어 MSAM10 같은 수학 기호 폰트—출력이 비어 있을 수 있습니다. set_preserve_unmapped_glyphs(true)를 호출하면 접근자가 대체 문자를 유지하여 이를 확인하고 후처리할 수 있습니다. 이전 설정값을 반환합니다.
Rust
// pdf_oxide::extractors::text::set_preserve_unmapped_glyphs(preserve: bool)
// -> bool (returns previous value)
use pdf_oxide::extractors::text::set_preserve_unmapped_glyphs;
let prev = set_preserve_unmapped_glyphs(true);
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
set_preserve_unmapped_glyphs(prev);
Go
// func SetPreserveUnmappedGlyphs(preserve int) int (1 = preserve; returns previous)
prev := pdfoxide.SetPreserveUnmappedGlyphs(1)
defer pdfoxide.SetPreserveUnmappedGlyphs(prev)
C#
// static int CAbi.SetPreserveUnmappedGlyphs(bool preserve) (returns previous, 0/1)
int prev = PdfOxide.Core.CAbi.SetPreserveUnmappedGlyphs(true);
try { /* extract math-heavy PDF */ }
finally { PdfOxide.Core.CAbi.SetPreserveUnmappedGlyphs(prev != 0); }
Swift
// static func setPreserveUnmappedGlyphs(_ preserve: Int32) -> Int32
let prev = PdfOxide.setPreserveUnmappedGlyphs(1)
defer { _ = PdfOxide.setPreserveUnmappedGlyphs(prev) }
PHP
use PdfOxide\FFI\FunctionBindings;
$bindings = new FunctionBindings();
// pdfOxideSetPreserveUnmappedGlyphs(int $preserve): int (1 = preserve; returns previous, 0/1)
$prev = $bindings->pdfOxideSetPreserveUnmappedGlyphs(1);
try { /* extract math-heavy PDF */ }
finally { $bindings->pdfOxideSetPreserveUnmappedGlyphs($prev); }
Ruby
require 'pdf_oxide'
# PdfOxide.set_preserve_unmapped_glyphs(preserve) -> previous value (0 or 1)
prev = PdfOxide.set_preserve_unmapped_glyphs(true)
begin
# ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
ensure
PdfOxide.set_preserve_unmapped_glyphs(prev)
end
C++
#include <pdf_oxide/pdf_oxide.hpp>
// int pdf_oxide::set_preserve_unmapped_glyphs(int preserve) -> previous value
int prev = pdf_oxide::set_preserve_unmapped_glyphs(1);
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
pdf_oxide::set_preserve_unmapped_glyphs(prev); // restore
Dart
import 'package:pdf_oxide/pdf_oxide.dart' as pdf_oxide;
// int setPreserveUnmappedGlyphs(int preserve) -> previous value
final prev = pdf_oxide.setPreserveUnmappedGlyphs(1);
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
pdf_oxide.setPreserveUnmappedGlyphs(prev); // restore
R
library(pdfoxide)
# pdf_set_preserve_unmapped_glyphs(preserve) -> previous value (0 or 1)
prev <- pdf_set_preserve_unmapped_glyphs(1L)
# ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
pdf_set_preserve_unmapped_glyphs(prev) # restore
Julia
using PdfOxide
# set_preserve_unmapped_glyphs(preserve::Integer) -> previous value (0 or 1)
prev = set_preserve_unmapped_glyphs(1)
# ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
set_preserve_unmapped_glyphs(prev) # restore
Zig
const pdf_oxide = @import("pdf_oxide");
// setPreserveUnmappedGlyphs(preserve: bool) i32 (returns previous value)
const prev = pdf_oxide.setPreserveUnmappedGlyphs(true);
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
_ = pdf_oxide.setPreserveUnmappedGlyphs(prev != 0); // restore
Objective-C
#import "POXPdfOxide.h"
// + setPreserveUnmappedGlyphs: -> previous value (0 or 1)
int32_t prev = [POXConfig setPreserveUnmappedGlyphs:1];
// ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
[POXConfig setPreserveUnmappedGlyphs:prev]; // restore
Elixir
# set_preserve_unmapped_glyphs(preserve) -> previous value (0 or 1)
prev = PdfOxide.set_preserve_unmapped_glyphs(1)
# ... extract a math-heavy PDF; U+FFFD glyphs are now kept ...
PdfOxide.set_preserve_unmapped_glyphs(prev)
C ABI에서 pdf_oxide_set_preserve_unmapped_glyphs(preserve)는 1(보존) 또는 0(필터링)을 받아 이전 값을 0 또는 1로 반환합니다.
자주 묻는 질문
OCR 모델은 어디에 저장되나요?
$PDF_OXIDE_MODEL_DIR이 설정된 경우 해당 경로에, 그렇지 않으면 플랫폼 캐시(Linux에서는 ~/.cache/pdf_oxide/models)에 저장됩니다. 이 경로는 prefetch_models의 반환값이기도 합니다.
prefetch_models를 반복해서 호출해도 안전한가요?
네, 멱등성이 있습니다. 이미 존재하는 파일은 건너뛰므로 매 시작 시 안전망으로 호출해도 부담이 없습니다.
prefetch를 호출했는데 prefetch_available이 false를 반환하는 이유는 무엇인가요?
빌드가 ocr 피처 없이 컴파일되어 HTTP 페처가 없습니다. prefetch_models는 캐시 디렉터리를 생성하지만 아무것도 다운로드하지 않으므로, model_manifest를 사용해 파일을 수동으로 제공하세요.
전역 설정 항목을 초기화해야 하나요? 프로세스 전역으로 변경될 때까지 유지됩니다. 특정 문서에만 설정을 적용하려면 각 설정 항목이 반환하는 이전 값으로 복원하세요. 두 항목 모두 실패하지 않으며 오류 채널도 없습니다.
관련 페이지
- 스캔된 PDF OCR 처리 — 모델 프로비저닝 후 OCR 실행
- 페이지 분류 — 실제로 OCR이 필요한 페이지 판별
- 로깅 및 디버그 출력 — 기타 프로세스 전역 라이브러리 설정
- PDF에서 텍스트 추출 —
set_preserve_unmapped_glyphs가 영향을 미치는 고수준 접근자