Add image adjustments pipeline (brightness, contrast, crop, effects)
New AdjustmentsConfig with brightness, contrast, saturation, sharpen, grayscale, sepia, crop to aspect ratio, trim whitespace, and canvas padding. All wired from UI through to executor. - Saturation uses luminance-based color blending - Sepia uses standard matrix transformation - Crop calculates center crop from aspect ratio - Trim whitespace detects uniform border by corner pixel comparison - Canvas padding adds white border around image
This commit is contained in:
198
pixstrip-core/src/operations/adjustments.rs
Normal file
198
pixstrip-core/src/operations/adjustments.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use image::{DynamicImage, Rgba, RgbaImage};
|
||||
|
||||
use crate::error::Result;
|
||||
use super::AdjustmentsConfig;
|
||||
|
||||
pub fn apply_adjustments(
|
||||
mut img: DynamicImage,
|
||||
config: &AdjustmentsConfig,
|
||||
) -> Result<DynamicImage> {
|
||||
// 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());
|
||||
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, ih)
|
||||
} else {
|
||||
// Image is taller than target, crop height
|
||||
let new_h = (iw as f64 / target_ratio) as u32;
|
||||
(iw, new_h)
|
||||
};
|
||||
|
||||
let x = (iw - crop_w) / 2;
|
||||
let y = (ih - 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<u8>| -> 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) + 1;
|
||||
let crop_h = bottom.saturating_sub(top) + 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 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 + padding * 2;
|
||||
let new_h = h + padding * 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)
|
||||
}
|
||||
Reference in New Issue
Block a user