use image::DynamicImage;
use std::path::Path;
const MM_PER_INCH: f32 = 25.4;
const POINTS_PER_INCH: f64 = 72.0;
pub fn load_image<P: AsRef<Path>>(path: P) -> Result<DynamicImage, String> {
let path = path.as_ref();
if !path.exists() {
return Err(format!("Image not found: {}", path.display()));
}
image::open(path).map_err(|e| format!("Failed to load image: {}", e))
}
pub fn ensure_file_exists<P: AsRef<Path>>(path: P) -> Result<(), String> {
let path = path.as_ref();
if !path.exists() {
return Err(format!("File not found: {}", path.display()));
}
if !path.is_file() {
return Err(format!("Path is not a file: {}", path.display()));
}
Ok(())
}
pub fn ensure_dir_writable<P: AsRef<Path>>(path: P) -> Result<(), String> {
let path = path.as_ref();
if !path.exists() {
std::fs::create_dir_all(path).map_err(|e| format!("Failed to create directory: {}", e))?;
}
let test_file = path.join(".write_test");
std::fs::write(&test_file, b"test")
.map_err(|_| format!("Directory not writable: {}", path.display()))?;
let _ = std::fs::remove_file(test_file);
Ok(())
}
#[inline]
pub fn pixels_to_mm(pixels: u32, dpi: u32) -> f32 {
(pixels as f32 / dpi as f32) * MM_PER_INCH
}
#[inline]
pub fn mm_to_pixels(mm: f32, dpi: u32) -> u32 {
(mm * dpi as f32 / MM_PER_INCH) as u32
}
#[inline]
pub fn points_to_mm(points: f64) -> f32 {
(points / POINTS_PER_INCH * f64::from(MM_PER_INCH)) as f32
}
#[inline]
pub fn mm_to_points(mm: f32) -> f64 {
(f64::from(mm) / f64::from(MM_PER_INCH)) * POINTS_PER_INCH
}
pub fn format_file_size(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn format_duration(duration: std::time::Duration) -> String {
let secs = duration.as_secs();
let millis = duration.subsec_millis();
if secs >= 3600 {
let hours = secs / 3600;
let mins = (secs % 3600) / 60;
format!("{}h {}m", hours, mins)
} else if secs >= 60 {
let mins = secs / 60;
let remaining_secs = secs % 60;
format!("{}m {}s", mins, remaining_secs)
} else if secs > 0 {
format!("{}.{:03}s", secs, millis)
} else {
format!("{}ms", millis)
}
}
#[inline]
pub fn clamp<T: PartialOrd>(value: T, min: T, max: T) -> T {
if value < min {
min
} else if value > max {
max
} else {
value
}
}
#[inline]
pub fn percentage(current: usize, total: usize) -> f32 {
if total == 0 {
0.0
} else {
(current as f32 / total as f32) * 100.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_pixels_to_mm() {
let mm = pixels_to_mm(72, 72);
assert!((mm - 25.4).abs() < 0.01);
let mm = pixels_to_mm(300, 300);
assert!((mm - 25.4).abs() < 0.01);
}
#[test]
fn test_mm_to_pixels() {
let px = mm_to_pixels(25.4, 72);
assert_eq!(px, 72);
let px = mm_to_pixels(25.4, 300);
assert_eq!(px, 300);
}
#[test]
fn test_points_to_mm() {
let mm = points_to_mm(72.0);
assert!((mm - 25.4).abs() < 0.01);
}
#[test]
fn test_mm_to_points() {
let pt = mm_to_points(25.4);
assert!((pt - 72.0).abs() < 0.01);
}
#[test]
fn test_ensure_file_exists_nonexistent() {
let result = ensure_file_exists("/nonexistent/file.txt");
assert!(result.is_err());
}
#[test]
fn test_load_image_nonexistent() {
let result = load_image("/nonexistent/image.png");
assert!(result.is_err());
}
#[test]
fn test_format_file_size() {
assert_eq!(format_file_size(0), "0 B");
assert_eq!(format_file_size(500), "500 B");
assert_eq!(format_file_size(1024), "1.00 KB");
assert_eq!(format_file_size(1536), "1.50 KB");
assert_eq!(format_file_size(1_048_576), "1.00 MB");
assert_eq!(format_file_size(1_073_741_824), "1.00 GB");
}
#[test]
fn test_format_duration() {
use std::time::Duration;
assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
assert_eq!(format_duration(Duration::from_secs(5)), "5.000s");
assert_eq!(format_duration(Duration::from_secs(65)), "1m 5s");
assert_eq!(format_duration(Duration::from_secs(3665)), "1h 1m");
}
#[test]
fn test_clamp() {
assert_eq!(clamp(5, 0, 10), 5);
assert_eq!(clamp(-5, 0, 10), 0);
assert_eq!(clamp(15, 0, 10), 10);
assert_eq!(clamp(0.5f32, 0.0, 1.0), 0.5);
}
#[test]
fn test_percentage() {
assert_eq!(percentage(50, 100), 50.0);
assert_eq!(percentage(0, 100), 0.0);
assert_eq!(percentage(100, 100), 100.0);
assert_eq!(percentage(0, 0), 0.0); }
#[test]
fn test_ensure_dir_writable() {
let temp_dir = tempfile::tempdir().unwrap();
let result = ensure_dir_writable(temp_dir.path());
assert!(result.is_ok());
}
#[test]
fn test_ensure_dir_writable_creates_dir() {
let temp_dir = tempfile::tempdir().unwrap();
let new_dir = temp_dir.path().join("new_subdir");
let result = ensure_dir_writable(&new_dir);
assert!(result.is_ok());
assert!(new_dir.exists());
}
#[test]
fn test_roundtrip_mm_pixels() {
let original_mm = 100.0f32;
let dpi = 300u32;
let pixels = mm_to_pixels(original_mm, dpi);
let back_to_mm = pixels_to_mm(pixels, dpi);
assert!((original_mm - back_to_mm).abs() < 0.1);
}
#[test]
fn test_roundtrip_mm_points() {
let original_mm = 100.0f32;
let points = mm_to_points(original_mm);
let back_to_mm = points_to_mm(points);
assert!((original_mm - back_to_mm).abs() < 0.1);
}
#[test]
fn test_format_file_size_edge_cases() {
assert_eq!(format_file_size(1023), "1023 B");
assert_eq!(format_file_size(1024 * 1024 - 1), "1024.00 KB");
}
#[test]
fn test_clamp_boundary_values() {
assert_eq!(clamp(0, 0, 10), 0);
assert_eq!(clamp(10, 0, 10), 10);
assert_eq!(clamp(5, 5, 5), 5);
}
#[test]
fn test_pixels_to_mm_high_dpi() {
let mm = pixels_to_mm(600, 600);
assert!((mm - 25.4).abs() < 0.01);
}
#[test]
fn test_pixels_to_mm_zero_pixels() {
let mm = pixels_to_mm(0, 300);
assert_eq!(mm, 0.0);
}
#[test]
fn test_pixels_to_mm_large_value() {
let mm = pixels_to_mm(7680, 300);
assert!((mm - 650.24).abs() < 0.1);
}
#[test]
fn test_mm_to_pixels_zero_mm() {
let px = mm_to_pixels(0.0, 300);
assert_eq!(px, 0);
}
#[test]
fn test_mm_to_pixels_high_dpi() {
let px = mm_to_pixels(25.4, 600);
assert_eq!(px, 600);
}
#[test]
fn test_mm_to_pixels_a4_width() {
let px = mm_to_pixels(210.0, 300);
assert_eq!(px, 2480);
}
#[test]
fn test_points_to_mm_zero() {
let mm = points_to_mm(0.0);
assert_eq!(mm, 0.0);
}
#[test]
fn test_points_to_mm_a4_height() {
let mm = points_to_mm(841.89);
assert!((mm - 297.0).abs() < 0.1);
}
#[test]
fn test_mm_to_points_zero() {
let pt = mm_to_points(0.0);
assert_eq!(pt, 0.0);
}
#[test]
fn test_mm_to_points_a4_width() {
let pt = mm_to_points(210.0);
assert!((pt - 595.28).abs() < 0.1);
}
#[test]
fn test_format_file_size_large_gb() {
let size = 5 * 1024 * 1024 * 1024u64; assert_eq!(format_file_size(size), "5.00 GB");
}
#[test]
fn test_format_file_size_fractional_mb() {
let size = (2.5 * 1024.0 * 1024.0) as u64;
assert_eq!(format_file_size(size), "2.50 MB");
}
#[test]
fn test_format_duration_zero() {
use std::time::Duration;
assert_eq!(format_duration(Duration::from_millis(0)), "0ms");
}
#[test]
fn test_format_duration_exact_minute() {
use std::time::Duration;
assert_eq!(format_duration(Duration::from_secs(60)), "1m 0s");
}
#[test]
fn test_format_duration_exact_hour() {
use std::time::Duration;
assert_eq!(format_duration(Duration::from_secs(3600)), "1h 0m");
}
#[test]
fn test_format_duration_multiple_hours() {
use std::time::Duration;
assert_eq!(
format_duration(Duration::from_secs(2 * 3600 + 30 * 60 + 45)),
"2h 30m"
);
}
#[test]
fn test_format_duration_with_millis() {
use std::time::Duration;
assert_eq!(format_duration(Duration::from_millis(1500)), "1.500s");
}
#[test]
fn test_clamp_negative_range() {
assert_eq!(clamp(-5, -10, -1), -5);
assert_eq!(clamp(-15, -10, -1), -10);
assert_eq!(clamp(0, -10, -1), -1);
}
#[test]
fn test_clamp_float_precision() {
assert!((clamp(0.5f64, 0.0, 1.0) - 0.5).abs() < f64::EPSILON);
assert!((clamp(-0.5f64, 0.0, 1.0) - 0.0).abs() < f64::EPSILON);
assert!((clamp(1.5f64, 0.0, 1.0) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_percentage_small_values() {
let p = percentage(1, 1000);
assert!((p - 0.1).abs() < 0.001);
}
#[test]
fn test_percentage_over_100() {
let p = percentage(150, 100);
assert_eq!(p, 150.0);
}
#[test]
fn test_percentage_large_numbers() {
let p = percentage(500_000, 1_000_000);
assert_eq!(p, 50.0);
}
#[test]
fn test_ensure_file_exists_is_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let result = ensure_file_exists(temp_dir.path());
assert!(result.is_err());
assert!(result.unwrap_err().contains("not a file"));
}
#[test]
fn test_ensure_file_exists_valid_file() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, b"test").unwrap();
let result = ensure_file_exists(&file_path);
assert!(result.is_ok());
}
#[test]
fn test_load_image_invalid_format() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("invalid.png");
std::fs::write(&file_path, b"not an image").unwrap();
let result = load_image(&file_path);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Failed to load image"));
}
#[test]
fn test_ensure_dir_writable_nested() {
let temp_dir = tempfile::tempdir().unwrap();
let nested_dir = temp_dir.path().join("a").join("b").join("c");
let result = ensure_dir_writable(&nested_dir);
assert!(result.is_ok());
assert!(nested_dir.exists());
}
#[test]
fn test_conversion_consistency() {
let dpi = 300u32;
let inch_in_mm = 25.4f32;
let inch_in_points = 72.0f64;
let inch_in_pixels = dpi;
let pixels_from_mm = mm_to_pixels(inch_in_mm, dpi);
assert_eq!(pixels_from_mm, inch_in_pixels);
let mm_from_points = points_to_mm(inch_in_points);
assert!((mm_from_points - inch_in_mm).abs() < 0.01);
}
#[test]
fn test_format_file_size_boundary_kb_mb() {
let exactly_mb = 1024u64 * 1024;
assert_eq!(format_file_size(exactly_mb), "1.00 MB");
assert_eq!(format_file_size(exactly_mb - 1), "1024.00 KB");
}
#[test]
fn test_format_file_size_boundary_mb_gb() {
let exactly_gb = 1024u64 * 1024 * 1024;
assert_eq!(format_file_size(exactly_gb), "1.00 GB");
assert_eq!(format_file_size(exactly_gb - 1), "1024.00 MB");
}
#[test]
fn test_format_file_size_thread_safe() {
use std::thread;
let handles: Vec<_> = (0..8)
.map(|i| {
thread::spawn(move || {
let size = 1024u64.pow(i % 4); format_file_size(size)
})
})
.collect();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
assert_eq!(results.len(), 8);
}
#[test]
fn test_format_duration_thread_safe() {
use std::thread;
use std::time::Duration;
let handles: Vec<_> = (0..8)
.map(|i| {
thread::spawn(move || {
let duration = Duration::from_secs(i * 100);
format_duration(duration)
})
})
.collect();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
assert_eq!(results.len(), 8);
}
#[test]
fn test_conversion_functions_thread_safe() {
use std::thread;
let handles: Vec<_> = (0..8)
.map(|i| {
thread::spawn(move || {
let dpi = 150 + (i as u32 * 50);
let px = mm_to_pixels(100.0, dpi);
let mm = pixels_to_mm(px, dpi);
(px, mm)
})
})
.collect();
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
assert_eq!(results.len(), 8);
for (_, mm) in &results {
assert!((*mm - 100.0).abs() < 1.0);
}
}
#[test]
fn test_clamp_thread_safe() {
use rayon::prelude::*;
let results: Vec<_> = (0..1000)
.into_par_iter()
.map(|i| clamp(i, 100, 900))
.collect();
assert_eq!(results.len(), 1000);
for (i, &val) in results.iter().enumerate() {
if i < 100 {
assert_eq!(val, 100);
} else if i > 900 {
assert_eq!(val, 900);
} else {
assert_eq!(val, i as i32);
}
}
}
#[test]
fn test_percentage_thread_safe() {
use rayon::prelude::*;
let results: Vec<_> = (0..=100)
.into_par_iter()
.map(|i| percentage(i, 100))
.collect();
assert_eq!(results.len(), 101);
for (i, &pct) in results.iter().enumerate() {
assert!(
(pct - i as f32).abs() < 0.001,
"Expected {} but got {}",
i,
pct
);
}
}
#[test]
fn test_pixels_to_mm_one_pixel() {
let mm_72 = pixels_to_mm(1, 72);
let mm_300 = pixels_to_mm(1, 300);
let mm_600 = pixels_to_mm(1, 600);
assert!(mm_72 > mm_300);
assert!(mm_300 > mm_600);
}
#[test]
fn test_pixels_to_mm_max_dpi() {
let mm = pixels_to_mm(2400, 2400);
assert!((mm - 25.4).abs() < 0.01);
}
#[test]
fn test_mm_to_pixels_fractional() {
let px = mm_to_pixels(0.1, 300);
assert_eq!(px, 1);
}
#[test]
fn test_points_to_mm_large_value() {
let mm = points_to_mm(2834.65); assert!((mm - 1000.0).abs() < 1.0);
}
#[test]
fn test_mm_to_points_small_value() {
let pt = mm_to_points(0.1);
assert!((pt - 0.283).abs() < 0.01);
}
#[test]
fn test_format_file_size_max_u64() {
let size = u64::MAX;
let result = format_file_size(size);
assert!(result.contains("GB"));
}
#[test]
fn test_format_duration_max_hours() {
use std::time::Duration;
let duration = Duration::from_secs(999 * 3600 + 59 * 60);
let result = format_duration(duration);
assert_eq!(result, "999h 59m");
}
#[test]
fn test_format_duration_subsec_only() {
use std::time::Duration;
let duration = Duration::from_millis(999);
assert_eq!(format_duration(duration), "999ms");
}
#[test]
fn test_clamp_with_u8() {
assert_eq!(clamp(0u8, 10, 200), 10);
assert_eq!(clamp(255u8, 10, 200), 200);
assert_eq!(clamp(128u8, 10, 200), 128);
}
#[test]
fn test_clamp_with_i64() {
assert_eq!(clamp(i64::MIN, -100, 100), -100);
assert_eq!(clamp(i64::MAX, -100, 100), 100);
assert_eq!(clamp(0i64, -100, 100), 0);
}
#[test]
fn test_percentage_boundary_values() {
assert_eq!(percentage(0, 0), 0.0);
assert_eq!(percentage(100, 0), 0.0);
assert_eq!(percentage(usize::MAX / 2, usize::MAX / 2), 100.0);
}
#[test]
fn test_ensure_dir_writable_existing_file() {
let temp_dir = tempfile::tempdir().unwrap();
let file_path = temp_dir.path().join("file.txt");
std::fs::write(&file_path, b"test").unwrap();
let result = ensure_dir_writable(&file_path);
assert!(result.is_err());
}
#[test]
fn test_load_image_error_message_contains_path() {
let path = "/nonexistent/path/to/image.png";
let result = load_image(path);
assert!(result.is_err());
assert!(result.unwrap_err().contains(path));
}
#[test]
fn test_ensure_file_exists_error_message_contains_path() {
let path = "/nonexistent/path/to/file.txt";
let result = ensure_file_exists(path);
assert!(result.is_err());
assert!(result.unwrap_err().contains(path));
}
#[test]
fn test_pixels_mm_consistency_across_dpis() {
let dpis = [72, 96, 150, 300, 600, 1200];
for dpi in dpis {
let mm = pixels_to_mm(dpi, dpi);
assert!(
(mm - 25.4).abs() < 0.01,
"Failed for DPI {}: expected 25.4, got {}",
dpi,
mm
);
}
}
#[test]
fn test_points_mm_bidirectional() {
let mm_values = [0.0, 1.0, 10.0, 25.4, 100.0, 297.0, 1000.0];
for &mm in &mm_values {
let points = mm_to_points(mm);
let back = points_to_mm(points);
assert!(
(mm - back).abs() < 0.01,
"Roundtrip failed for mm={}: got {}",
mm,
back
);
}
}
}