1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
//! Pluggable OCR backend trait and error types.
//!
//! Provides [`OcrBackend`] — an abstraction over OCR engines.
//!
//! Supported user-facing OCR is currently limited to:
//! - `ocr-tesseract` — system Tesseract via `tesseract-rs`.
//!
//! Experimental scaffolding is kept separate and is not exposed as a supported
//! CLI backend:
//! - `ocr-onnx` — generic tract/CTC helper with no stable model contract yet.
//! - `ocr-neural` — placeholder Candle backend; `load()` returns a clear
//! unsupported-backend error until a concrete model implementation lands.
//!
//! ## Design
//!
//! The trait is a deliberate runtime seam, not a speculative abstraction: the
//! `djvu ocr --backend` CLI selector builds a `Box<dyn OcrBackend>` and drives
//! it polymorphically. It is retained by decision even though only Tesseract is
//! fully wired today. See `docs/ocr-backend-seam.md` (issue #382) for the
//! rationale and the deferred plan for `OcrOptions`.
//!
//! [`OcrBackend`]: crate::ocr::OcrBackend
use crate::pixmap::Pixmap;
use crate::text::TextLayer;
/// Error type for OCR operations.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum OcrError {
/// The OCR engine failed to initialize.
#[error("OCR init failed: {0}")]
InitFailed(String),
/// Recognition failed on a page image.
#[error("OCR recognition failed: {0}")]
RecognitionFailed(String),
/// The specified language or model is not available.
#[error("OCR model/language not found: {0}")]
ModelNotFound(String),
/// Model bytes do not match their pinned manifest entry (#693).
///
/// Raised by the `ocr-onnx` manifest loader when a model file's byte size
/// or SHA-256 differs from `docs/ocr-model-manifest.toml`. Unverified
/// weights are never loaded — this is a hard error, not a warning.
#[error("model verification failed for '{name}': {detail}")]
ModelVerificationFailed {
/// Manifest entry name (e.g. "ppocr-v4-mobile-det").
name: String,
/// What differed: size or SHA-256, with expected/actual values.
detail: String,
},
/// The embedded model manifest is malformed (#693).
///
/// Indicates a bug in `docs/ocr-model-manifest.toml` itself; guarded by
/// unit tests, so callers should never see this for the built-in manifest.
#[error("model manifest invalid: {0}")]
ManifestInvalid(String),
/// I/O error (e.g. loading model file).
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
}
/// Configuration for an OCR run.
///
/// These are **advisory hints**: each backend honours the fields that apply to
/// it and ignores the rest. The Tesseract backend uses both; an ONNX model fixes
/// its own input size and vocabulary at load time and ignores them. A
/// model-neutral recast is deferred until a second CLI-live backend needs to be
/// configured through the trait — see `docs/ocr-backend-seam.md`.
#[derive(Debug, Clone)]
pub struct OcrOptions {
/// Languages to recognize (e.g. "eng", "rus+eng"). Advisory; ignored by
/// backends whose model is not language-parameterized.
pub languages: String,
/// Page DPI (helps OCR engines scale internally). Advisory; ignored by
/// backends with a fixed input resolution.
pub dpi: u32,
}
impl Default for OcrOptions {
fn default() -> Self {
Self {
languages: "eng".into(),
dpi: 300,
}
}
}
/// Trait for pluggable OCR backends.
///
/// Implementations receive a rendered page pixmap and return a structured
/// text layer that can be written back into the DjVu file as a TXTz chunk.
pub trait OcrBackend {
/// Recognize text in the given page image.
///
/// Returns a [`TextLayer`] whose bounding boxes are in the pixmap's
/// coordinate system (top-left origin).
///
/// **Minimum granularity guarantee.** Callers may rely only on a populated
/// top-level [`TextLayer`]`::text` string and at least one page-level zone;
/// the richer `page -> line -> word` hierarchy is best-effort and
/// backend-dependent. The Tesseract backend produces the full hierarchy;
/// the `ocr-onnx` neural pipeline emits `page -> line -> word` with
/// *heuristic* word rects (proportional split of the line box); other
/// experimental backends (see the module docs) may emit a coarser tree or
/// none at all. Consumers that need word-level rects (e.g. the hOCR/ALTO
/// exporters) must tolerate a flatter layer.
fn recognize(&self, pixmap: &Pixmap, options: &OcrOptions) -> Result<TextLayer, OcrError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ocr_options_default_values() {
let opts = OcrOptions::default();
assert_eq!(opts.languages, "eng");
assert_eq!(opts.dpi, 300);
}
}