diff --git a/Cargo.lock b/Cargo.lock index 73e6299..50eeb68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -940,6 +940,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kamadak-exif" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4fc70d0ab7e5b6bafa30216a6b48705ea964cdfc29c050f2412295eba58077" +dependencies = [ + "mutate_once", +] + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -1041,6 +1050,7 @@ dependencies = [ "gio", "gtk-macros", "gtk4", + "kamadak-exif", "libadwaita", "log", "once_cell", @@ -1074,6 +1084,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "mutate_once" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" + [[package]] name = "nix" version = "0.24.2" diff --git a/Cargo.toml b/Cargo.toml index 4b7bce0..f34d2cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] anyhow = "1.0.66" gtk-macros = "0.3.0" +kamadak-exif = "0.5.5" log = "0.4.17" once_cell = "1.16.0" pretty_env_logger = "0.4.0" diff --git a/data/gtk/image_page.ui b/data/gtk/image_page.ui index 7053eaa..60f82fb 100644 --- a/data/gtk/image_page.ui +++ b/data/gtk/image_page.ui @@ -22,8 +22,6 @@ - center - center 3 @@ -63,10 +61,10 @@ _Copy win.copy - + _Set as Background… win.set-background diff --git a/data/gtk/window.ui b/data/gtk/window.ui index f1aa043..09929ba 100644 --- a/data/gtk/window.ui +++ b/data/gtk/window.ui @@ -129,10 +129,10 @@ _Print… win.print - + _Set as Background… win.set-background diff --git a/src/image_metadata.rs b/src/image_metadata.rs new file mode 100644 index 0000000..aeae609 --- /dev/null +++ b/src/image_metadata.rs @@ -0,0 +1,88 @@ +use gio::prelude::*; + +#[derive(Default)] +pub enum ImageMetadata { + Exif(exif::Exif), + #[default] + None, +} + +impl ImageMetadata { + // TODO: error handling + pub fn load(file: &gio::File) -> Self { + let file = std::fs::File::open(file.path().unwrap()).unwrap(); + let mut bufreader = std::io::BufReader::new(&file); + let exifreader = exif::Reader::new(); + + if let Ok(exif) = exifreader.read_from_container(&mut bufreader) { + Self::Exif(exif) + } else { + Self::None + } + } + + pub fn orientation(&self) -> Orientation { + match self { + Self::Exif(exif) => { + if let Some(orientation) = exif + .get_field(exif::Tag::Orientation, exif::In::PRIMARY) + .and_then(|x| x.value.get_uint(0)) + { + Orientation::from(orientation) + } else { + Orientation::default() + } + } + Self::None => Orientation::default(), + } + } +} + +// TODO: implement something (that does not fill the complete screen with binary data) +impl std::fmt::Debug for ImageMetadata { + fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + Ok(()) + } +} + +#[derive(Default, Debug, Clone, Copy)] +pub struct Orientation { + pub rotation: f64, + pub mirrored: bool, +} + +impl From for Orientation { + fn from(number: u32) -> Self { + match number { + 8 => Self { + rotation: 90., + mirrored: false, + }, + 3 => Self { + rotation: 180., + mirrored: false, + }, + 6 => Self { + rotation: 270., + mirrored: false, + }, + 2 => Self { + rotation: 0., + mirrored: true, + }, + 5 => Self { + rotation: 90., + mirrored: true, + }, + 4 => Self { + rotation: 180., + mirrored: true, + }, + 7 => Self { + rotation: 270., + mirrored: true, + }, + _ => Self::default(), + } + } +} diff --git a/src/main.rs b/src/main.rs index 9ebb39b..e88bc20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,6 +24,7 @@ mod application; mod config; mod file_model; mod i18n; +mod image_metadata; mod thumbnail; mod util; mod widgets; diff --git a/src/widgets/image.rs b/src/widgets/image.rs index f8d853c..1490620 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -19,21 +19,31 @@ use crate::deps::*; -use gtk::{prelude::*, subclass::prelude::*}; +use adw::{prelude::*, subclass::prelude::*}; use once_cell::sync::Lazy; use std::cell::{Cell, RefCell}; +use crate::image_metadata::ImageMetadata; + mod imp { use super::*; #[derive(Debug, Default)] pub struct LpImage { pub file: RefCell>, - pub image_width: Cell, - pub image_height: Cell, pub texture: RefCell>, + /// Rotation final value (can differ from `rotation` during animation) + pub rotation_target: Cell, + /// Rotated presentation of original image in degrees clockwise + pub rotation: Cell, + // Animates the `rotation` property + pub rotation_animation: RefCell>, + /// Mirrored presentation of original image + pub mirrored: Cell, + /// Currently EXIF data + pub image_metadata: RefCell, } #[glib::object_subclass] @@ -46,13 +56,13 @@ mod imp { impl ObjectImpl for LpImage { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy> = Lazy::new(|| { - vec![glib::ParamSpecObject::new( - "file", - "File", - "The current file", - gio::File::static_type(), - glib::ParamFlags::READWRITE, - )] + vec![ + glib::ParamSpecObject::builder::("file") + .read_only() + .build(), + glib::ParamSpecDouble::builder("rotation").build(), + glib::ParamSpecBoolean::builder("mirrored").build(), + ] }); PROPERTIES.as_ref() @@ -62,6 +72,8 @@ mod imp { let obj = self.instance(); match pspec.name() { "file" => obj.file().to_value(), + "rotation" => obj.rotation().to_value(), + "mirrored" => obj.mirrored().to_value(), _ => unimplemented!(), } } @@ -69,7 +81,8 @@ mod imp { fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { let obj = self.instance(); match pspec.name() { - "file" => obj.set_file(&value.get().unwrap()), + "rotation" => obj.set_rotation(value.get().unwrap()), + "mirrored" => obj.set_mirrored(value.get().unwrap()), _ => unimplemented!(), } } @@ -78,6 +91,13 @@ mod imp { let obj = self.instance(); obj.set_hexpand(true); obj.set_vexpand(true); + + let rotation_animation = adw::TimedAnimation::builder() + .duration(200) + .widget(&*obj) + .target(&adw::PropertyAnimationTarget::new(&*obj, "rotation")) + .build(); + self.rotation_animation.replace(Some(rotation_animation)); } fn dispose(&self) { @@ -92,69 +112,78 @@ mod imp { fn snapshot(&self, snapshot: >k::Snapshot) { if let Some(texture) = self.texture.borrow().as_ref() { let widget = self.instance(); - let width = widget.width() as f64; - let height = widget.height() as f64; + let widget_width = widget.width() as f64; + let widget_height = widget.height() as f64; let texture_ratio = texture.intrinsic_aspect_ratio(); + let rotated = widget.rotation().to_radians().sin().abs(); if texture_ratio == 0.0 { - texture.snapshot(snapshot, width, height); + // TODO: We probably need rotation etc on this? + // Maybe just set ratio to 1. or widget_width/widget_height and run code below? + texture.snapshot(snapshot, widget_width, widget_height); } else { - let widget_ratio = width / height; + let widget_ratio = widget_width / widget_height; - let (snapshot_width, snapshot_height) = if texture_ratio > widget_ratio { - (width, width / texture_ratio) - } else { - (height * texture_ratio, height) - }; + // image dimension for default orientation + let (snapshot_width_default, snapshot_height_default) = + if texture_ratio > widget_ratio { + (widget_width, widget_width / texture_ratio) + } else { + (widget_height * texture_ratio, widget_height) + }; - let x = (width - snapshot_width.ceil()) / 2.0; - let y = (height - snapshot_height.ceil()).floor() / 2.0; + // image dimensions for 90deg rotated + let (snapshot_width_rotated, snapshot_height_rotated) = + if (1.0 / texture_ratio) > widget_ratio { + (widget_width * texture_ratio, widget_width) + } else { + (widget_height, widget_height / texture_ratio) + }; + + // interpolate between size computation during rotation + let mut snapshot_height = rotated * snapshot_height_rotated + + (1. - rotated) * snapshot_height_default; + let mut snapshot_width = + rotated * snapshot_width_rotated + (1. - rotated) * snapshot_width_default; + + // Don't make the image larger then default size + if snapshot_height > texture.height() as f64 + || snapshot_width > texture.width() as f64 + { + snapshot_height = texture.height() as f64; + snapshot_width = texture.width() as f64; + } snapshot.save(); + + // center image in widget + let x = (widget_width - snapshot_width) / 2.0; + let y = (widget_height - snapshot_height) / 2.0; snapshot.translate(&graphene::Point::new(x as f32, y as f32)); + + // center image in coordinates to rotate origin + snapshot.translate(&graphene::Point::new( + (snapshot_width / 2.) as f32, + (snapshot_height / 2.) as f32, + )); + + // apply the transformations from properties + snapshot.rotate(widget.rotation() as f32); + if widget.mirrored() { + snapshot.scale(-1., 1.); + } + + // move back to original position + snapshot.translate(&graphene::Point::new( + (-snapshot_width / 2.) as f32, + (-snapshot_height / 2.) as f32, + )); + texture.snapshot(snapshot, snapshot_width, snapshot_height); snapshot.restore(); } } } - - fn request_mode(&self) -> gtk::SizeRequestMode { - gtk::SizeRequestMode::HeightForWidth - } - - fn measure(&self, orienation: gtk::Orientation, for_size: i32) -> (i32, i32, i32, i32) { - if let Some(texture) = self.texture.borrow().as_ref() { - let default_size = 16.0; // Fall back to default icon size; Just a guess since the API GtkPicture uses is private - let measurement = if for_size < 0 { 0 } else { for_size }; - let (min_width, min_height): (f64, f64) = (0.0, 0.0); - - let (min, nat) = if orienation == gtk::Orientation::Horizontal { - let size = texture.compute_concrete_size( - 0.0, - measurement.into(), - default_size, - default_size, - ); - let nat_width = size.0; - - (min_width.ceil(), nat_width.ceil()) - } else { - let size = texture.compute_concrete_size( - measurement.into(), - 0.0, - default_size, - default_size, - ); - let nat_height = size.1; - - (min_height.ceil(), nat_height.ceil()) - }; - - (min as i32, nat as i32, -1, -1) - } else { - (0, 0, -1, -1) - } - } } } @@ -169,40 +198,22 @@ impl LpImage { imp.file.borrow().clone() } - pub fn set_file(&self, file: &gio::File) { - let imp = self.imp(); - - match gdk::Texture::from_file(file) { - Ok(texture) => { - imp.image_width.set(texture.width()); - imp.image_height.set(texture.height()); - imp.file.replace(Some(file.clone())); - imp.texture.replace(Some(texture)); - } - Err(e) => log::error!("Could not load a valid image from this file: {}", e), - }; - - self.queue_draw(); - self.queue_resize(); - } - - pub fn image_width(&self) -> i32 { - let imp = self.imp(); - imp.image_width.get() - } - - pub fn image_height(&self) -> i32 { - let imp = self.imp(); - imp.image_height.get() - } - pub fn set_texture_with_file(&self, texture: gdk::Texture, source_file: &gio::File) { let imp = self.imp(); + imp.texture.replace(Some(texture)); imp.file.replace(Some(source_file.clone())); + let metadata = ImageMetadata::load(source_file); + let orientation = metadata.orientation(); + + imp.image_metadata.replace(metadata); + + imp.rotation_target.set(-orientation.rotation); + imp.rotation.set(-orientation.rotation); + imp.mirrored.set(orientation.mirrored); + self.queue_draw(); - self.queue_resize(); } pub fn texture(&self) -> Option { @@ -210,6 +221,24 @@ impl LpImage { imp.texture.borrow().clone() } + pub fn rotation(&self) -> f64 { + self.imp().rotation.get() + } + + pub fn set_rotation(&self, rotation: f64) { + self.imp().rotation.set(rotation); + self.queue_draw(); + } + + pub fn mirrored(&self) -> bool { + self.imp().mirrored.get() + } + + pub fn set_mirrored(&self, mirrored: bool) { + self.imp().mirrored.set(mirrored); + self.queue_draw(); + } + pub fn content_provider(&self) -> gdk::ContentProvider { let imp = self.imp(); let mut contents = vec![]; @@ -227,4 +256,16 @@ impl LpImage { gdk::ContentProvider::new_union(contents.as_slice()) } + + pub fn rotate(&self) { + let target = &self.imp().rotation_target; + target.set(target.get() + 90.); + + let borrow = self.imp().rotation_animation.borrow(); + let animation = borrow.as_ref().unwrap(); + + animation.set_value_from(self.rotation()); + animation.set_value_to(target.get()); + animation.play(); + } } diff --git a/src/widgets/image_page.rs b/src/widgets/image_page.rs index 820759c..1559da8 100644 --- a/src/widgets/image_page.rs +++ b/src/widgets/image_page.rs @@ -132,6 +132,10 @@ impl LpImagePage { self.imp().file.get().cloned() } + pub fn image(&self) -> LpImage { + self.imp().image.get() + } + pub fn texture(&self) -> Option { self.imp().image.texture() } diff --git a/src/widgets/image_view.rs b/src/widgets/image_view.rs index 6318c49..5d93f62 100644 --- a/src/widgets/image_view.rs +++ b/src/widgets/image_view.rs @@ -387,6 +387,12 @@ impl LpImageView { } } + pub fn rotate_image(&self) { + if let Some(current_page) = self.current_page() { + current_page.image().rotate(); + } + } + pub fn set_background(&self) -> anyhow::Result<()> { let background = self.uri().context("No URI for current file")?; spawn!(clone!(@weak self as view => diff --git a/src/window.rs b/src/window.rs index e685f37..ac6a59a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -102,6 +102,10 @@ mod imp { win.open_with(); }); + klass.install_action("win.rotate-image", None, move |win, _, _| { + win.rotate_image(); + }); + klass.install_action("win.set-background", None, move |win, _, _| { win.set_background(); }); @@ -280,6 +284,10 @@ impl LpWindow { } } + fn rotate_image(&self) { + self.imp().image_view.rotate_image() + } + fn set_background(&self) { let imp = self.imp();