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:
2026-03-07 19:47:23 +02:00
parent 270a7db60d
commit b432cc7431
44 changed files with 5748 additions and 2221 deletions

View File

@@ -57,21 +57,27 @@ pub fn apply_adjustments(
fn crop_to_aspect_ratio(img: DynamicImage, w_ratio: f64, h_ratio: f64) -> DynamicImage {
let (iw, ih) = (img.width(), img.height());
if !w_ratio.is_finite() || !h_ratio.is_finite()
|| w_ratio <= 0.0 || h_ratio <= 0.0
|| iw == 0 || ih == 0
{
return img;
}
let target_ratio = w_ratio / h_ratio;
let current_ratio = iw as f64 / ih as f64;
let (crop_w, crop_h) = if current_ratio > target_ratio {
// Image is wider than target, crop width
let new_w = (ih as f64 * target_ratio) as u32;
(new_w, ih)
(new_w.min(iw), ih)
} else {
// Image is taller than target, crop height
let new_h = (iw as f64 / target_ratio) as u32;
(iw, new_h)
(iw, new_h.min(ih))
};
let x = (iw - crop_w) / 2;
let y = (ih - crop_h) / 2;
let x = iw.saturating_sub(crop_w) / 2;
let y = ih.saturating_sub(crop_h) / 2;
img.crop_imm(x, y, crop_w, crop_h)
}
@@ -140,8 +146,8 @@ fn trim_whitespace(img: DynamicImage) -> DynamicImage {
}
}
let crop_w = right.saturating_sub(left) + 1;
let crop_h = bottom.saturating_sub(top) + 1;
let crop_w = right.saturating_sub(left).saturating_add(1);
let crop_h = bottom.saturating_sub(top).saturating_add(1);
if crop_w == 0 || crop_h == 0 || (crop_w == w && crop_h == h) {
return img;
@@ -151,6 +157,7 @@ fn trim_whitespace(img: DynamicImage) -> DynamicImage {
}
fn adjust_saturation(img: DynamicImage, amount: i32) -> DynamicImage {
let amount = amount.clamp(-100, 100);
let mut rgba = img.into_rgba8();
let factor = 1.0 + (amount as f64 / 100.0);
@@ -187,8 +194,8 @@ fn apply_sepia(img: DynamicImage) -> DynamicImage {
fn add_canvas_padding(img: DynamicImage, padding: u32) -> DynamicImage {
let (w, h) = (img.width(), img.height());
let new_w = w + padding * 2;
let new_h = h + padding * 2;
let new_w = w.saturating_add(padding.saturating_mul(2));
let new_h = h.saturating_add(padding.saturating_mul(2));
let mut canvas = RgbaImage::from_pixel(new_w, new_h, Rgba([255, 255, 255, 255]));