Skip to main content

superbook_pdf/deskew/
types.rs

1//! Deskew module core types
2//!
3//! Contains basic data structures for skew detection and correction.
4
5use std::path::{Path, PathBuf};
6use thiserror::Error;
7
8// ============================================================
9// Constants
10// ============================================================
11
12/// Default maximum angle for deskew detection (degrees)
13pub const DEFAULT_MAX_ANGLE: f64 = 15.0;
14
15/// Default threshold angle - angles below this are not corrected (degrees)
16pub const DEFAULT_THRESHOLD_ANGLE: f64 = 0.1;
17
18/// Default background color (white) for filled areas after rotation
19pub const DEFAULT_BACKGROUND_COLOR: [u8; 3] = [255, 255, 255];
20
21/// Grayscale threshold for binarization in projection analysis
22pub const GRAYSCALE_THRESHOLD: u8 = 128;
23
24/// White pixel value for image processing
25pub const WHITE_PIXEL: u8 = 255;
26
27/// Fully opaque alpha value for RGBA images
28pub const ALPHA_OPAQUE: u8 = 255;
29
30// ============================================================
31// Error Types
32// ============================================================
33
34/// Deskew error types
35#[derive(Debug, Error)]
36pub enum DeskewError {
37    #[error("Image not found: {0}")]
38    ImageNotFound(PathBuf),
39
40    #[error("Invalid image format: {0}")]
41    InvalidFormat(String),
42
43    #[error("Detection failed: {0}")]
44    DetectionFailed(String),
45
46    #[error("Correction failed: {0}")]
47    CorrectionFailed(String),
48
49    #[error("IO error: {0}")]
50    IoError(#[from] std::io::Error),
51}
52
53pub type Result<T> = std::result::Result<T, DeskewError>;
54
55// ============================================================
56// Options and Enums
57// ============================================================
58
59/// Deskew detection algorithms
60#[derive(Debug, Clone, Copy, Default)]
61pub enum DeskewAlgorithm {
62    /// Hough line transform
63    #[default]
64    HoughLines,
65    /// Projection profile method
66    ProjectionProfile,
67    /// Text line detection
68    TextLineDetection,
69    /// Combined (average of multiple methods)
70    Combined,
71    /// Page edge detection (for scanned book pages)
72    PageEdge,
73}
74
75/// Quality modes for rotation
76#[derive(Debug, Clone, Copy, Default)]
77pub enum QualityMode {
78    /// Fast (bilinear interpolation)
79    Fast,
80    /// Standard (bicubic interpolation)
81    #[default]
82    Standard,
83    /// High quality (Lanczos interpolation)
84    HighQuality,
85}
86
87/// Deskew detection options
88#[derive(Debug, Clone)]
89pub struct DeskewOptions {
90    /// Detection algorithm
91    pub algorithm: DeskewAlgorithm,
92    /// Maximum detection angle (degrees)
93    pub max_angle: f64,
94    /// Correction threshold (angles below this are ignored)
95    pub threshold_angle: f64,
96    /// Background color for filled areas after rotation
97    pub background_color: [u8; 3],
98    /// Quality mode for interpolation
99    pub quality_mode: QualityMode,
100}
101
102impl Default for DeskewOptions {
103    fn default() -> Self {
104        Self {
105            algorithm: DeskewAlgorithm::HoughLines,
106            max_angle: DEFAULT_MAX_ANGLE,
107            threshold_angle: DEFAULT_THRESHOLD_ANGLE,
108            background_color: DEFAULT_BACKGROUND_COLOR,
109            quality_mode: QualityMode::Standard,
110        }
111    }
112}
113
114impl DeskewOptions {
115    /// Create a new options builder
116    pub fn builder() -> DeskewOptionsBuilder {
117        DeskewOptionsBuilder::default()
118    }
119
120    /// Create options optimized for high quality output
121    pub fn high_quality() -> Self {
122        Self {
123            algorithm: DeskewAlgorithm::Combined,
124            quality_mode: QualityMode::HighQuality,
125            ..Default::default()
126        }
127    }
128
129    /// Create options optimized for fast processing
130    pub fn fast() -> Self {
131        Self {
132            algorithm: DeskewAlgorithm::ProjectionProfile,
133            quality_mode: QualityMode::Fast,
134            threshold_angle: 0.5, // Skip small corrections
135            ..Default::default()
136        }
137    }
138}
139
140/// Builder for DeskewOptions
141#[derive(Debug, Default)]
142pub struct DeskewOptionsBuilder {
143    options: DeskewOptions,
144}
145
146impl DeskewOptionsBuilder {
147    /// Set the detection algorithm
148    #[must_use]
149    pub fn algorithm(mut self, algorithm: DeskewAlgorithm) -> Self {
150        self.options.algorithm = algorithm;
151        self
152    }
153
154    /// Set the maximum detection angle
155    #[must_use]
156    pub fn max_angle(mut self, angle: f64) -> Self {
157        self.options.max_angle = angle.abs();
158        self
159    }
160
161    /// Set the correction threshold angle
162    #[must_use]
163    pub fn threshold_angle(mut self, angle: f64) -> Self {
164        self.options.threshold_angle = angle.abs();
165        self
166    }
167
168    /// Set the background color for rotated areas
169    #[must_use]
170    pub fn background_color(mut self, color: [u8; 3]) -> Self {
171        self.options.background_color = color;
172        self
173    }
174
175    /// Set the quality mode
176    #[must_use]
177    pub fn quality_mode(mut self, mode: QualityMode) -> Self {
178        self.options.quality_mode = mode;
179        self
180    }
181
182    /// Build the options
183    #[must_use]
184    pub fn build(self) -> DeskewOptions {
185        self.options
186    }
187}
188
189// ============================================================
190// Result Types
191// ============================================================
192
193/// Skew detection result
194#[derive(Debug, Clone)]
195pub struct SkewDetection {
196    /// Detected angle in degrees (positive = clockwise)
197    pub angle: f64,
198    /// Detection confidence (0.0 - 1.0)
199    pub confidence: f64,
200    /// Number of features used for detection
201    pub feature_count: usize,
202}
203
204/// Deskew operation result
205#[derive(Debug)]
206pub struct DeskewResult {
207    /// Original detection result
208    pub detection: SkewDetection,
209    /// Whether correction was applied
210    pub corrected: bool,
211    /// Output image path
212    pub output_path: PathBuf,
213    /// Original image size
214    pub original_size: (u32, u32),
215    /// Corrected image size
216    pub corrected_size: (u32, u32),
217}
218
219// ============================================================
220// Deskewer Trait
221// ============================================================
222
223/// Deskewer trait
224pub trait Deskewer {
225    /// Detect skew angle
226    fn detect_skew(image_path: &Path, options: &DeskewOptions) -> Result<SkewDetection>;
227
228    /// Correct skew
229    fn correct_skew(
230        input_path: &Path,
231        output_path: &Path,
232        options: &DeskewOptions,
233    ) -> Result<DeskewResult>;
234
235    /// Detect and correct in one operation
236    fn deskew(
237        input_path: &Path,
238        output_path: &Path,
239        options: &DeskewOptions,
240    ) -> Result<DeskewResult>;
241
242    /// Batch processing
243    fn deskew_batch(
244        images: &[(PathBuf, PathBuf)],
245        options: &DeskewOptions,
246    ) -> Vec<Result<DeskewResult>>;
247}
248
249#[cfg(test)]
250mod tests {
251    use super::*;
252
253    #[test]
254    fn test_deskew_options_default() {
255        let opts = DeskewOptions::default();
256        assert_eq!(opts.max_angle, 15.0);
257        assert_eq!(opts.threshold_angle, 0.1);
258        assert_eq!(opts.background_color, [255, 255, 255]);
259        assert!(matches!(opts.algorithm, DeskewAlgorithm::HoughLines));
260        assert!(matches!(opts.quality_mode, QualityMode::Standard));
261    }
262
263    #[test]
264    fn test_deskew_options_high_quality() {
265        let opts = DeskewOptions::high_quality();
266        assert!(matches!(opts.algorithm, DeskewAlgorithm::Combined));
267        assert!(matches!(opts.quality_mode, QualityMode::HighQuality));
268    }
269
270    #[test]
271    fn test_deskew_options_fast() {
272        let opts = DeskewOptions::fast();
273        assert!(matches!(opts.algorithm, DeskewAlgorithm::ProjectionProfile));
274        assert!(matches!(opts.quality_mode, QualityMode::Fast));
275        assert_eq!(opts.threshold_angle, 0.5);
276    }
277
278    #[test]
279    fn test_deskew_options_builder() {
280        let opts = DeskewOptions::builder()
281            .algorithm(DeskewAlgorithm::TextLineDetection)
282            .max_angle(20.0)
283            .threshold_angle(0.3)
284            .background_color([0, 0, 0])
285            .quality_mode(QualityMode::HighQuality)
286            .build();
287
288        assert!(matches!(opts.algorithm, DeskewAlgorithm::TextLineDetection));
289        assert_eq!(opts.max_angle, 20.0);
290        assert_eq!(opts.threshold_angle, 0.3);
291        assert_eq!(opts.background_color, [0, 0, 0]);
292        assert!(matches!(opts.quality_mode, QualityMode::HighQuality));
293    }
294
295    #[test]
296    fn test_builder_abs_angle() {
297        let opts = DeskewOptions::builder().max_angle(-10.0).build();
298        assert_eq!(opts.max_angle, 10.0);
299
300        let opts = DeskewOptions::builder().threshold_angle(-0.5).build();
301        assert_eq!(opts.threshold_angle, 0.5);
302    }
303
304    #[test]
305    fn test_skew_detection() {
306        let detection = SkewDetection {
307            angle: 2.5,
308            confidence: 0.95,
309            feature_count: 150,
310        };
311        assert_eq!(detection.angle, 2.5);
312        assert_eq!(detection.confidence, 0.95);
313        assert_eq!(detection.feature_count, 150);
314    }
315
316    #[test]
317    fn test_algorithm_variants() {
318        let algorithms = [
319            DeskewAlgorithm::HoughLines,
320            DeskewAlgorithm::ProjectionProfile,
321            DeskewAlgorithm::TextLineDetection,
322            DeskewAlgorithm::Combined,
323        ];
324        for alg in algorithms {
325            let _copy = alg;
326        }
327    }
328
329    #[test]
330    fn test_quality_mode_variants() {
331        let modes = [
332            QualityMode::Fast,
333            QualityMode::Standard,
334            QualityMode::HighQuality,
335        ];
336        for mode in modes {
337            let _copy = mode;
338        }
339    }
340
341    #[test]
342    fn test_error_types() {
343        let _err1 = DeskewError::ImageNotFound(PathBuf::from("/test"));
344        let _err2 = DeskewError::InvalidFormat("bad".to_string());
345        let _err3 = DeskewError::DetectionFailed("fail".to_string());
346        let _err4 = DeskewError::CorrectionFailed("fail".to_string());
347        let _err5: DeskewError = std::io::Error::other("test").into();
348    }
349}