Skip to content

OCR PDF — витягування тексту зі сканованих PDF за допомогою PDF Oxide

Витягуйте текст зі сканованих PDF за допомогою вбудованого OCR. Починаючи з v0.3.27, OCR доступний у всіх мовних прив’язках — Python, Node.js, Go, C# та Rust — через єдиний FFI-шар (pdf_ocr_engine_create, pdf_ocr_page_needs_ocr, pdf_ocr_extract_text).

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}");

Java

import fyi.oxide.pdf.PdfDocument;

try (PdfDocument doc = PdfDocument.open("scanned.pdf")) {
    // Lean-tier bindings have no OCR engine handle — extractTextAuto
    // routes scanned pages through OCR automatically (graceful fallback).
    String text = doc.extractTextAuto(0);
    System.out.println(text);
}

PHP

<?php
use PdfOxide\PdfDocument;

$doc = PdfDocument::open("scanned.pdf");
// No OCR engine handle in the lean tier — extractTextAuto routes
// scanned pages through OCR automatically (graceful fallback).
echo $doc->extractTextAuto(0);

Ruby

require "pdf_oxide"

doc = PdfOxide::PdfDocument.open("scanned.pdf")
# No OCR engine handle in the lean tier — extract_text_auto routes
# scanned pages through OCR automatically (graceful fallback).
puts doc.extract_text_auto(0)

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("scanned.pdf");
auto engine = pdf_oxide::OcrEngine::create("det.onnx", "rec.onnx", "dict.txt");

if (doc.ocr_page_needs_ocr(0)) {
    std::string text = doc.ocr_extract_text(0, &engine);
    std::cout << text << "\n";
}

Swift

import PdfOxide

let doc = try Document.open("scanned.pdf")
let engine = try OcrEngine.create(
    detModelPath: "det.onnx", recModelPath: "rec.onnx", dictPath: "dict.txt")

if try doc.ocrPageNeedsOcr(0) {
    let text = try doc.ocrExtractText(0, engine: engine)
    print(text)
}

Kotlin

import fyi.oxide.pdf.PdfDocument

PdfDocument.open("scanned.pdf").use { doc ->
    // Lean-tier bindings have no OCR engine handle — extractTextAuto
    // routes scanned pages through OCR automatically (graceful fallback).
    println(doc.extractTextAuto(0))
}

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('scanned.pdf');
final engine = OcrEngine.create('det.onnx', 'rec.onnx', 'dict.txt');

if (doc.pageNeedsOcr(0)) {
  print(doc.ocrExtractText(0, engine));
}
engine.close();
doc.close();

R

library(pdfoxide)

doc    <- pdf_open("scanned.pdf")
engine <- pdf_ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")

if (pdf_ocr_page_needs_ocr(doc, 0)) {
  text <- pdf_ocr_extract_text(doc, 0, engine)
  cat(text)
}

Julia

using PdfOxide

doc    = open_document("scanned.pdf")
engine = ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")

if page_needs_ocr(doc, 0)
    text = ocr_extract_text(doc, 0, engine)
    println(text)
end

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("scanned.pdf");
defer doc.deinit();
var engine = try pdf_oxide.OcrEngine.create("det.onnx", "rec.onnx", "dict.txt");
defer engine.deinit();

if (try doc.ocrPageNeedsOcr(0)) {
    const text = try doc.ocrExtractText(a, 0, engine);
    defer a.free(text);
    std.debug.print("{s}\n", .{text});
}

Scala

import fyi.oxide.pdf.PdfDocument

val doc = PdfDocument.open("scanned.pdf")
// Lean-tier bindings have no OCR engine handle — extractTextAuto
// routes scanned pages through OCR automatically (graceful fallback).
println(doc.extractTextAuto(0))
doc.close()

Clojure

(require '[pdf-oxide.core :as pdf])

(with-open [doc (pdf/open "scanned.pdf")]
  ;; Lean-tier bindings have no OCR engine handle — the AutoExtractor
  ;; routes scanned pages through OCR automatically (graceful fallback).
  (let [ax (pdf/auto-extractor doc)]
    (println (pdf/auto-text ax))))

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"scanned.pdf" error:&err];
POXOcrEngine *engine = [POXOcrEngine createWithDetModelPath:@"det.onnx"
                                              recModelPath:@"rec.onnx"
                                                  dictPath:@"dict.txt"
                                                     error:&err];

if ([doc pageNeedsOcr:0 error:&err]) {
    NSString *text = [doc ocrExtractText:0 engine:engine error:&err];
    NSLog(@"%@", text);
}

Elixir

{:ok, doc} = PdfOxide.open("scanned.pdf")
{:ok, engine} = PdfOxide.ocr_engine("det.onnx", "rec.onnx", "dict.txt")

case PdfOxide.ocr_page_needs_ocr(doc, 0) do
  {:ok, true} ->
    {:ok, text} = PdfOxide.ocr_extract_text(doc, 0, engine)
    IO.puts(text)

  _ ->
    :ok
end

PDF Oxide містить PaddleOCR через ONNX Runtime — без встановлення Tesseract, без системних залежностей, без викликів дочірніх процесів. OCR-рушій працює безпосередньо всередині процесу. Підтримуються сімейства моделей PP-OCRv3, PP-OCRv4 та PP-OCRv5.

Примітка: OCR недоступний у WebAssembly (потрібен нативний ONNX Runtime). Для Go / Node.js / C# / Rust збирайте з фічею ocr. Wheels Python постачаються з увімкненим OCR за замовчуванням.

OCR сканованих PDF у Python без Tesseract

Більшість Python-рішень для OCR потребують встановлення Tesseract як системної залежності — складного налаштування, що відрізняється залежно від операційної системи та CI-середовища. PDF Oxide включає моделі PaddleOCR безпосередньо в Python-wheel:

  • Без системних залежностей — достатньо 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 (дочірній процес)
Складність налаштування Один рядок Встановлення Tesseract для кожної ОС
CI/Docker Без додаткової конфігурації Потрібен apt-get install tesseract-ocr
Моделі включено Так (у wheel) Ні (окреме завантаження)

Встановлення

Python

pip install pdf_oxide

OCR-моделі вже включено у wheel. Додаткових завантажень не потрібно.

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-пакет постачається з увімкненим OCR у стандартних бінарних файлах для Linux / macOS / Windows — додаткова конфігурація не потрібна.

Коли використовувати OCR

Більшість PDF містять вбудований текст, який extract_text() обробляє за 0,8 мс на сторінку. OCR потрібен лише для:

  • Сканованих документів — паперових документів, відсканованих у PDF
  • PDF лише із зображеннями — PDF, створених із фотографій або скриншотів
  • PDF із текстом у вигляді зображень — деякі генератори растеризують текст
  • Гібридних сторінок — сторінок із нативним текстом і сканованими областями зображень

Версії моделей PP-OCR

PDF Oxide підтримує три покоління моделей PaddleOCR. Стандартне налаштування працює з PP-OCRv3 та PP-OCRv4. Серверні моделі PP-OCRv5 потребують іншої стратегії масштабування.

PP-OCRv3 / PP-OCRv4 (за замовчуванням)

Моделі, оптимізовані для мобільних пристроїв, що зменшують зображення до максимальної довжини сторони. Підходять для більшості документів.

  • Модель виявлення: DBNet++ (легка)
  • Модель розпізнавання: SVTR
  • Стратегія масштабування: MaxSide — зменшує довгу сторону до 960 пікс.
  • Найкраще підходить для: стандартних документів, мобільного/крайового розгортання

Python

from pdf_oxide import OcrConfig, OcrEngine

# Default config works with v3/v4 models
config = OcrConfig()
engine = OcrEngine("det_v4.onnx", "rec_v4.onnx", "dict.txt", config)

Rust

use pdf_oxide::ocr::{OcrConfig, OcrEngine};

// Default config: 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 — забезпечує мінімум 64 пікс. для короткої сторони, максимум 4000 пікс.
  • Найкраще підходить для: високоточного витягування, серверних середовищ, щільного тексту

Python

from pdf_oxide import OcrConfig, OcrEngine

# v5 config: high-resolution input for server models
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 config: 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 (зменшення до 960 пікс.) MinSide (збільшення, максимум 4000 пікс.)
Вхідна роздільна здатність Нижча (швидше) Вища (точніше)
Розмір моделі виявлення ~3 МБ ~12 МБ
Розмір моделі розпізнавання ~12 МБ ~25 МБ
Найкраще підходить для Мобільні, крайові, стандартні документи Сервер, щільний текст, дрібний шрифт
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:
        # Likely scanned — use 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 => {
            // Has both native text and scanned images — merges both sources
            let text = extract_text_with_ocr(&mut doc, i, Some(&engine), OcrExtractOptions::default())?;
            println!("Page {} (hybrid): {}...", i + 1, &text[..100.min(text.len())]);
        }
    }
}

Java

import fyi.oxide.pdf.PdfDocument;

try (PdfDocument doc = PdfDocument.open("mixed.pdf")) {
    for (int i = 0; i < doc.pageCount(); i++) {
        // extractTextAuto classifies each page and routes scanned
        // pages through OCR automatically (graceful fallback).
        System.out.printf("Page %d: %s%n", i + 1, doc.extractTextAuto(i));
    }
}

PHP

<?php
use PdfOxide\PdfDocument;

$doc = PdfDocument::open("mixed.pdf");
for ($i = 0; $i < $doc->pageCount(); $i++) {
    // extractTextAuto classifies each page and routes scanned
    // pages through OCR automatically (graceful fallback).
    printf("Page %d: %s\n", $i + 1, $doc->extractTextAuto($i));
}

Ruby

require "pdf_oxide"

doc = PdfOxide::PdfDocument.open("mixed.pdf")
doc.page_count.times do |i|
  # extract_text_auto classifies each page and routes scanned
  # pages through OCR automatically (graceful fallback).
  puts "Page #{i + 1}: #{doc.extract_text_auto(i)}"
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("mixed.pdf");
auto engine = pdf_oxide::OcrEngine::create("det.onnx", "rec.onnx", "dict.txt");

for (int i = 0; i < doc.page_count(); ++i) {
    std::string text = doc.ocr_page_needs_ocr(i)
        ? doc.ocr_extract_text(i, &engine)   // scanned / hybrid → OCR
        : doc.extract_text(i);               // native text
    std::cout << "Page " << (i + 1) << ": " << text << "\n";
}

Swift

import PdfOxide

let doc = try Document.open("mixed.pdf")
let engine = try OcrEngine.create(
    detModelPath: "det.onnx", recModelPath: "rec.onnx", dictPath: "dict.txt")

for i in 0..<(try doc.pageCount()) {
    let text = try doc.ocrPageNeedsOcr(i)
        ? doc.ocrExtractText(i, engine: engine)   // scanned / hybrid → OCR
        : doc.extractText(i)                      // native text
    print("Page \(i + 1): \(text)")
}

Kotlin

import fyi.oxide.pdf.PdfDocument

PdfDocument.open("mixed.pdf").use { doc ->
    for (i in 0 until doc.pageCount()) {
        // extractTextAuto classifies each page and routes scanned
        // pages through OCR automatically (graceful fallback).
        println("Page ${i + 1}: ${doc.extractTextAuto(i)}")
    }
}

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('mixed.pdf');
final engine = OcrEngine.create('det.onnx', 'rec.onnx', 'dict.txt');

for (var i = 0; i < doc.pageCount; i++) {
  final text = doc.pageNeedsOcr(i)
      ? doc.ocrExtractText(i, engine)   // scanned / hybrid → OCR
      : doc.extractText(i);             // native text
  print('Page ${i + 1}: $text');
}
engine.close();
doc.close();

R

library(pdfoxide)

doc    <- pdf_open("mixed.pdf")
engine <- pdf_ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")

for (i in seq_len(pdf_page_count(doc)) - 1) {
  text <- if (pdf_ocr_page_needs_ocr(doc, i)) {
    pdf_ocr_extract_text(doc, i, engine)   # scanned / hybrid -> OCR
  } else {
    pdf_extract_text(doc, i)               # native text
  }
  cat(sprintf("Page %d: %s\n", i + 1, text))
}

Julia

using PdfOxide

doc    = open_document("mixed.pdf")
engine = ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")

for i in 0:(page_count(doc) - 1)
    text = page_needs_ocr(doc, i) ?
        ocr_extract_text(doc, i, engine) :   # scanned / hybrid -> OCR
        extract_text(doc, i)                 # native text
    println("Page $(i + 1): $text")
end

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("mixed.pdf");
defer doc.deinit();
var engine = try pdf_oxide.OcrEngine.create("det.onnx", "rec.onnx", "dict.txt");
defer engine.deinit();

var i: i32 = 0;
const n = try doc.pageCount();
while (i < n) : (i += 1) {
    const text = if (try doc.ocrPageNeedsOcr(i))
        try doc.ocrExtractText(a, i, engine)   // scanned / hybrid → OCR
    else
        try doc.extractText(a, i);             // native text
    defer a.free(text);
    std.debug.print("Page {d}: {s}\n", .{ i + 1, text });
}

Scala

import fyi.oxide.pdf.PdfDocument

val doc = PdfDocument.open("mixed.pdf")
for (i <- 0 until doc.pageCount) {
  // extractTextAuto classifies each page and routes scanned
  // pages through OCR automatically (graceful fallback).
  println(s"Page ${i + 1}: ${doc.extractTextAuto(i)}")
}
doc.close()

Clojure

(require '[pdf-oxide.core :as pdf])

(with-open [doc (pdf/open "mixed.pdf")]
  ;; The AutoExtractor classifies each page and routes scanned pages
  ;; through OCR automatically (graceful fallback).
  (println (pdf/auto-text (pdf/auto-extractor doc))))

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"mixed.pdf" error:&err];
POXOcrEngine *engine = [POXOcrEngine createWithDetModelPath:@"det.onnx"
                                              recModelPath:@"rec.onnx"
                                                  dictPath:@"dict.txt"
                                                     error:&err];

NSInteger n = [doc pageCountError:&err];
for (NSInteger i = 0; i < n; i++) {
    NSString *text = [doc pageNeedsOcr:i error:&err]
        ? [doc ocrExtractText:i engine:engine error:&err]   // scanned / hybrid → OCR
        : [doc extractText:i error:&err];                   // native text
    NSLog(@"Page %ld: %@", (long)(i + 1), text);
}

Elixir

{:ok, doc} = PdfOxide.open("mixed.pdf")
{:ok, engine} = PdfOxide.ocr_engine("det.onnx", "rec.onnx", "dict.txt")
{:ok, n} = PdfOxide.page_count(doc)

for i <- 0..(n - 1) do
  {:ok, text} =
    case PdfOxide.ocr_page_needs_ocr(doc, i) do
      {:ok, true} -> PdfOxide.ocr_extract_text(doc, i, engine)   # scanned / hybrid -> OCR
      _ -> PdfOxide.extract_text(doc, i)                         # native text
    end

  IO.puts("Page #{i + 1}: #{text}")
end

Варіанти PageType (Rust)

Варіант Опис
NativeText Сторінка містить вбудований текст — OCR не потрібен
ScannedPage Сторінка повністю скановано (велике зображення, текст відсутній або мінімальний) — повний OCR
HybridPage Сторінка містить і нативний текст, і скановані зображення — об’єднує нативний текст і результати OCR

Допоміжна функція needs_ocr() повертає true як для ScannedPage, так і для HybridPage:

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. Зображення масштабується відповідно до стратегії виявлення (MaxSide для v3/v4, MinSide для v5)
  3. DBNet++ знаходить текстові області у вигляді чотирикутних обмежувальних рамок
  4. SVTR зчитує символи з кожної виявленої області
  5. Результати збираються в текст із сортуванням за порядком читання
  6. Для гібридних сторінок OCR-текст об’єднується з нативним текстом

Увесь конвеєр працює всередині процесу через ONNX Runtime. Жодних зовнішніх бінарних файлів, дочірніх процесів і тимчасових файлів.


Налаштування OCR

Python

from pdf_oxide import OcrConfig, OcrEngine

# Default (v3/v4)
config = OcrConfig()

# PP-OCRv5 server models
config = OcrConfig(use_v5=True)

# Custom thresholds
config = OcrConfig(
    det_threshold=0.5,    # Detection confidence (0.0-1.0)
    box_threshold=0.7,    # Box confidence (0.0-1.0)
    rec_threshold=0.6,    # Recognition confidence (0.0-1.0)
    num_threads=8,        # ONNX Runtime threads
    max_candidates=500,   # Max text regions
)

# v5 with custom thresholds
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};

// Default (v3/v4): MaxSide { max_side: 960 }
let config = OcrConfig::default();

// PP-OCRv5: MinSide { min_side: 64, max_side_limit: 4000 }
let config = OcrConfig::v5();

// Custom builder
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)        // Enable style detection from OCR geometry
    .build();

// Custom resize strategy
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)    // Infer styles from text geometry
    .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

Керуйте роздільною здатністю рендерингу при конвертуванні PDF-сторінок у зображення для OCR:

Python

from pdf_oxide import PdfDocument

doc = PdfDocument("scanned.pdf")

# Default is 300 DPI — good balance of accuracy and speed
text = doc.extract_text_ocr(0)

# Higher DPI for better accuracy on fine print
text = doc.extract_text_ocr(0)  # DPI configured via OcrExtractOptions in Rust

Rust

use pdf_oxide::ocr::OcrExtractOptions;

// Higher DPI = better accuracy but slower
let options = OcrExtractOptions::default().with_dpi(300.0);

// Lower DPI = faster but less accurate
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)?;

// Full text in reading order
println!("{}", output.text_in_reading_order());

// Per-span details
for span in &output.spans {
    println!("Text: '{}' (confidence: {:.2})", span.text, span.confidence);
    println!("  Bounding box: {:?}", span.bounding_rect());
    println!("  Per-char confidence: {:?}", span.char_confidences);
}

// Overall confidence
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:
        # Scanned page — OCR then format
        text = doc.extract_text_ocr(i)
        md = text  # OCR output is plain text
    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);
}

Java

import fyi.oxide.pdf.PdfDocument;

try (PdfDocument doc = PdfDocument.open("scanned-report.pdf")) {
    for (int i = 0; i < doc.pageCount(); i++) {
        String md = doc.toMarkdown(i);
        if (md.strip().length() < 50) {
            // Scanned page — auto-routing returns OCR text (plain).
            md = doc.extractTextAuto(i);
        }
        System.out.printf("--- Page %d ---%n%s%n", i + 1, md);
    }
}

PHP

<?php
use PdfOxide\PdfDocument;

$doc = PdfDocument::open("scanned-report.pdf");
for ($i = 0; $i < $doc->pageCount(); $i++) {
    $md = $doc->toMarkdown($i);
    if (strlen(trim($md)) < 50) {
        // Scanned page — auto-routing returns OCR text (plain).
        $md = $doc->extractTextAuto($i);
    }
    printf("--- Page %d ---\n%s\n", $i + 1, $md);
}

Ruby

require "pdf_oxide"

doc = PdfOxide::PdfDocument.open("scanned-report.pdf")
doc.page_count.times do |i|
  md = doc.to_markdown(i)
  if md.strip.length < 50
    # Scanned page — auto-routing returns OCR text (plain).
    md = doc.extract_text_auto(i)
  end
  puts "--- Page #{i + 1} ---\n#{md}"
end

C++

#include <pdf_oxide/pdf_oxide.hpp>

auto doc = pdf_oxide::Document::open("scanned-report.pdf");
auto engine = pdf_oxide::OcrEngine::create("det.onnx", "rec.onnx", "dict.txt");

for (int i = 0; i < doc.page_count(); ++i) {
    std::string text = doc.ocr_page_needs_ocr(i)
        ? doc.ocr_extract_text(i, &engine)   // scanned / hybrid → OCR
        : doc.to_markdown(i);                // native → Markdown
    std::cout << "--- Page " << (i + 1) << " ---\n" << text << "\n";
}

Swift

import PdfOxide

let doc = try Document.open("scanned-report.pdf")
let engine = try OcrEngine.create(
    detModelPath: "det.onnx", recModelPath: "rec.onnx", dictPath: "dict.txt")

for i in 0..<(try doc.pageCount()) {
    let text = try doc.ocrPageNeedsOcr(i)
        ? doc.ocrExtractText(i, engine: engine)   // scanned / hybrid → OCR
        : doc.toMarkdown(i)                       // native → Markdown
    print("--- Page \(i + 1) ---\n\(text)")
}

Kotlin

import fyi.oxide.pdf.PdfDocument

PdfDocument.open("scanned-report.pdf").use { doc ->
    for (i in 0 until doc.pageCount()) {
        var md = doc.toMarkdown(i)
        if (md.trim().length < 50) {
            // Scanned page — auto-routing returns OCR text (plain).
            md = doc.extractTextAuto(i)
        }
        println("--- Page ${i + 1} ---\n$md")
    }
}

Dart

import 'package:pdf_oxide/pdf_oxide.dart';

final doc = PdfDocument.open('scanned-report.pdf');
final engine = OcrEngine.create('det.onnx', 'rec.onnx', 'dict.txt');

for (var i = 0; i < doc.pageCount; i++) {
  final text = doc.pageNeedsOcr(i)
      ? doc.ocrExtractText(i, engine)   // scanned / hybrid → OCR
      : doc.toMarkdown(i);              // native → Markdown
  print('--- Page ${i + 1} ---\n$text');
}
engine.close();
doc.close();

R

library(pdfoxide)

doc    <- pdf_open("scanned-report.pdf")
engine <- pdf_ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")

for (i in seq_len(pdf_page_count(doc)) - 1) {
  text <- if (pdf_ocr_page_needs_ocr(doc, i)) {
    pdf_ocr_extract_text(doc, i, engine)   # scanned / hybrid -> OCR
  } else {
    pdf_to_markdown(doc, i)                # native -> Markdown
  }
  cat(sprintf("--- Page %d ---\n%s\n", i + 1, text))
}

Julia

using PdfOxide

doc    = open_document("scanned-report.pdf")
engine = ocr_engine_create("det.onnx", "rec.onnx", "dict.txt")

for i in 0:(page_count(doc) - 1)
    text = page_needs_ocr(doc, i) ?
        ocr_extract_text(doc, i, engine) :   # scanned / hybrid -> OCR
        to_markdown(doc, i)                  # native -> Markdown
    println("--- Page $(i + 1) ---\n$text")
end

Zig

const pdf_oxide = @import("pdf_oxide");
const a = std.heap.page_allocator;

var doc = try pdf_oxide.Document.open("scanned-report.pdf");
defer doc.deinit();
var engine = try pdf_oxide.OcrEngine.create("det.onnx", "rec.onnx", "dict.txt");
defer engine.deinit();

var i: i32 = 0;
const n = try doc.pageCount();
while (i < n) : (i += 1) {
    const text = if (try doc.ocrPageNeedsOcr(i))
        try doc.ocrExtractText(a, i, engine)   // scanned / hybrid → OCR
    else
        try doc.toMarkdown(a, i);              // native → Markdown
    defer a.free(text);
    std.debug.print("--- Page {d} ---\n{s}\n", .{ i + 1, text });
}

Scala

import fyi.oxide.pdf.PdfDocument

val doc = PdfDocument.open("scanned-report.pdf")
for (i <- 0 until doc.pageCount) {
  var md = doc.toMarkdown(i)
  if (md.trim.length < 50) {
    // Scanned page — auto-routing returns OCR text (plain).
    md = doc.extractTextAuto(i)
  }
  println(s"--- Page ${i + 1} ---\n$md")
}
doc.close()

Clojure

(require '[pdf-oxide.core :as pdf])

(with-open [doc (pdf/open "scanned-report.pdf")]
  ;; The AutoExtractor routes scanned pages through OCR automatically.
  (println (pdf/auto-text (pdf/auto-extractor doc))))

Objective-C

#import "POXPdfOxide.h"
NSError *err = nil;

POXDocument *doc = [POXDocument openPath:@"scanned-report.pdf" error:&err];
POXOcrEngine *engine = [POXOcrEngine createWithDetModelPath:@"det.onnx"
                                              recModelPath:@"rec.onnx"
                                                  dictPath:@"dict.txt"
                                                     error:&err];

NSInteger n = [doc pageCountError:&err];
for (NSInteger i = 0; i < n; i++) {
    NSString *text = [doc pageNeedsOcr:i error:&err]
        ? [doc ocrExtractText:i engine:engine error:&err]   // scanned / hybrid → OCR
        : [doc toMarkdown:i error:&err];                    // native → Markdown
    NSLog(@"--- Page %ld ---\n%@", (long)(i + 1), text);
}

Elixir

{:ok, doc} = PdfOxide.open("scanned-report.pdf")
{:ok, engine} = PdfOxide.ocr_engine("det.onnx", "rec.onnx", "dict.txt")
{:ok, n} = PdfOxide.page_count(doc)

for i <- 0..(n - 1) do
  {:ok, text} =
    case PdfOxide.ocr_page_needs_ocr(doc, i) do
      {:ok, true} -> PdfOxide.ocr_extract_text(doc, i, engine)   # scanned / hybrid -> OCR
      _ -> PdfOxide.to_markdown(doc, i)                          # native -> Markdown
    end

  IO.puts("--- Page #{i + 1} ---\n#{text}")
end

Міркування щодо продуктивності

OCR значно повільніший за витягування тексту:

Операція Типова швидкість
Витягування тексту 0,8 мс на сторінку
OCR (v3/v4) 200–1000 мс на сторінку
OCR (v5 сервер) 500–2000 мс на сторінку

Швидкість 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())?;

Пов’язані сторінки