Skip to main content

pdf_ocr/
engine.rs

1//! Pluggable OCR engine trait and built-in implementations.
2//!
3//! Provides the `OcrEngine` trait for integrating different OCR backends,
4//! and a `NoOpEngine` for testing.
5
6// Copyright (c) 2026 Innovation Trigger B.V.
7//
8// PDFluent is available under two licences, at your option: the GNU AGPLv3, or
9// the PDFluent Commercial Licence. See the LICENSE file in this repository --
10// that file travels with the copy you received, which a URL does not.
11
12/// A single word recognized by OCR.
13#[derive(Debug, Clone)]
14pub struct OcrWord {
15    /// The recognized text.
16    pub text: String,
17    /// Bounding box in pixel coordinates [x0, y0, x1, y1].
18    pub bbox_px: [u32; 4],
19    /// Recognition confidence (0.0 to 1.0).
20    pub confidence: f32,
21}
22
23/// Result of OCR processing on a single page.
24#[derive(Debug, Clone)]
25pub struct OcrPageResult {
26    /// Recognized words.
27    pub words: Vec<OcrWord>,
28    /// Overall confidence for the page.
29    pub confidence: f32,
30    /// Width of the source image in pixels.
31    pub image_width: u32,
32    /// Height of the source image in pixels.
33    pub image_height: u32,
34}
35
36impl OcrPageResult {
37    /// Get the full text of the page by joining all words with spaces.
38    pub fn full_text(&self) -> String {
39        self.words
40            .iter()
41            .map(|w| w.text.as_str())
42            .collect::<Vec<_>>()
43            .join(" ")
44    }
45}
46
47/// Trait for pluggable OCR engines.
48///
49/// Implementors provide character recognition on rasterized page images.
50pub trait OcrEngine: Send + Sync {
51    /// Recognize text in an image.
52    ///
53    /// # Arguments
54    /// * `image_data` - Raw pixel data (RGB, row-major).
55    /// * `width` - Image width in pixels.
56    /// * `height` - Image height in pixels.
57    /// * `dpi` - Resolution in dots per inch.
58    fn recognize(
59        &self,
60        image_data: &[u8],
61        width: u32,
62        height: u32,
63        dpi: u32,
64    ) -> std::result::Result<OcrPageResult, String>;
65
66    /// Return the list of supported languages.
67    fn supported_languages(&self) -> Vec<String>;
68}
69
70/// A no-op OCR engine that always returns empty results.
71///
72/// Useful for testing the pipeline without a real OCR backend.
73#[derive(Debug, Default)]
74pub struct NoOpEngine;
75
76impl OcrEngine for NoOpEngine {
77    fn recognize(
78        &self,
79        _image_data: &[u8],
80        width: u32,
81        height: u32,
82        _dpi: u32,
83    ) -> std::result::Result<OcrPageResult, String> {
84        Ok(OcrPageResult {
85            words: Vec::new(),
86            confidence: 0.0,
87            image_width: width,
88            image_height: height,
89        })
90    }
91
92    fn supported_languages(&self) -> Vec<String> {
93        Vec::new()
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    #[test]
102    fn noop_engine_returns_empty() {
103        let engine = NoOpEngine;
104        let result = engine.recognize(&[], 100, 100, 300).unwrap();
105        assert!(result.words.is_empty());
106        assert_eq!(result.confidence, 0.0);
107        assert_eq!(result.image_width, 100);
108        assert_eq!(result.image_height, 100);
109        assert!(engine.supported_languages().is_empty());
110    }
111
112    #[test]
113    fn ocr_page_result_full_text() {
114        let result = OcrPageResult {
115            words: vec![
116                OcrWord {
117                    text: "Hello".to_string(),
118                    bbox_px: [0, 0, 50, 20],
119                    confidence: 0.95,
120                },
121                OcrWord {
122                    text: "World".to_string(),
123                    bbox_px: [60, 0, 110, 20],
124                    confidence: 0.90,
125                },
126            ],
127            confidence: 0.92,
128            image_width: 200,
129            image_height: 100,
130        };
131        assert_eq!(result.full_text(), "Hello World");
132    }
133}