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<bool>) to prevent infinite loops
This commit is contained in:
@@ -331,7 +331,7 @@ impl PipelineExecutor {
|
|||||||
Rotation::Cw90 => img.rotate90(),
|
Rotation::Cw90 => img.rotate90(),
|
||||||
Rotation::Cw180 => img.rotate180(),
|
Rotation::Cw180 => img.rotate180(),
|
||||||
Rotation::Cw270 => img.rotate270(),
|
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)
|
.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 {
|
fn find_unique_path(path: &std::path::Path) -> std::path::PathBuf {
|
||||||
let stem = path
|
let stem = path
|
||||||
.file_stem()
|
.file_stem()
|
||||||
|
|||||||
@@ -2,6 +2,15 @@ use adw::prelude::*;
|
|||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
use crate::app::AppState;
|
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 {
|
pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
||||||
let scrolled = gtk::ScrolledWindow::builder()
|
let scrolled = gtk::ScrolledWindow::builder()
|
||||||
.hscrollbar_policy(gtk::PolicyType::Never)
|
.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))
|
.adjustment(>k::Adjustment::new(cfg.resize_width as f64, 0.0, 10000.0, 1.0, 100.0, 0.0))
|
||||||
.build();
|
.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()
|
let height_row = adw::SpinRow::builder()
|
||||||
.title("Height")
|
.title("Height")
|
||||||
.subtitle("Target height in pixels (0 = auto from aspect ratio)")
|
.subtitle("Target height in pixels (0 = auto from aspect ratio)")
|
||||||
@@ -68,6 +84,7 @@ pub fn build_resize_page(state: &AppState) -> adw::NavigationPage {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
wh_group.add(&width_row);
|
wh_group.add(&width_row);
|
||||||
|
wh_group.add(&lock_row);
|
||||||
wh_group.add(&height_row);
|
wh_group.add(&height_row);
|
||||||
wh_box.append(&wh_group);
|
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();
|
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 jc = state.job_config.clone();
|
||||||
let pw = preview_width.clone();
|
let pw = preview_width.clone();
|
||||||
|
let ph = preview_height.clone();
|
||||||
let draw = drawing.clone();
|
let draw = drawing.clone();
|
||||||
let rt = render_thumb.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| {
|
width_row.connect_value_notify(move |row| {
|
||||||
|
if upd.get() { return; }
|
||||||
let val = row.value() as u32;
|
let val = row.value() as u32;
|
||||||
jc.borrow_mut().resize_width = val;
|
jc.borrow_mut().resize_width = val;
|
||||||
*pw.borrow_mut() = 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();
|
draw.queue_draw();
|
||||||
rt();
|
rt();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let jc = state.job_config.clone();
|
let jc = state.job_config.clone();
|
||||||
|
let pw = preview_width.clone();
|
||||||
let ph = preview_height.clone();
|
let ph = preview_height.clone();
|
||||||
let draw = drawing.clone();
|
let draw = drawing.clone();
|
||||||
let rt = render_thumb.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| {
|
height_row.connect_value_notify(move |row| {
|
||||||
|
if upd.get() { return; }
|
||||||
let val = row.value() as u32;
|
let val = row.value() as u32;
|
||||||
jc.borrow_mut().resize_height = val;
|
jc.borrow_mut().resize_height = val;
|
||||||
*ph.borrow_mut() = 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();
|
draw.queue_draw();
|
||||||
rt();
|
rt();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user