monocr_onnx/lib.rs
1//! MonOcr - Mon language OCR library using ONNX models
2//!
3//! This library provides OCR (Optical Character Recognition) functionality for Mon text
4//! using deep learning models. It supports reading text from images and PDFs, with optional
5//! accuracy measurement against ground truth text.
6//!
7//! # The model
8//!
9//! Weights are downloaded from [janakhpon/monocr](https://huggingface.co/janakhpon/monocr),
10//! pinned to revision [`model_manager::MODEL_REVISION`]. That artifact takes a
11//! `[1, 1, 160, 1024]` input and emits `[1, sequence, 277]` logits: 276
12//! characters plus the CTC blank. The width is static: v3.5 accepts 1024 and
13//! nothing else, where v2 accepted any width.
14//!
15//! The charset, the input height and the classifier width are one contract. If
16//! they drift apart the model still runs and still returns text — it is just the
17//! wrong text, with no error anywhere. So the graph is read on load and a
18//! disagreement yields [`ModelContractError`] instead of a result.
19//!
20//! # Quick Start
21//!
22//! ```no_run
23//! use monocr_onnx::read_image;
24//!
25//! #[tokio::main]
26//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
27//! let text = read_image("path/to/image.png").await?;
28//! println!("Recognized text: {}", text);
29//! Ok(())
30//! }
31//! ```
32//!
33//! # Features
34//!
35//! - Read text from single images (PNG, JPG, etc.)
36//! - Read text from multiple images in batch
37//! - Read text from PDF files (requires poppler-utils)
38//! - Measure OCR accuracy against ground truth
39//! - Customizable model paths and character sets
40//! - Line segmentation for full page OCR
41//! - Lines too wide for the 1024px model window are tiled at whitespace columns
42//! rather than squeezed into it; see [`MonOcr::predict_page`] for the measured
43//! reason
44
45use anyhow::Result;
46use std::path::Path;
47
48pub mod model_manager;
49mod monocr;
50pub mod segmenter;
51mod utils;
52
53pub use model_manager::ModelManager;
54pub use monocr::{
55 normalize_charset, normalize_polarity, page_text, BBox, LineResult, ModelContractError, MonOcr,
56 MonOcrBuilder, DEFAULT_INPUT_WIDTH, EXPECTED_INPUT_HEIGHT,
57};
58pub use segmenter::{
59 cut_column, tile_line, CUT_INK_THRESHOLD, CUT_SEARCH_FRACTION, DEFAULT_DENSITY_THRESHOLD_RATIO,
60};
61pub use utils::calculate_accuracy;
62
63/// Read text from a single image file
64///
65/// This function initializes a new MonOcr instance with default settings and performs
66/// OCR on the given image. The image is automatically segmented into lines, and each
67/// line is recognized using the ONNX model.
68///
69/// # Arguments
70///
71/// * `image_path` - Path to the image file (PNG, JPG, BMP, etc.)
72///
73/// # Returns
74///
75/// Returns a `Result<String>` containing the recognized text, with lines separated by newlines.
76///
77/// # Example
78///
79/// ```no_run
80/// use monocr_onnx::read_image;
81///
82/// #[tokio::main]
83/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
84/// let text = read_image("document.png").await?;
85/// println!("Recognized: {}", text);
86/// Ok(())
87/// }
88/// ```
89pub async fn read_image(image_path: impl AsRef<Path>) -> Result<String> {
90 let mut ocr = MonOcr::builder().build().await?;
91 ocr.read_image(image_path).await
92}
93
94/// Read text from multiple image files
95///
96/// This function processes multiple images in sequence, returning a vector of recognized texts.
97/// Each image is segmented into lines and processed individually.
98///
99/// # Arguments
100///
101/// * `image_paths` - A slice of paths to image files
102///
103/// # Returns
104///
105/// Returns a `Result<Vec<String>>` where each element contains the recognized text
106/// from the corresponding image. Lines within each text are separated by newlines.
107///
108/// # Example
109///
110/// ```no_run
111/// use monocr_onnx::read_images;
112///
113/// #[tokio::main]
114/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
115/// let paths = vec!["page1.png", "page2.png", "page3.png"];
116/// let results = read_images(&paths).await?;
117/// for (i, text) in results.iter().enumerate() {
118/// println!("Page {}: {}", i + 1, text);
119/// }
120/// Ok(())
121/// }
122/// ```
123pub async fn read_images(image_paths: &[impl AsRef<Path>]) -> Result<Vec<String>> {
124 let mut ocr = MonOcr::builder().build().await?;
125 ocr.read_images(image_paths).await
126}
127
128/// Read text from a PDF file
129///
130/// This function converts a PDF document to images (using pdftoppm from poppler-utils)
131/// and performs OCR on each page. Each page is treated as a separate image.
132///
133/// # Arguments
134///
135/// * `pdf_path` - Path to the PDF file
136///
137/// # Returns
138///
139/// Returns a `Result<Vec<String>>` where each element contains the recognized text
140/// from the corresponding page.
141///
142/// # Requirements
143///
144/// This function requires `pdftoppm` from the poppler-utils package to be installed:
145/// - Ubuntu/Debian: `sudo apt-get install poppler-utils`
146/// - macOS: `brew install poppler`
147/// - Fedora/RHEL: `sudo dnf install poppler-utils`
148///
149/// # Example
150///
151/// ```no_run
152/// use monocr_onnx::read_pdf;
153///
154/// #[tokio::main]
155/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
156/// let pages = read_pdf("document.pdf").await?;
157/// for (i, text) in pages.iter().enumerate() {
158/// println!("=== Page {} ===\n{}", i + 1, text);
159/// }
160/// Ok(())
161/// }
162/// ```
163pub async fn read_pdf(pdf_path: impl AsRef<Path>) -> Result<Vec<String>> {
164 let mut ocr = MonOcr::builder().build().await?;
165 ocr.read_pdf(pdf_path).await
166}
167
168/// Read text from an image with accuracy measurement
169///
170/// This function performs OCR on an image and calculates the accuracy by comparing
171/// the recognized text against the ground truth using Levenshtein distance.
172///
173/// # Arguments
174///
175/// * `image_path` - Path to the image file
176/// * `ground_truth` - The expected/ground truth text to compare against
177///
178/// # Returns
179///
180/// Returns a `Result<OcrResult>` containing:
181/// - `text`: The recognized text from the image
182/// - `accuracy`: A percentage (0-100) representing how close the recognized text is
183/// to the ground truth
184///
185/// # Accuracy Calculation
186///
187/// Accuracy is calculated as: `(1 - CER) * 100` where CER is the Character Error Rate
188/// (Levenshtein distance divided by the maximum length of the two strings).
189/// This gives a percentage score where 100% means perfect recognition.
190///
191/// # Example
192///
193/// ```no_run
194/// use monocr_onnx::read_image_with_accuracy;
195///
196/// #[tokio::main]
197/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
198/// let result = read_image_with_accuracy("image.png", "ဘာသာမန်").await?;
199/// println!("Recognized: {}", result.text);
200/// println!("Accuracy: {:.2}%", result.accuracy);
201/// Ok(())
202/// }
203/// ```
204pub async fn read_image_with_accuracy(
205 image_path: impl AsRef<Path>,
206 ground_truth: &str,
207) -> Result<OcrResult> {
208 let mut ocr = MonOcr::builder().build().await?;
209 ocr.read_image_with_accuracy(image_path, ground_truth).await
210}
211
212/// OCR result containing recognized text and accuracy measurement
213///
214/// This struct is returned by [`read_image_with_accuracy`] and contains both
215/// the recognized text and the accuracy score when compared against ground truth.
216///
217/// # Fields
218///
219/// * `text` - The recognized text from the OCR process
220/// * `accuracy` - A percentage value (0-100) indicating how closely the recognized
221/// text matches the ground truth
222///
223/// # Example
224///
225/// ```no_run
226/// use monocr_onnx::read_image_with_accuracy;
227///
228/// #[tokio::main]
229/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
230/// let result = read_image_with_accuracy("test.png", "Hello World").await?;
231/// if result.accuracy >= 90.0 {
232/// println!("Good recognition: {}", result.text);
233/// } else {
234/// println!("Poor recognition: {} ({}% accuracy)", result.text, result.accuracy);
235/// }
236/// Ok(())
237/// }
238/// ```
239#[derive(Debug, Clone)]
240pub struct OcrResult {
241 /// The recognized text from the image
242 pub text: String,
243 /// Accuracy percentage (0-100) based on Levenshtein distance
244 pub accuracy: f64,
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 /// The pinned model (`model_manager::MODEL_REVISION`) emits 277 classes:
252 /// 276 characters plus the CTC blank at index 0.
253 const PINNED_CHARSET_LEN: usize = 276;
254
255 const EMBEDDED_CHARSET: &str = include_str!("charset.txt");
256
257 #[test]
258 fn embedded_charset_matches_the_pinned_model() {
259 let n = normalize_charset(EMBEDDED_CHARSET).chars().count();
260 assert_eq!(
261 n,
262 PINNED_CHARSET_LEN,
263 "bundled charset has {n} characters, the pinned model expects {PINNED_CHARSET_LEN} \
264 ({} classes minus the CTC blank)",
265 PINNED_CHARSET_LEN + 1
266 );
267 }
268
269 /// The charset's first character is U+0020. A bare `.trim()` eats it,
270 /// dropping 276 to 275 and shifting every index in the decode by one — the
271 /// model still runs and still returns text, just the wrong text.
272 #[test]
273 fn embedded_charset_keeps_its_leading_space() {
274 let charset = normalize_charset(EMBEDDED_CHARSET);
275 assert_eq!(
276 charset.chars().next(),
277 Some(' '),
278 "charset must start with U+0020"
279 );
280 assert_eq!(
281 charset.trim().chars().count(),
282 PINNED_CHARSET_LEN - 1,
283 "expected .trim() to drop exactly the leading space"
284 );
285 }
286
287 #[test]
288 fn normalize_charset_trims_only_line_terminators() {
289 assert_eq!(normalize_charset(" abc"), " abc");
290 assert_eq!(normalize_charset(" abc\n"), " abc");
291 assert_eq!(normalize_charset(" abc\r\n"), " abc");
292 assert_eq!(normalize_charset("\n abc\n"), " abc");
293 // A trailing space is a class too.
294 assert_eq!(normalize_charset(" abc "), " abc ");
295 }
296
297 #[tokio::test]
298 #[ignore = "requires network access to download model from HuggingFace"]
299 async fn test_builder() {
300 let builder = MonOcr::builder();
301 assert!(builder.build().await.is_ok());
302 }
303}