image: Add rotation and auto rotate based on exif

This commit is contained in:
Sophie Herold 2022-11-03 17:40:06 +01:00
parent 3d6564377a
commit a2e6767584
10 changed files with 257 additions and 94 deletions

88
src/image_metadata.rs Normal file
View 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(),
}
}
}

View file

@ -24,6 +24,7 @@ mod application;
mod config;
mod file_model;
mod i18n;
mod image_metadata;
mod thumbnail;
mod util;
mod widgets;

View file

@ -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: &gtk::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<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();
}
}

View file

@ -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()
}

View file

@ -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 =>

View file

@ -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();