1use std::path::{Path, PathBuf};
6use thiserror::Error;
7
8pub const DEFAULT_MAX_ANGLE: f64 = 15.0;
14
15pub const DEFAULT_THRESHOLD_ANGLE: f64 = 0.1;
17
18pub const DEFAULT_BACKGROUND_COLOR: [u8; 3] = [255, 255, 255];
20
21pub const GRAYSCALE_THRESHOLD: u8 = 128;
23
24pub const WHITE_PIXEL: u8 = 255;
26
27pub const ALPHA_OPAQUE: u8 = 255;
29
30#[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#[derive(Debug, Clone, Copy, Default)]
61pub enum DeskewAlgorithm {
62 #[default]
64 HoughLines,
65 ProjectionProfile,
67 TextLineDetection,
69 Combined,
71 PageEdge,
73}
74
75#[derive(Debug, Clone, Copy, Default)]
77pub enum QualityMode {
78 Fast,
80 #[default]
82 Standard,
83 HighQuality,
85}
86
87#[derive(Debug, Clone)]
89pub struct DeskewOptions {
90 pub algorithm: DeskewAlgorithm,
92 pub max_angle: f64,
94 pub threshold_angle: f64,
96 pub background_color: [u8; 3],
98 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 pub fn builder() -> DeskewOptionsBuilder {
117 DeskewOptionsBuilder::default()
118 }
119
120 pub fn high_quality() -> Self {
122 Self {
123 algorithm: DeskewAlgorithm::Combined,
124 quality_mode: QualityMode::HighQuality,
125 ..Default::default()
126 }
127 }
128
129 pub fn fast() -> Self {
131 Self {
132 algorithm: DeskewAlgorithm::ProjectionProfile,
133 quality_mode: QualityMode::Fast,
134 threshold_angle: 0.5, ..Default::default()
136 }
137 }
138}
139
140#[derive(Debug, Default)]
142pub struct DeskewOptionsBuilder {
143 options: DeskewOptions,
144}
145
146impl DeskewOptionsBuilder {
147 #[must_use]
149 pub fn algorithm(mut self, algorithm: DeskewAlgorithm) -> Self {
150 self.options.algorithm = algorithm;
151 self
152 }
153
154 #[must_use]
156 pub fn max_angle(mut self, angle: f64) -> Self {
157 self.options.max_angle = angle.abs();
158 self
159 }
160
161 #[must_use]
163 pub fn threshold_angle(mut self, angle: f64) -> Self {
164 self.options.threshold_angle = angle.abs();
165 self
166 }
167
168 #[must_use]
170 pub fn background_color(mut self, color: [u8; 3]) -> Self {
171 self.options.background_color = color;
172 self
173 }
174
175 #[must_use]
177 pub fn quality_mode(mut self, mode: QualityMode) -> Self {
178 self.options.quality_mode = mode;
179 self
180 }
181
182 #[must_use]
184 pub fn build(self) -> DeskewOptions {
185 self.options
186 }
187}
188
189#[derive(Debug, Clone)]
195pub struct SkewDetection {
196 pub angle: f64,
198 pub confidence: f64,
200 pub feature_count: usize,
202}
203
204#[derive(Debug)]
206pub struct DeskewResult {
207 pub detection: SkewDetection,
209 pub corrected: bool,
211 pub output_path: PathBuf,
213 pub original_size: (u32, u32),
215 pub corrected_size: (u32, u32),
217}
218
219pub trait Deskewer {
225 fn detect_skew(image_path: &Path, options: &DeskewOptions) -> Result<SkewDetection>;
227
228 fn correct_skew(
230 input_path: &Path,
231 output_path: &Path,
232 options: &DeskewOptions,
233 ) -> Result<DeskewResult>;
234
235 fn deskew(
237 input_path: &Path,
238 output_path: &Path,
239 options: &DeskewOptions,
240 ) -> Result<DeskewResult>;
241
242 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}