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}