use image::{DynamicImage, Rgba, RgbaImage}; use crate::error::Result; use super::AdjustmentsConfig; pub fn apply_adjustments( mut img: DynamicImage, config: &AdjustmentsConfig, ) -> Result { // Crop to aspect ratio (before other adjustments) if let Some((w_ratio, h_ratio)) = config.crop_aspect_ratio { img = crop_to_aspect_ratio(img, w_ratio, h_ratio); } // Trim whitespace if config.trim_whitespace { img = trim_whitespace(img); } // Brightness if config.brightness != 0 { img = img.brighten(config.brightness); } // Contrast if config.contrast != 0 { img = img.adjust_contrast(config.contrast as f32); } // Saturation if config.saturation != 0 { img = adjust_saturation(img, config.saturation); } // Grayscale if config.grayscale { img = img.grayscale(); } // Sepia if config.sepia { img = apply_sepia(img); } // Sharpen if config.sharpen { img = img.unsharpen(1.0, 5); } // Canvas padding if config.canvas_padding > 0 { img = add_canvas_padding(img, config.canvas_padding); } Ok(img) } fn crop_to_aspect_ratio(img: DynamicImage, w_ratio: f64, h_ratio: f64) -> DynamicImage { let (iw, ih) = (img.width(), img.height()); if !w_ratio.is_finite() || !h_ratio.is_finite() || w_ratio <= 0.0 || h_ratio <= 0.0 || iw == 0 || ih == 0 { return img; } let target_ratio = w_ratio / h_ratio; let current_ratio = iw as f64 / ih as f64; let (crop_w, crop_h) = if current_ratio > target_ratio { // Image is wider than target, crop width let new_w = (ih as f64 * target_ratio) as u32; (new_w.min(iw), ih) } else { // Image is taller than target, crop height let new_h = (iw as f64 / target_ratio) as u32; (iw, new_h.min(ih)) }; let x = iw.saturating_sub(crop_w) / 2; let y = ih.saturating_sub(crop_h) / 2; img.crop_imm(x, y, crop_w, crop_h) } fn trim_whitespace(img: DynamicImage) -> DynamicImage { let rgba = img.to_rgba8(); let (w, h) = (rgba.width(), rgba.height()); if w == 0 || h == 0 { return img; } // Get corner pixel as reference background color let bg = *rgba.get_pixel(0, 0); let threshold = 30u32; let is_bg = |p: &Rgba| -> bool { let dr = (p[0] as i32 - bg[0] as i32).unsigned_abs(); let dg = (p[1] as i32 - bg[1] as i32).unsigned_abs(); let db = (p[2] as i32 - bg[2] as i32).unsigned_abs(); dr + dg + db < threshold }; let mut top = 0u32; let mut bottom = h - 1; let mut left = 0u32; let mut right = w - 1; // Find top 'top: for y in 0..h { for x in 0..w { if !is_bg(rgba.get_pixel(x, y)) { top = y; break 'top; } } } // Find bottom 'bottom: for y in (0..h).rev() { for x in 0..w { if !is_bg(rgba.get_pixel(x, y)) { bottom = y; break 'bottom; } } } // Find left 'left: for x in 0..w { for y in top..=bottom { if !is_bg(rgba.get_pixel(x, y)) { left = x; break 'left; } } } // Find right 'right: for x in (0..w).rev() { for y in top..=bottom { if !is_bg(rgba.get_pixel(x, y)) { right = x; break 'right; } } } let crop_w = right.saturating_sub(left).saturating_add(1); let crop_h = bottom.saturating_sub(top).saturating_add(1); if crop_w == 0 || crop_h == 0 || (crop_w == w && crop_h == h) { return img; } img.crop_imm(left, top, crop_w, crop_h) } fn adjust_saturation(img: DynamicImage, amount: i32) -> DynamicImage { let amount = amount.clamp(-100, 100); let mut rgba = img.into_rgba8(); let factor = 1.0 + (amount as f64 / 100.0); for pixel in rgba.pixels_mut() { let r = pixel[0] as f64; let g = pixel[1] as f64; let b = pixel[2] as f64; let gray = 0.2126 * r + 0.7152 * g + 0.0722 * b; pixel[0] = (gray + (r - gray) * factor).clamp(0.0, 255.0) as u8; pixel[1] = (gray + (g - gray) * factor).clamp(0.0, 255.0) as u8; pixel[2] = (gray + (b - gray) * factor).clamp(0.0, 255.0) as u8; } DynamicImage::ImageRgba8(rgba) } fn apply_sepia(img: DynamicImage) -> DynamicImage { let mut rgba = img.into_rgba8(); for pixel in rgba.pixels_mut() { let r = pixel[0] as f64; let g = pixel[1] as f64; let b = pixel[2] as f64; pixel[0] = (0.393 * r + 0.769 * g + 0.189 * b).clamp(0.0, 255.0) as u8; pixel[1] = (0.349 * r + 0.686 * g + 0.168 * b).clamp(0.0, 255.0) as u8; pixel[2] = (0.272 * r + 0.534 * g + 0.131 * b).clamp(0.0, 255.0) as u8; } DynamicImage::ImageRgba8(rgba) } fn add_canvas_padding(img: DynamicImage, padding: u32) -> DynamicImage { let (w, h) = (img.width(), img.height()); let new_w = w.saturating_add(padding.saturating_mul(2)); let new_h = h.saturating_add(padding.saturating_mul(2)); let mut canvas = RgbaImage::from_pixel(new_w, new_h, Rgba([255, 255, 255, 255])); image::imageops::overlay(&mut canvas, &img.to_rgba8(), padding as i64, padding as i64); DynamicImage::ImageRgba8(canvas) }