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::error::{PixstripError, Result};
|
||||||
use crate::loader::ImageLoader;
|
use crate::loader::ImageLoader;
|
||||||
use crate::operations::adjustments::apply_adjustments;
|
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::watermark::apply_watermark;
|
||||||
use crate::operations::{Flip, Rotation};
|
use crate::operations::{Flip, Rotation};
|
||||||
use crate::pipeline::ProcessingJob;
|
use crate::pipeline::ProcessingJob;
|
||||||
@@ -344,7 +344,7 @@ impl PipelineExecutor {
|
|||||||
|
|
||||||
// Resize
|
// Resize
|
||||||
if let Some(ref config) = job.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)
|
// Adjustments (brightness, contrast, saturation, effects, crop, padding)
|
||||||
@@ -413,6 +413,25 @@ impl PipelineExecutor {
|
|||||||
job.output_path_for(source, Some(output_format))
|
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
|
// Ensure output directory exists
|
||||||
if let Some(parent) = output_path.parent() {
|
if let Some(parent) = output_path.parent() {
|
||||||
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
|
std::fs::create_dir_all(parent).map_err(PixstripError::Io)?;
|
||||||
@@ -451,6 +470,30 @@ fn num_cpus() -> usize {
|
|||||||
.unwrap_or(1)
|
.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) {
|
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.
|
// Best-effort: try to copy EXIF from source to output using little_exif.
|
||||||
// If it fails (e.g. non-JPEG, no EXIF), silently continue.
|
// 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 ---
|
// --- Rotation / Flip ---
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[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 ---
|
// --- Rename ---
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[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::error::{PixstripError, Result};
|
||||||
use crate::types::Dimensions;
|
use crate::types::Dimensions;
|
||||||
|
|
||||||
use super::ResizeConfig;
|
use super::{ResizeConfig, ResizeAlgorithm};
|
||||||
|
|
||||||
pub fn resize_image(
|
pub fn resize_image(
|
||||||
src: &image::DynamicImage,
|
src: &image::DynamicImage,
|
||||||
config: &ResizeConfig,
|
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> {
|
) -> Result<image::DynamicImage> {
|
||||||
let original = Dimensions {
|
let original = Dimensions {
|
||||||
width: src.width(),
|
width: src.width(),
|
||||||
@@ -40,7 +48,13 @@ pub fn resize_image(
|
|||||||
);
|
);
|
||||||
|
|
||||||
let mut resizer = Resizer::new();
|
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
|
resizer
|
||||||
.resize(&src_image, &mut dst_image, &options)
|
.resize(&src_image, &mut dst_image, &options)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub struct ProcessingJob {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub sources: Vec<ImageSource>,
|
pub sources: Vec<ImageSource>,
|
||||||
pub resize: Option<ResizeConfig>,
|
pub resize: Option<ResizeConfig>,
|
||||||
|
pub resize_algorithm: ResizeAlgorithm,
|
||||||
pub rotation: Option<Rotation>,
|
pub rotation: Option<Rotation>,
|
||||||
pub flip: Option<Flip>,
|
pub flip: Option<Flip>,
|
||||||
pub adjustments: Option<AdjustmentsConfig>,
|
pub adjustments: Option<AdjustmentsConfig>,
|
||||||
@@ -20,6 +21,7 @@ pub struct ProcessingJob {
|
|||||||
pub metadata: Option<MetadataConfig>,
|
pub metadata: Option<MetadataConfig>,
|
||||||
pub watermark: Option<WatermarkConfig>,
|
pub watermark: Option<WatermarkConfig>,
|
||||||
pub rename: Option<RenameConfig>,
|
pub rename: Option<RenameConfig>,
|
||||||
|
pub overwrite_behavior: OverwriteBehavior,
|
||||||
pub preserve_directory_structure: bool,
|
pub preserve_directory_structure: bool,
|
||||||
pub progressive_jpeg: bool,
|
pub progressive_jpeg: bool,
|
||||||
pub avif_speed: u8,
|
pub avif_speed: u8,
|
||||||
@@ -32,6 +34,7 @@ impl ProcessingJob {
|
|||||||
output_dir: output_dir.as_ref().to_path_buf(),
|
output_dir: output_dir.as_ref().to_path_buf(),
|
||||||
sources: Vec::new(),
|
sources: Vec::new(),
|
||||||
resize: None,
|
resize: None,
|
||||||
|
resize_algorithm: ResizeAlgorithm::default(),
|
||||||
rotation: None,
|
rotation: None,
|
||||||
flip: None,
|
flip: None,
|
||||||
adjustments: None,
|
adjustments: None,
|
||||||
@@ -40,6 +43,7 @@ impl ProcessingJob {
|
|||||||
metadata: None,
|
metadata: None,
|
||||||
watermark: None,
|
watermark: None,
|
||||||
rename: None,
|
rename: None,
|
||||||
|
overwrite_behavior: OverwriteBehavior::default(),
|
||||||
preserve_directory_structure: false,
|
preserve_directory_structure: false,
|
||||||
progressive_jpeg: false,
|
progressive_jpeg: false,
|
||||||
avif_speed: 6,
|
avif_speed: 6,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ impl Preset {
|
|||||||
output_dir: output_dir.into(),
|
output_dir: output_dir.into(),
|
||||||
sources: Vec::new(),
|
sources: Vec::new(),
|
||||||
resize: self.resize.clone(),
|
resize: self.resize.clone(),
|
||||||
|
resize_algorithm: crate::operations::ResizeAlgorithm::default(),
|
||||||
rotation: self.rotation,
|
rotation: self.rotation,
|
||||||
flip: self.flip,
|
flip: self.flip,
|
||||||
adjustments: None,
|
adjustments: None,
|
||||||
@@ -39,6 +40,7 @@ impl Preset {
|
|||||||
metadata: self.metadata.clone(),
|
metadata: self.metadata.clone(),
|
||||||
watermark: self.watermark.clone(),
|
watermark: self.watermark.clone(),
|
||||||
rename: self.rename.clone(),
|
rename: self.rename.clone(),
|
||||||
|
overwrite_behavior: crate::operations::OverwriteBehavior::default(),
|
||||||
preserve_directory_structure: false,
|
preserve_directory_structure: false,
|
||||||
progressive_jpeg: false,
|
progressive_jpeg: false,
|
||||||
avif_speed: 6,
|
avif_speed: 6,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub struct JobConfig {
|
|||||||
pub resize_width: u32,
|
pub resize_width: u32,
|
||||||
pub resize_height: u32,
|
pub resize_height: u32,
|
||||||
pub allow_upscale: bool,
|
pub allow_upscale: bool,
|
||||||
|
pub resize_algorithm: u32,
|
||||||
// Adjustments
|
// Adjustments
|
||||||
pub adjustments_enabled: bool,
|
pub adjustments_enabled: bool,
|
||||||
pub rotation: u32,
|
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_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 },
|
resize_height: if remember { sess_state.resize_height.unwrap_or(0) } else { 0 },
|
||||||
allow_upscale: false,
|
allow_upscale: false,
|
||||||
|
resize_algorithm: 0,
|
||||||
adjustments_enabled: false,
|
adjustments_enabled: false,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
flip: 0,
|
flip: 0,
|
||||||
@@ -1255,6 +1257,12 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
|||||||
allow_upscale: cfg.allow_upscale,
|
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 {
|
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)
|
// Rotation and Flip apply from the resize step, so enable when either resize or adjustments is active
|
||||||
if cfg.adjustments_enabled {
|
if cfg.resize_enabled || cfg.adjustments_enabled {
|
||||||
job.rotation = Some(match cfg.rotation {
|
job.rotation = Some(match cfg.rotation {
|
||||||
1 => pixstrip_core::operations::Rotation::Cw90,
|
1 => pixstrip_core::operations::Rotation::Cw90,
|
||||||
2 => pixstrip_core::operations::Rotation::Cw180,
|
2 => pixstrip_core::operations::Rotation::Cw180,
|
||||||
@@ -1359,7 +1367,10 @@ fn run_processing(_window: &adw::ApplicationWindow, ui: &WizardUi) {
|
|||||||
2 => pixstrip_core::operations::Flip::Vertical,
|
2 => pixstrip_core::operations::Flip::Vertical,
|
||||||
_ => pixstrip_core::operations::Flip::None,
|
_ => pixstrip_core::operations::Flip::None,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjustments (brightness, contrast, etc.)
|
||||||
|
if cfg.adjustments_enabled {
|
||||||
let crop = match cfg.crop_aspect_ratio {
|
let crop = match cfg.crop_aspect_ratio {
|
||||||
1 => Some((1.0, 1.0)),
|
1 => Some((1.0, 1.0)),
|
||||||
2 => Some((4.0, 3.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.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);
|
drop(cfg);
|
||||||
|
|
||||||
|
|||||||
@@ -453,6 +453,12 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
jc.borrow_mut().flip = row.selected();
|
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));
|
scrolled.set_child(Some(&content));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user