Wire resize algorithm selection, overwrite behavior, and fix rotation/flip scope

- Add ResizeAlgorithm enum (Lanczos3/CatmullRom/Bilinear/Nearest) to core
- Thread algorithm selection from UI ComboRow through ProcessingJob to resize_image
- Add OverwriteBehavior enum (AutoRename/Overwrite/Skip) to core
- Implement overwrite handling in executor with auto-rename suffix logic
- Wire overwrite behavior from output step through to processing job
- Fix rotation/flip to apply when resize step is enabled, not just adjustments
This commit is contained in:
2026-03-06 15:17:59 +02:00
parent 064194df3d
commit a29256921e
7 changed files with 123 additions and 6 deletions

View File

@@ -8,7 +8,7 @@ use crate::encoder::{EncoderOptions, OutputEncoder};
use crate::error::{PixstripError, Result};
use crate::loader::ImageLoader;
use crate::operations::adjustments::apply_adjustments;
use crate::operations::resize::resize_image;
use crate::operations::resize::resize_image_with_algorithm;
use crate::operations::watermark::apply_watermark;
use crate::operations::{Flip, Rotation};
use crate::pipeline::ProcessingJob;
@@ -344,7 +344,7 @@ impl PipelineExecutor {
// Resize
if let Some(ref config) = job.resize {
img = resize_image(&img, config)?;
img = resize_image_with_algorithm(&img, config, job.resize_algorithm)?;
}
// Adjustments (brightness, contrast, saturation, effects, crop, padding)
@@ -413,6 +413,25 @@ impl PipelineExecutor {
job.output_path_for(source, Some(output_format))
};
// Handle overwrite behavior
let output_path = match job.overwrite_behavior {
crate::operations::OverwriteBehavior::Skip => {
if output_path.exists() {
// Return 0 bytes written - file was skipped
return Ok((input_size, 0));
}
output_path
}
crate::operations::OverwriteBehavior::AutoRename => {
if output_path.exists() {
find_unique_path(&output_path)
} else {
output_path
}
}
crate::operations::OverwriteBehavior::Overwrite => output_path,
};
// Ensure output directory exists
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
@@ -451,6 +470,30 @@ fn num_cpus() -> usize {
.unwrap_or(1)
}
fn find_unique_path(path: &std::path::Path) -> std::path::PathBuf {
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("output");
let ext = path
.extension()
.and_then(|e| e.to_str())
.unwrap_or("bin");
let parent = path.parent().unwrap_or_else(|| std::path::Path::new("."));
for i in 1u32..10000 {
let candidate = parent.join(format!("{}_{}.{}", stem, i, ext));
if !candidate.exists() {
return candidate;
}
}
// Extremely unlikely fallback
parent.join(format!("{}_{}.{}", stem, std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0), ext))
}
fn copy_metadata_from_source(source: &std::path::Path, output: &std::path::Path) {
// Best-effort: try to copy EXIF from source to output using little_exif.
// If it fails (e.g. non-JPEG, no EXIF), silently continue.

View File

@@ -46,6 +46,22 @@ impl ResizeConfig {
}
}
// --- Resize Algorithm ---
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum ResizeAlgorithm {
Lanczos3,
CatmullRom,
Bilinear,
Nearest,
}
impl Default for ResizeAlgorithm {
fn default() -> Self {
Self::Lanczos3
}
}
// --- Rotation / Flip ---
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
@@ -225,6 +241,21 @@ impl AdjustmentsConfig {
}
}
// --- Overwrite Behavior ---
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum OverwriteBehavior {
AutoRename,
Overwrite,
Skip,
}
impl Default for OverwriteBehavior {
fn default() -> Self {
Self::AutoRename
}
}
// --- Rename ---
#[derive(Debug, Clone, Serialize, Deserialize)]

View File

@@ -3,11 +3,19 @@ use fast_image_resize::{images::Image, Resizer, ResizeOptions, ResizeAlg, Filter
use crate::error::{PixstripError, Result};
use crate::types::Dimensions;
use super::ResizeConfig;
use super::{ResizeConfig, ResizeAlgorithm};
pub fn resize_image(
src: &image::DynamicImage,
config: &ResizeConfig,
) -> Result<image::DynamicImage> {
resize_image_with_algorithm(src, config, ResizeAlgorithm::default())
}
pub fn resize_image_with_algorithm(
src: &image::DynamicImage,
config: &ResizeConfig,
algorithm: ResizeAlgorithm,
) -> Result<image::DynamicImage> {
let original = Dimensions {
width: src.width(),
@@ -40,7 +48,13 @@ pub fn resize_image(
);
let mut resizer = Resizer::new();
let options = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Lanczos3));
let alg = match algorithm {
ResizeAlgorithm::Lanczos3 => ResizeAlg::Convolution(FilterType::Lanczos3),
ResizeAlgorithm::CatmullRom => ResizeAlg::Convolution(FilterType::CatmullRom),
ResizeAlgorithm::Bilinear => ResizeAlg::Convolution(FilterType::Bilinear),
ResizeAlgorithm::Nearest => ResizeAlg::Nearest,
};
let options = ResizeOptions::new().resize_alg(alg);
resizer
.resize(&src_image, &mut dst_image, &options)

View File

@@ -12,6 +12,7 @@ pub struct ProcessingJob {
#[serde(skip)]
pub sources: Vec<ImageSource>,
pub resize: Option<ResizeConfig>,
pub resize_algorithm: ResizeAlgorithm,
pub rotation: Option<Rotation>,
pub flip: Option<Flip>,
pub adjustments: Option<AdjustmentsConfig>,
@@ -20,6 +21,7 @@ pub struct ProcessingJob {
pub metadata: Option<MetadataConfig>,
pub watermark: Option<WatermarkConfig>,
pub rename: Option<RenameConfig>,
pub overwrite_behavior: OverwriteBehavior,
pub preserve_directory_structure: bool,
pub progressive_jpeg: bool,
pub avif_speed: u8,
@@ -32,6 +34,7 @@ impl ProcessingJob {
output_dir: output_dir.as_ref().to_path_buf(),
sources: Vec::new(),
resize: None,
resize_algorithm: ResizeAlgorithm::default(),
rotation: None,
flip: None,
adjustments: None,
@@ -40,6 +43,7 @@ impl ProcessingJob {
metadata: None,
watermark: None,
rename: None,
overwrite_behavior: OverwriteBehavior::default(),
preserve_directory_structure: false,
progressive_jpeg: false,
avif_speed: 6,

View File

@@ -31,6 +31,7 @@ impl Preset {
output_dir: output_dir.into(),
sources: Vec::new(),
resize: self.resize.clone(),
resize_algorithm: crate::operations::ResizeAlgorithm::default(),
rotation: self.rotation,
flip: self.flip,
adjustments: None,
@@ -39,6 +40,7 @@ impl Preset {
metadata: self.metadata.clone(),
watermark: self.watermark.clone(),
rename: self.rename.clone(),
overwrite_behavior: crate::operations::OverwriteBehavior::default(),
preserve_directory_structure: false,
progressive_jpeg: false,
avif_speed: 6,