image: Add rotation and auto rotate based on exif
This commit is contained in:
parent
3d6564377a
commit
a2e6767584
10 changed files with 257 additions and 94 deletions
16
Cargo.lock
generated
16
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@
|
|||
</child>
|
||||
<child>
|
||||
<object class="LpImage" id="image">
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<child>
|
||||
<object class="GtkGestureClick" id="click_gesture">
|
||||
<property name="button">3</property>
|
||||
|
|
@ -63,10 +61,10 @@
|
|||
<attribute name="label" translatable="yes">_Copy</attribute>
|
||||
<attribute name="action">win.copy</attribute>
|
||||
</item>
|
||||
<!--<item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Rotate</attribute>
|
||||
<attribute name="action">win.rotate-image</attribute>
|
||||
</item>-->
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Set as Background…</attribute>
|
||||
<attribute name="action">win.set-background</attribute>
|
||||
|
|
|
|||
|
|
@ -129,10 +129,10 @@
|
|||
<attribute name="label" translatable="yes">_Print…</attribute>
|
||||
<attribute name="action">win.print</attribute>
|
||||
</item>
|
||||
<!--<item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Rotate</attribute>
|
||||
<attribute name="action">win.rotate-image</attribute>
|
||||
</item>-->
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="label" translatable="yes">_Set as Background…</attribute>
|
||||
<attribute name="action">win.set-background</attribute>
|
||||
|
|
|
|||
88
src/image_metadata.rs
Normal file
88
src/image_metadata.rs
Normal file
|
|
@ -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<u32> 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ mod application;
|
|||
mod config;
|
||||
mod file_model;
|
||||
mod i18n;
|
||||
mod image_metadata;
|
||||
mod thumbnail;
|
||||
mod util;
|
||||
mod widgets;
|
||||
|
|
|
|||
|
|
@ -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<Option<gio::File>>,
|
||||
pub image_width: Cell<i32>,
|
||||
pub image_height: Cell<i32>,
|
||||
pub texture: RefCell<Option<gdk::Texture>>,
|
||||
/// Rotation final value (can differ from `rotation` during animation)
|
||||
pub rotation_target: Cell<f64>,
|
||||
/// Rotated presentation of original image in degrees clockwise
|
||||
pub rotation: Cell<f64>,
|
||||
// Animates the `rotation` property
|
||||
pub rotation_animation: RefCell<Option<adw::TimedAnimation>>,
|
||||
/// Mirrored presentation of original image
|
||||
pub mirrored: Cell<bool>,
|
||||
/// Currently EXIF data
|
||||
pub image_metadata: RefCell<ImageMetadata>,
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
|
|
@ -46,13 +56,13 @@ mod imp {
|
|||
impl ObjectImpl for LpImage {
|
||||
fn properties() -> &'static [glib::ParamSpec] {
|
||||
static PROPERTIES: Lazy<Vec<glib::ParamSpec>> = Lazy::new(|| {
|
||||
vec![glib::ParamSpecObject::new(
|
||||
"file",
|
||||
"File",
|
||||
"The current file",
|
||||
gio::File::static_type(),
|
||||
glib::ParamFlags::READWRITE,
|
||||
)]
|
||||
vec![
|
||||
glib::ParamSpecObject::builder::<gio::File>("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)
|
||||
// image dimension for default orientation
|
||||
let (snapshot_width_default, snapshot_height_default) =
|
||||
if texture_ratio > widget_ratio {
|
||||
(widget_width, widget_width / texture_ratio)
|
||||
} else {
|
||||
(height * texture_ratio, height)
|
||||
(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<gdk::Texture> {
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<gdk::Texture> {
|
||||
self.imp().image.texture()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue