Fix 26 bugs, edge cases, and consistency issues from fifth audit pass
Critical: undo toast now trashes only batch output files (not entire dir), JPEG scanline write errors propagated, selective metadata write result returned. High: zero-dimension guards in ResizeConfig/fit_within, negative aspect ratio rejection, FM integration toggle infinite recursion guard, saturating counter arithmetic in executor. Medium: PNG compression level passed to oxipng, pct mode updates job_config, external file loading updates step indicator, CLI undo removes history entries, watch config write failures reported, fast-copy path reads image dimensions for rename templates, discovery excludes unprocessable formats (heic/svg/ico/jxl), CLI warns on invalid algorithm/overwrite values, resolve_collision trailing dot fix, generation guards on all preview threads to cancel stale results, default DPI aligned to 0, watermark text width uses char count not byte length. Low: binary path escaped in Nautilus extension, file dialog filter aligned with discovery, reset_wizard clears preset_mode and output_dir.
This commit is contained in:
@@ -23,25 +23,43 @@ pub enum ResizeConfig {
|
||||
|
||||
impl ResizeConfig {
|
||||
pub fn target_for(&self, original: Dimensions) -> Dimensions {
|
||||
match self {
|
||||
if original.width == 0 || original.height == 0 {
|
||||
return original;
|
||||
}
|
||||
let result = match self {
|
||||
Self::ByWidth(w) => {
|
||||
if *w == 0 {
|
||||
return original;
|
||||
}
|
||||
let scale = *w as f64 / original.width as f64;
|
||||
Dimensions {
|
||||
width: *w,
|
||||
height: (original.height as f64 * scale).round() as u32,
|
||||
height: (original.height as f64 * scale).round().max(1.0) as u32,
|
||||
}
|
||||
}
|
||||
Self::ByHeight(h) => {
|
||||
if *h == 0 {
|
||||
return original;
|
||||
}
|
||||
let scale = *h as f64 / original.height as f64;
|
||||
Dimensions {
|
||||
width: (original.width as f64 * scale).round() as u32,
|
||||
width: (original.width as f64 * scale).round().max(1.0) as u32,
|
||||
height: *h,
|
||||
}
|
||||
}
|
||||
Self::FitInBox { max, allow_upscale } => {
|
||||
original.fit_within(*max, *allow_upscale)
|
||||
}
|
||||
Self::Exact(dims) => *dims,
|
||||
Self::Exact(dims) => {
|
||||
if dims.width == 0 || dims.height == 0 {
|
||||
return original;
|
||||
}
|
||||
*dims
|
||||
}
|
||||
};
|
||||
Dimensions {
|
||||
width: result.width.max(1),
|
||||
height: result.height.max(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,6 +242,7 @@ pub enum WatermarkRotation {
|
||||
Degrees45,
|
||||
DegreesNeg45,
|
||||
Degrees90,
|
||||
Custom(f32),
|
||||
}
|
||||
|
||||
// --- Adjustments ---
|
||||
@@ -255,16 +274,16 @@ impl AdjustmentsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Overwrite Behavior ---
|
||||
// --- Overwrite Action (concrete action, no "Ask" variant) ---
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum OverwriteBehavior {
|
||||
pub enum OverwriteAction {
|
||||
AutoRename,
|
||||
Overwrite,
|
||||
Skip,
|
||||
}
|
||||
|
||||
impl Default for OverwriteBehavior {
|
||||
impl Default for OverwriteAction {
|
||||
fn default() -> Self {
|
||||
Self::AutoRename
|
||||
}
|
||||
@@ -278,39 +297,85 @@ pub struct RenameConfig {
|
||||
pub suffix: String,
|
||||
pub counter_start: u32,
|
||||
pub counter_padding: u32,
|
||||
#[serde(default)]
|
||||
pub counter_enabled: bool,
|
||||
/// 0=before prefix, 1=before name, 2=after name, 3=after suffix, 4=replace name
|
||||
#[serde(default = "default_counter_position")]
|
||||
pub counter_position: u32,
|
||||
pub template: Option<String>,
|
||||
/// 0=none, 1=lowercase, 2=uppercase, 3=title case
|
||||
pub case_mode: u32,
|
||||
/// 0=none, 1=underscore, 2=hyphen, 3=dot, 4=camelcase, 5=remove
|
||||
#[serde(default)]
|
||||
pub replace_spaces: u32,
|
||||
/// 0=keep all, 1=filesystem-safe, 2=web-safe, 3=hyphens+underscores, 4=hyphens only, 5=alphanumeric only
|
||||
#[serde(default)]
|
||||
pub special_chars: u32,
|
||||
pub regex_find: String,
|
||||
pub regex_replace: String,
|
||||
}
|
||||
|
||||
fn default_counter_position() -> u32 { 3 }
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
// Apply regex find-and-replace on the original name
|
||||
// 1. Apply regex find-and-replace on the original name
|
||||
let working_name = rename::apply_regex_replace(original_name, &self.regex_find, &self.regex_replace);
|
||||
|
||||
let mut name = String::new();
|
||||
if !self.prefix.is_empty() {
|
||||
name.push_str(&self.prefix);
|
||||
}
|
||||
name.push_str(&working_name);
|
||||
if !self.suffix.is_empty() {
|
||||
name.push_str(&self.suffix);
|
||||
}
|
||||
name.push('_');
|
||||
name.push_str(&counter_str);
|
||||
// 2. Apply space replacement
|
||||
let working_name = rename::apply_space_replacement(&working_name, self.replace_spaces);
|
||||
|
||||
// Apply case conversion
|
||||
let name = rename::apply_case_conversion(&name, self.case_mode);
|
||||
// 3. Apply special character filtering
|
||||
let working_name = rename::apply_special_chars(&working_name, self.special_chars);
|
||||
|
||||
format!("{}.{}", name, extension)
|
||||
// 4. Build counter string
|
||||
let counter_str = if self.counter_enabled {
|
||||
let counter = self.counter_start.saturating_add(index.saturating_sub(1));
|
||||
let padding = (self.counter_padding as usize).min(10);
|
||||
format!("{:0>width$}", counter, width = padding)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let has_counter = self.counter_enabled && !counter_str.is_empty();
|
||||
|
||||
// 5. Assemble parts based on counter position
|
||||
// Positions: 0=before prefix, 1=before name, 2=after name, 3=after suffix, 4=replace name
|
||||
let mut result = String::new();
|
||||
|
||||
if has_counter && self.counter_position == 0 {
|
||||
result.push_str(&counter_str);
|
||||
result.push('_');
|
||||
}
|
||||
|
||||
result.push_str(&self.prefix);
|
||||
|
||||
if has_counter && self.counter_position == 1 {
|
||||
result.push_str(&counter_str);
|
||||
result.push('_');
|
||||
}
|
||||
|
||||
if has_counter && self.counter_position == 4 {
|
||||
result.push_str(&counter_str);
|
||||
} else {
|
||||
result.push_str(&working_name);
|
||||
}
|
||||
|
||||
if has_counter && self.counter_position == 2 {
|
||||
result.push('_');
|
||||
result.push_str(&counter_str);
|
||||
}
|
||||
|
||||
result.push_str(&self.suffix);
|
||||
|
||||
if has_counter && self.counter_position == 3 {
|
||||
result.push('_');
|
||||
result.push_str(&counter_str);
|
||||
}
|
||||
|
||||
// 6. Apply case conversion
|
||||
let result = rename::apply_case_conversion(&result, self.case_mode);
|
||||
|
||||
format!("{}.{}", result, extension)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user