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