From 704f5568676612d058099762abe190cb0a796da0 Mon Sep 17 00:00:00 2001 From: lashman Date: Fri, 6 Mar 2026 18:18:04 +0200 Subject: [PATCH] Add EXIF auto-orient and aspect ratio lock toggle - Implement auto_orient_from_exif() that reads EXIF Orientation tag and applies the correct rotation/flip for all 8 EXIF orientations - Add aspect ratio lock toggle to resize step Width/Height mode - When lock is active, changing width auto-calculates height from the first loaded image's aspect ratio, and vice versa - Uses recursive update guard (Cell) to prevent infinite loops --- pixstrip-core/src/executor.rs | 45 +++++++++++++++++++- pixstrip-gtk/src/steps/step_resize.rs | 59 +++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/pixstrip-core/src/executor.rs b/pixstrip-core/src/executor.rs index 2460dba..0205a07 100644 --- a/pixstrip-core/src/executor.rs +++ b/pixstrip-core/src/executor.rs @@ -331,7 +331,7 @@ impl PipelineExecutor { Rotation::Cw90 => img.rotate90(), Rotation::Cw180 => img.rotate180(), Rotation::Cw270 => img.rotate270(), - Rotation::AutoOrient => img, + Rotation::AutoOrient => auto_orient_from_exif(img, &source.path), }; } @@ -502,6 +502,49 @@ fn num_cpus() -> usize { .unwrap_or(1) } +/// Read EXIF orientation tag and apply the appropriate rotation/flip. +/// EXIF orientation values: +/// 1 = Normal, 2 = Flipped horizontal, 3 = Rotated 180, +/// 4 = Flipped vertical, 5 = Transposed (flip H + rotate 270), +/// 6 = Rotated 90 CW, 7 = Transverse (flip H + rotate 90), +/// 8 = Rotated 270 CW +fn auto_orient_from_exif( + img: image::DynamicImage, + path: &std::path::Path, +) -> image::DynamicImage { + let Ok(metadata) = little_exif::metadata::Metadata::new_from_path(path) else { + return img; + }; + + let endian = metadata.get_endian(); + let Some(tag) = metadata.get_tag(&little_exif::exif_tag::ExifTag::Orientation(Vec::new())) else { + return img; + }; + + let bytes = tag.value_as_u8_vec(endian); + if bytes.len() < 2 { + return img; + } + + // Orientation is a 16-bit unsigned integer + let orientation = match endian { + little_exif::endian::Endian::Little => u16::from_le_bytes([bytes[0], bytes[1]]), + little_exif::endian::Endian::Big => u16::from_be_bytes([bytes[0], bytes[1]]), + }; + + match orientation { + 1 => img, // Normal + 2 => img.fliph(), // Flipped horizontal + 3 => img.rotate180(), // Rotated 180 + 4 => img.flipv(), // Flipped vertical + 5 => img.fliph().rotate270(), // Transposed + 6 => img.rotate90(), // Rotated 90 CW + 7 => img.fliph().rotate90(), // Transverse + 8 => img.rotate270(), // Rotated 270 CW + _ => img, + } +} + fn find_unique_path(path: &std::path::Path) -> std::path::PathBuf { let stem = path .file_stem() diff --git a/pixstrip-gtk/src/steps/step_resize.rs b/pixstrip-gtk/src/steps/step_resize.rs index 477572a..9810da0 100644 --- a/pixstrip-gtk/src/steps/step_resize.rs +++ b/pixstrip-gtk/src/steps/step_resize.rs @@ -2,6 +2,15 @@ use adw::prelude::*; use gtk::glib; use crate::app::AppState; +/// Get the aspect ratio (width/height) of the first loaded image. +/// Returns 0.0 if no images are loaded or the image can't be read. +fn get_first_image_aspect(files: &[std::path::PathBuf]) -> f64 { + let Some(first) = files.first() else { return 0.0 }; + let Ok((w, h)) = image::image_dimensions(first) else { return 0.0 }; + if h == 0 { return 0.0; } + w as f64 / h as f64 +} + pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { let scrolled = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) @@ -61,6 +70,13 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .adjustment(>k::Adjustment::new(cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0)) .build(); + let lock_row = adw::SwitchRow::builder() + .title("Lock Aspect Ratio") + .subtitle("Changing one dimension auto-calculates the other") + .active(true) + .build(); + lock_row.add_prefix(>k::Image::from_icon_name("changes-prevent-symbolic")); + let height_row = adw::SpinRow::builder() .title("Height") .subtitle("Target height in pixels (0 = auto from aspect ratio)") @@ -68,6 +84,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { .build(); wh_group.add(&width_row); + wh_group.add(&lock_row); wh_group.add(&height_row); wh_box.append(&wh_group); @@ -524,28 +541,70 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage { jc.borrow_mut().resize_enabled = row.is_active(); }); } + // Shared flag to prevent recursive updates from aspect ratio lock + let updating_lock = std::rc::Rc::new(std::cell::Cell::new(false)); { let jc = state.job_config.clone(); let pw = preview_width.clone(); + let ph = preview_height.clone(); let draw = drawing.clone(); let rt = render_thumb.clone(); + let lock = lock_row.clone(); + let hr = height_row.clone(); + let files = state.loaded_files.clone(); + let upd = updating_lock.clone(); width_row.connect_value_notify(move |row| { + if upd.get() { return; } let val = row.value() as u32; jc.borrow_mut().resize_width = val; *pw.borrow_mut() = val; + + // Auto-calculate height if lock is active + if lock.is_active() && val > 0 { + let aspect = get_first_image_aspect(&files.borrow()); + if aspect > 0.0 { + let new_h = (val as f64 / aspect).round() as u32; + upd.set(true); + hr.set_value(new_h as f64); + jc.borrow_mut().resize_height = new_h; + *ph.borrow_mut() = new_h; + upd.set(false); + } + } + draw.queue_draw(); rt(); }); } { let jc = state.job_config.clone(); + let pw = preview_width.clone(); let ph = preview_height.clone(); let draw = drawing.clone(); let rt = render_thumb.clone(); + let lock = lock_row.clone(); + let wr = width_row.clone(); + let files = state.loaded_files.clone(); + let upd = updating_lock.clone(); height_row.connect_value_notify(move |row| { + if upd.get() { return; } let val = row.value() as u32; jc.borrow_mut().resize_height = val; *ph.borrow_mut() = val; + + // Auto-calculate width if lock is active + if lock.is_active() && val > 0 { + let aspect = get_first_image_aspect(&files.borrow()); + if aspect > 0.0 { + let new_w = (val as f64 * aspect).round() as u32; + upd.set(true); + wr.set_value(new_w as f64); + jc.borrow_mut().resize_width = new_w; + *pw.borrow_mut() = new_w; + upd.set(false); + } + } + draw.queue_draw(); rt(); });