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:
@@ -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.
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct JobConfig {
|
||||
pub resize_width: u32,
|
||||
pub resize_height: u32,
|
||||
pub allow_upscale: bool,
|
||||
pub resize_algorithm: u32,
|
||||
// Adjustments
|
||||
pub adjustments_enabled: bool,
|
||||
pub rotation: u32,
|
||||
@@ -222,6 +223,7 @@ fn build_ui(app: &adw::Application) {
|
||||
resize_width: if remember { sess_state.resize_width.unwrap_or(1200) } else { 1200 },
|
||||
resize_height: if remember { sess_state.resize_height.unwrap_or(0) } else { 0 },
|
||||
allow_upscale: false,
|
||||
resize_algorithm: 0,
|
||||
adjustments_enabled: false,
|
||||
rotation: 0,
|
||||
flip: 0,
|
||||
@@ -1255,6 +1257,12 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
allow_upscale: cfg.allow_upscale,
|
||||
});
|
||||
}
|
||||
job.resize_algorithm = match cfg.resize_algorithm {
|
||||
1 => pixstrip_core::operations::ResizeAlgorithm::CatmullRom,
|
||||
2 => pixstrip_core::operations::ResizeAlgorithm::Bilinear,
|
||||
3 => pixstrip_core::operations::ResizeAlgorithm::Nearest,
|
||||
_ => pixstrip_core::operations::ResizeAlgorithm::Lanczos3,
|
||||
};
|
||||
}
|
||||
|
||||
if cfg.convert_enabled {
|
||||
@@ -1344,8 +1352,8 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
});
|
||||
}
|
||||
|
||||
// Rotation, Flip, and Adjustments (only when adjustments step is enabled)
|
||||
if cfg.adjustments_enabled {
|
||||
// Rotation and Flip apply from the resize step, so enable when either resize or adjustments is active
|
||||
if cfg.resize_enabled || cfg.adjustments_enabled {
|
||||
job.rotation = Some(match cfg.rotation {
|
||||
1 => pixstrip_core::operations::Rotation::Cw90,
|
||||
2 => pixstrip_core::operations::Rotation::Cw180,
|
||||
@@ -1359,7 +1367,10 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
2 => pixstrip_core::operations::Flip::Vertical,
|
||||
_ => pixstrip_core::operations::Flip::None,
|
||||
});
|
||||
}
|
||||
|
||||
// Adjustments (brightness, contrast, etc.)
|
||||
if cfg.adjustments_enabled {
|
||||
let crop = match cfg.crop_aspect_ratio {
|
||||
1 => Some((1.0, 1.0)),
|
||||
2 => Some((4.0, 3.0)),
|
||||
@@ -1436,6 +1447,12 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
||||
}
|
||||
|
||||
job.preserve_directory_structure = cfg.preserve_dir_structure;
|
||||
job.overwrite_behavior = match cfg.overwrite_behavior {
|
||||
1 => pixstrip_core::operations::OverwriteBehavior::AutoRename,
|
||||
2 => pixstrip_core::operations::OverwriteBehavior::Overwrite,
|
||||
3 => pixstrip_core::operations::OverwriteBehavior::Skip,
|
||||
_ => pixstrip_core::operations::OverwriteBehavior::AutoRename, // 0 "Ask" defaults to auto-rename in batch
|
||||
};
|
||||
|
||||
drop(cfg);
|
||||
|
||||
|
||||
@@ -453,6 +453,12 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
||||
jc.borrow_mut().flip = row.selected();
|
||||
});
|
||||
}
|
||||
{
|
||||
let jc = state.job_config.clone();
|
||||
algorithm_row.connect_selected_notify(move |row| {
|
||||
jc.borrow_mut().resize_algorithm = row.selected();
|
||||
});
|
||||
}
|
||||
|
||||
scrolled.set_child(Some(&content));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user