image_optimizer/file_ops/
image_scanner.rs

1use std::ffi::OsStr;
2use std::path::PathBuf;
3use walkdir::WalkDir;
4
5/// List of supported image file extensions for optimization.
6const SUPPORTED_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "webp", "svg"];
7
8/// Scans a directory or file for supported image formats.
9///
10/// This function discovers image files that can be processed by the optimizer.
11/// It supports both single file input and directory scanning with optional recursion.
12/// Only files with supported extensions (JPEG, PNG, WebP, SVG) are returned.
13///
14/// # Arguments
15///
16/// * `path` - Path to scan (can be a file or directory)
17/// * `recursive` - Whether to recursively scan subdirectories (ignored for single files)
18///
19/// # Returns
20///
21/// A vector of `PathBuf` containing all discovered image files with supported formats.
22/// Returns an empty vector if no supported images are found or if the path doesn't exist.
23///
24/// # Supported Formats
25///
26/// - **JPEG**: `.jpg`, `.jpeg` (case-insensitive)
27/// - **PNG**: `.png` (case-insensitive)  
28/// - **WebP**: `.webp` (case-insensitive)
29/// - **SVG**: `.svg` (case-insensitive)
30///
31/// # Examples
32///
33/// ```rust
34/// use std::path::Path;
35/// use image_optimizer::file_ops::scan_images;
36///
37/// // Scan a single file
38/// let images = scan_images(Path::new("photo.jpg"), false);
39///
40/// // Scan directory recursively
41/// let images = scan_images(Path::new("./photos"), true);
42///
43/// // Scan directory non-recursively
44/// let images = scan_images(Path::new("./photos"), false);
45/// ```
46pub fn scan_images(path: &std::path::Path, recursive: bool) -> Vec<PathBuf> {
47    let mut image_files = Vec::new();
48
49    if path.is_file() {
50        if let Some(extension) = path.extension().and_then(OsStr::to_str)
51            && SUPPORTED_EXTENSIONS.contains(&extension.to_lowercase().as_str())
52        {
53            image_files.push(path.to_path_buf());
54        }
55        return image_files;
56    }
57
58    let walker = if recursive {
59        WalkDir::new(path)
60    } else {
61        WalkDir::new(path).max_depth(1)
62    };
63
64    for entry in walker.into_iter().filter_map(Result::ok) {
65        if entry.file_type().is_file()
66            && let Some(extension) = entry.path().extension().and_then(OsStr::to_str)
67            && SUPPORTED_EXTENSIONS.contains(&extension.to_lowercase().as_str())
68        {
69            image_files.push(entry.path().to_path_buf());
70        }
71    }
72
73    image_files
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use std::fs;
80    use std::path::Path;
81
82    #[test]
83    fn test_supported_extensions() {
84        assert!(SUPPORTED_EXTENSIONS.contains(&"jpg"));
85        assert!(SUPPORTED_EXTENSIONS.contains(&"jpeg"));
86        assert!(SUPPORTED_EXTENSIONS.contains(&"png"));
87        assert!(SUPPORTED_EXTENSIONS.contains(&"webp"));
88        assert!(SUPPORTED_EXTENSIONS.contains(&"svg"));
89        assert!(!SUPPORTED_EXTENSIONS.contains(&"gif"));
90        assert!(!SUPPORTED_EXTENSIONS.contains(&"txt"));
91    }
92
93    #[test]
94    fn test_scan_single_file() {
95        let temp_dir = std::env::temp_dir();
96        let test_file = temp_dir.join("test.jpg");
97        fs::write(&test_file, "fake jpg content").unwrap();
98
99        let result = scan_images(&test_file, false);
100        assert_eq!(result.len(), 1);
101        assert_eq!(result[0], test_file);
102
103        fs::remove_file(&test_file).unwrap();
104    }
105
106    #[test]
107    fn test_scan_unsupported_file() {
108        let temp_dir = std::env::temp_dir();
109        let test_file = temp_dir.join("test.txt");
110        fs::write(&test_file, "text content").unwrap();
111
112        let result = scan_images(&test_file, false);
113        assert_eq!(result.len(), 0);
114
115        fs::remove_file(&test_file).unwrap();
116    }
117
118    #[test]
119    fn test_scan_nonexistent_path() {
120        let nonexistent = Path::new("/nonexistent/path");
121        let result = scan_images(nonexistent, false);
122        assert_eq!(result.len(), 0);
123    }
124
125    #[test]
126    fn test_case_insensitive_extensions() {
127        let temp_dir = std::env::temp_dir();
128        let test_files = [
129            ("test_upper.JPG", "jpg"),
130            ("test_upper.JPEG", "jpeg"),
131            ("test_upper.PNG", "png"),
132            ("test_upper.WEBP", "webp"),
133            ("test_upper.SVG", "svg"),
134        ];
135
136        for (filename, _) in &test_files {
137            let test_file = temp_dir.join(filename);
138            fs::write(&test_file, "fake content").unwrap();
139
140            let result = scan_images(&test_file, false);
141            assert_eq!(result.len(), 1, "Failed for file: {}", filename);
142            assert_eq!(result[0], test_file);
143
144            fs::remove_file(&test_file).unwrap();
145        }
146    }
147}