glint-mask-tools 0.1.1

Rust implementation of glint mask generation tools for UAV and aerial imagery
Documentation
/// Utility functions for the glint mask generation library.
///
/// This module provides various utility functions used throughout
/// the library for common operations.
use crate::error::{GlintError, Result};
use std::path::{Path, PathBuf};

/// Create a circular kernel for morphological operations
///
/// Returns a 2D boolean array representing a circular kernel of the given radius.
/// Used for pixel buffering and other morphological operations.
pub fn create_circular_kernel(radius: usize) -> ndarray::Array2<bool> {
    let size = 2 * radius + 1;
    let mut kernel = ndarray::Array2::from_elem((size, size), false);
    let center = radius as i32;

    for y in 0..size {
        for x in 0..size {
            let dy = y as i32 - center;
            let dx = x as i32 - center;
            let distance_sq = dx * dx + dy * dy;

            if distance_sq <= (radius as i32) * (radius as i32) {
                kernel[[y, x]] = true;
            }
        }
    }

    kernel
}

/// List all image files in a directory with the given extensions
///
/// Recursively searches the directory for files with any of the specified
/// extensions (case-insensitive). Returns a sorted list of file paths.
pub fn list_image_files(dir: &Path, extensions: &[String]) -> Result<Vec<PathBuf>> {
    if !dir.exists() {
        return Err(GlintError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("Directory does not exist: {}", dir.display()),
        )));
    }

    if !dir.is_dir() {
        return Err(GlintError::Io(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Path is not a directory: {}", dir.display()),
        )));
    }

    let mut files = Vec::new();
    let extensions_lower: Vec<String> = extensions.iter().map(|e| e.to_lowercase()).collect();

    fn visit_dir(dir: &Path, extensions: &[String], files: &mut Vec<PathBuf>) -> Result<()> {
        for entry in std::fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_dir() {
                // Recursively visit subdirectories
                visit_dir(&path, extensions, files)?;
            } else if path.is_file() {
                if let Some(ext) = path.extension() {
                    let ext_str = ext.to_string_lossy().to_lowercase();
                    if extensions.iter().any(|e| *e == ext_str) {
                        files.push(path);
                    }
                }
            }
        }
        Ok(())
    }

    visit_dir(dir, &extensions_lower, &mut files)?;

    // Sort files for consistent ordering
    files.sort();

    Ok(files)
}

/// Normalize image values from arbitrary bit depth to [0, 1] range
///
/// # Arguments
///
/// * `value` - The value to normalize
/// * `bit_depth` - The bit depth of the source data (8, 16, or 32)
///
/// # Returns
///
/// The normalized value in the range [0, 1]
pub fn normalize_value(value: f64, bit_depth: u8) -> Result<f64> {
    let max_value = match bit_depth {
        8 => 255.0,
        16 => 65535.0,
        32 => 4294967295.0,
        _ => return Err(GlintError::InvalidBitDepth { bit_depth }),
    };

    Ok(value / max_value)
}

/// Get the maximum value for a given bit depth
pub fn max_value_for_bit_depth(bit_depth: u8) -> Result<f64> {
    match bit_depth {
        8 => Ok(255.0),
        16 => Ok(65535.0),
        32 => Ok(4294967295.0),
        _ => Err(GlintError::InvalidBitDepth { bit_depth }),
    }
}

/// Validate that a path exists and is accessible
pub fn validate_path_exists(path: &Path, path_type: &str) -> Result<()> {
    if !path.exists() {
        return Err(GlintError::Io(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("{} does not exist: {}", path_type, path.display()),
        )));
    }
    Ok(())
}

/// Validate that a directory exists and is accessible
pub fn validate_directory(path: &Path) -> Result<()> {
    validate_path_exists(path, "Directory")?;

    if !path.is_dir() {
        return Err(GlintError::Io(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Path is not a directory: {}", path.display()),
        )));
    }

    Ok(())
}

/// Create a directory if it doesn't exist
pub fn ensure_directory_exists(path: &Path) -> Result<()> {
    if !path.exists() {
        std::fs::create_dir_all(path)?;
    } else if !path.is_dir() {
        return Err(GlintError::Io(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            format!("Path exists but is not a directory: {}", path.display()),
        )));
    }

    Ok(())
}

/// Extract the stem (filename without extension) from a path
pub fn get_file_stem(path: &Path) -> Result<String> {
    path.file_stem()
        .and_then(|s| s.to_str())
        .map(|s| s.to_string())
        .ok_or_else(|| {
            GlintError::processing(format!(
                "Could not extract file stem from path: {}",
                path.display()
            ))
        })
}

/// Check if a file has one of the specified extensions (case-insensitive)
pub fn has_extension(path: &Path, extensions: &[String]) -> bool {
    if let Some(ext) = path.extension() {
        let ext_str = ext.to_string_lossy().to_lowercase();
        extensions.iter().any(|e| e.to_lowercase() == ext_str)
    } else {
        false
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::tempdir;

    #[test]
    fn test_create_circular_kernel() {
        let kernel = create_circular_kernel(1);
        assert_eq!(kernel.dim(), (3, 3));

        // Center should be true
        assert!(kernel[[1, 1]]);

        // Adjacent pixels should be true
        assert!(kernel[[0, 1]]);
        assert!(kernel[[1, 0]]);
        assert!(kernel[[2, 1]]);
        assert!(kernel[[1, 2]]);

        // Corners should be false for radius 1
        assert!(!kernel[[0, 0]]);
        assert!(!kernel[[2, 2]]);

        let kernel_0 = create_circular_kernel(0);
        assert_eq!(kernel_0.dim(), (1, 1));
        assert!(kernel_0[[0, 0]]);
    }

    #[test]
    fn test_normalize_value() {
        assert_eq!(normalize_value(255.0, 8).unwrap(), 1.0);
        assert_eq!(normalize_value(127.5, 8).unwrap(), 0.5);
        assert_eq!(normalize_value(0.0, 8).unwrap(), 0.0);

        assert_eq!(normalize_value(65535.0, 16).unwrap(), 1.0);
        assert_eq!(normalize_value(32767.5, 16).unwrap(), 0.5);

        assert!(normalize_value(100.0, 7).is_err()); // Invalid bit depth
    }

    #[test]
    fn test_max_value_for_bit_depth() {
        assert_eq!(max_value_for_bit_depth(8).unwrap(), 255.0);
        assert_eq!(max_value_for_bit_depth(16).unwrap(), 65535.0);
        assert_eq!(max_value_for_bit_depth(32).unwrap(), 4294967295.0);
        assert!(max_value_for_bit_depth(7).is_err());
    }

    #[test]
    fn test_list_image_files() {
        let temp_dir = tempdir().unwrap();
        let dir_path = temp_dir.path();

        // Create test files
        fs::write(dir_path.join("image1.jpg"), b"fake image").unwrap();
        fs::write(dir_path.join("image2.PNG"), b"fake image").unwrap();
        fs::write(dir_path.join("document.txt"), b"text").unwrap();

        // Create subdirectory with more files
        let sub_dir = dir_path.join("subdir");
        fs::create_dir(&sub_dir).unwrap();
        fs::write(sub_dir.join("image3.tiff"), b"fake image").unwrap();

        let extensions = vec!["jpg".to_string(), "png".to_string(), "tiff".to_string()];
        let files = list_image_files(dir_path, &extensions).unwrap();

        assert_eq!(files.len(), 3);
        assert!(files.iter().any(|p| p.file_name().unwrap() == "image1.jpg"));
        assert!(files.iter().any(|p| p.file_name().unwrap() == "image2.PNG"));
        assert!(files
            .iter()
            .any(|p| p.file_name().unwrap() == "image3.tiff"));
        assert!(!files
            .iter()
            .any(|p| p.file_name().unwrap() == "document.txt"));
    }

    #[test]
    fn test_validate_directory() {
        let temp_dir = tempdir().unwrap();
        let dir_path = temp_dir.path();
        let file_path = dir_path.join("file.txt");
        fs::write(&file_path, b"content").unwrap();

        // Valid directory should pass
        assert!(validate_directory(dir_path).is_ok());

        // File should fail
        assert!(validate_directory(&file_path).is_err());

        // Non-existent path should fail
        assert!(validate_directory(&dir_path.join("nonexistent")).is_err());
    }

    #[test]
    fn test_ensure_directory_exists() {
        let temp_dir = tempdir().unwrap();
        let new_dir = temp_dir.path().join("new_directory");

        // Directory doesn't exist initially
        assert!(!new_dir.exists());

        // Create it
        ensure_directory_exists(&new_dir).unwrap();
        assert!(new_dir.exists());
        assert!(new_dir.is_dir());

        // Calling again should be fine
        ensure_directory_exists(&new_dir).unwrap();
    }

    #[test]
    fn test_get_file_stem() {
        let path = Path::new("image.jpg");
        assert_eq!(get_file_stem(path).unwrap(), "image");

        let path = Path::new("/path/to/image.with.dots.png");
        assert_eq!(get_file_stem(path).unwrap(), "image.with.dots");

        let path = Path::new("no_extension");
        assert_eq!(get_file_stem(path).unwrap(), "no_extension");
    }

    #[test]
    fn test_has_extension() {
        let extensions = vec!["jpg".to_string(), "png".to_string()];

        assert!(has_extension(Path::new("image.jpg"), &extensions));
        assert!(has_extension(Path::new("image.JPG"), &extensions)); // Case insensitive
        assert!(has_extension(Path::new("image.PNG"), &extensions));
        assert!(!has_extension(Path::new("image.tiff"), &extensions));
        assert!(!has_extension(Path::new("no_extension"), &extensions));
    }
}