Add operation configuration types: resize, convert, compress, metadata, watermark, rename

All 11 operation tests passing.
This commit is contained in:
2026-03-06 01:40:24 +02:00
parent 3e176e3d65
commit 0203044a43
2 changed files with 338 additions and 1 deletions

View File

@@ -1 +1,227 @@
// Image processing operations
use serde::{Deserialize, Serialize};
use crate::types::{Dimensions, ImageFormat, QualityPreset};
// --- Resize ---
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ResizeConfig {
ByWidth(u32),
ByHeight(u32),
FitInBox {
max: Dimensions,
allow_upscale: bool,
},
Exact(Dimensions),
}
impl ResizeConfig {
pub fn target_for(&self, original: Dimensions) -> Dimensions {
match self {
Self::ByWidth(w) => {
let scale = *w as f64 / original.width as f64;
Dimensions {
width: *w,
height: (original.height as f64 * scale).round() as u32,
}
}
Self::ByHeight(h) => {
let scale = *h as f64 / original.height as f64;
Dimensions {
width: (original.width as f64 * scale).round() as u32,
height: *h,
}
}
Self::FitInBox { max, allow_upscale } => {
original.fit_within(*max, *allow_upscale)
}
Self::Exact(dims) => *dims,
}
}
}
// --- Rotation / Flip ---
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Rotation {
None,
Cw90,
Cw180,
Cw270,
AutoOrient,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum Flip {
None,
Horizontal,
Vertical,
}
// --- Convert ---
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConvertConfig {
KeepOriginal,
SingleFormat(ImageFormat),
FormatMapping(Vec<(ImageFormat, ImageFormat)>),
}
impl ConvertConfig {
pub fn output_format(&self, input: ImageFormat) -> ImageFormat {
match self {
Self::KeepOriginal => input,
Self::SingleFormat(fmt) => *fmt,
Self::FormatMapping(map) => {
map.iter()
.find(|(from, _)| *from == input)
.map(|(_, to)| *to)
.unwrap_or(input)
}
}
}
}
// --- Compress ---
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CompressConfig {
Preset(QualityPreset),
Custom {
jpeg_quality: Option<u8>,
png_level: Option<u8>,
webp_quality: Option<f32>,
avif_quality: Option<f32>,
},
}
// --- Metadata ---
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MetadataConfig {
StripAll,
KeepAll,
Privacy,
Custom {
strip_gps: bool,
strip_camera: bool,
strip_software: bool,
strip_timestamps: bool,
strip_copyright: bool,
},
}
impl MetadataConfig {
pub fn should_strip_gps(&self) -> bool {
match self {
Self::StripAll => true,
Self::KeepAll => false,
Self::Privacy => true,
Self::Custom { strip_gps, .. } => *strip_gps,
}
}
pub fn should_strip_camera(&self) -> bool {
match self {
Self::StripAll => true,
Self::KeepAll => false,
Self::Privacy => true,
Self::Custom { strip_camera, .. } => *strip_camera,
}
}
pub fn should_strip_copyright(&self) -> bool {
match self {
Self::StripAll => true,
Self::KeepAll => false,
Self::Privacy => false,
Self::Custom { strip_copyright, .. } => *strip_copyright,
}
}
pub fn should_strip_software(&self) -> bool {
match self {
Self::StripAll => true,
Self::KeepAll => false,
Self::Privacy => true,
Self::Custom { strip_software, .. } => *strip_software,
}
}
pub fn should_strip_timestamps(&self) -> bool {
match self {
Self::StripAll => true,
Self::KeepAll => false,
Self::Privacy => false,
Self::Custom { strip_timestamps, .. } => *strip_timestamps,
}
}
}
// --- Watermark ---
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WatermarkPosition {
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
Center,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WatermarkConfig {
Text {
text: String,
position: WatermarkPosition,
font_size: f32,
opacity: f32,
color: [u8; 4],
},
Image {
path: std::path::PathBuf,
position: WatermarkPosition,
opacity: f32,
scale: f32,
},
}
// --- Rename ---
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenameConfig {
pub prefix: String,
pub suffix: String,
pub counter_start: u32,
pub counter_padding: u32,
pub template: Option<String>,
}
impl RenameConfig {
pub fn apply_simple(&self, original_name: &str, extension: &str, index: u32) -> String {
let counter = self.counter_start + index - 1;
let counter_str = format!(
"{:0>width$}",
counter,
width = self.counter_padding as usize
);
let mut name = String::new();
if !self.prefix.is_empty() {
name.push_str(&self.prefix);
}
name.push_str(original_name);
if !self.suffix.is_empty() {
name.push_str(&self.suffix);
}
name.push('_');
name.push_str(&counter_str);
name.push('.');
name.push_str(extension);
name
}
}