From afcc24e235112486dbca9f1e7593009df20eda36 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Wed, 19 Jun 2024 00:36:54 +0200 Subject: [PATCH] editor: Apply rotation to files --- Cargo.lock | 2 + Cargo.toml | 4 ++ build-aux/flatpak/org.gnome.Loupe.Devel.json | 5 +- po/POTFILES.in | 1 + src/decoder/formats/glycin_proxy.rs | 1 + src/editing.rs | 63 ++++++++++++++++++++ src/main.rs | 1 + src/widgets/image.rs | 7 +++ src/widgets/image/loading.rs | 6 ++ src/widgets/image/rotation.rs | 29 ++++++++- 10 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 src/editing.rs diff --git a/Cargo.lock b/Cargo.lock index 7371338..2016b27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1584,6 +1584,7 @@ dependencies = [ "arc-swap", "ashpd", "async-channel", + "async-lock", "cairo-rs", "env_logger", "futures-channel", @@ -1593,6 +1594,7 @@ dependencies = [ "gio 0.20.0", "glycin", "gtk4", + "gufo-common", "gvdb-macros", "indexmap", "kamadak-exif", diff --git a/Cargo.toml b/Cargo.toml index 4ff4537..a3ed36f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ ashpd = { version = "0.9.0", features = [ "gtk4_wayland", ], git = "https://github.com/sophie-h/ashpd.git", branch = "wip/sophie-h/gtk-rs-0.20" } async-channel = "2.1.0" +async-lock = "3.4.0" cairo-rs = { version = "*", features = [ "pdf", ], git = "https://github.com/gtk-rs/gtk-rs-core.git", branch = "master" } @@ -49,3 +50,6 @@ log = "0.4.17" once_cell = "1.16.0" strum = { version = "0.26.2", features = ["derive"] } url = "2.3.1" +gufo-common = { version = "0.1.0", git = "https://github.com/gufo-rs/gufo.git", features = [ + "serde", +] } diff --git a/build-aux/flatpak/org.gnome.Loupe.Devel.json b/build-aux/flatpak/org.gnome.Loupe.Devel.json index 74ad4fb..7168a14 100644 --- a/build-aux/flatpak/org.gnome.Loupe.Devel.json +++ b/build-aux/flatpak/org.gnome.Loupe.Devel.json @@ -152,11 +152,14 @@ { "name": "glycin", "buildsystem": "meson", + "config-opts": [ + "-Dlibglycin=false" + ], "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/sophie-h/glycin.git", - "commit": "4b5d7f06ae23bb8f5045ce069c687c3aa05c5f84" + "commit": "acdca33b6ba9541959fd9688697ac32617437c1b" } ] }, diff --git a/po/POTFILES.in b/po/POTFILES.in index e490fac..f257e9a 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -9,6 +9,7 @@ src/decoder/formats.rs src/decoder/formats/glycin_proxy.rs src/decoder/formats/svg.rs src/decoder/tiling.rs +src/editing.rs src/file_model.rs src/main.rs src/metadata.rs diff --git a/src/decoder/formats/glycin_proxy.rs b/src/decoder/formats/glycin_proxy.rs index 167cdc4..cf3a19b 100644 --- a/src/decoder/formats/glycin_proxy.rs +++ b/src/decoder/formats/glycin_proxy.rs @@ -50,6 +50,7 @@ impl Glycin { updater.clone().spawn_error_handled(async move { log::trace!("Setting up loader"); let mut loader = glycin::Loader::new(file); + loader.apply_transformations(false); #[cfg(feature = "disable-glycin-sandbox")] loader.sandbox_mechanism(Some(glycin::SandboxMechanism::NotSandboxed)); diff --git a/src/editing.rs b/src/editing.rs new file mode 100644 index 0000000..821b9d3 --- /dev/null +++ b/src/editing.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; +use glycin::{EditOutcome, Operations}; + +use crate::deps::*; +use crate::widgets::LpImage; + +/// Queue for image edit operations that still have to be applied. +/// +/// A queue is needed since further operations might be triggered while an +/// editing operation is in progress. +#[derive(Clone, Default, Debug)] +pub struct Queue { + operations: Arc>>, + /// Lock for in progress image operation + lock: Arc>, +} + +impl Queue { + pub fn push(&self, operation: glycin::Operation) { + self.operations.rcu(move |current_operations| { + let mut new = (**current_operations).clone(); + new.push(operation.clone()); + new + }); + } + + pub fn write_to_image(&self, image: &LpImage) { + let queue = self.clone(); + let image = image.clone(); + glib::spawn_future_local(async move { + let lock: async_lock::MutexGuard<()> = queue.lock.lock().await; + if let Err(err) = queue.apply(image).await { + log::error!("Err: {err}"); + } + drop(lock); + }); + } + + async fn apply(&self, image: LpImage) -> anyhow::Result<()> { + let file = image.file().unwrap(); + #[allow(unused_mut)] + let mut editor = glycin::Editor::new(file.clone()); + #[cfg(feature = "disable-glycin-sandbox")] + editor.sandbox_mechanism(Some(glycin::SandboxMechanism::NotSandboxed)); + + let mut list = Vec::new(); + self.operations.rcu(|current_operations| { + list.clone_from(&(**current_operations)); + Vec::new() + }); + + let operations = Operations::new(list); + let edit_instruction = editor.apply_sparse(operations).await?; + + if edit_instruction.apply_to(file).await? == EditOutcome::Unchanged { + log::error!("Writing new files is not supported yet"); + } + + Ok(()) + } +} diff --git a/src/main.rs b/src/main.rs index 3b9015a..b4c7ba9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -55,6 +55,7 @@ mod about; mod application; mod config; mod decoder; +mod editing; mod file_model; mod metadata; mod util; diff --git a/src/widgets/image.rs b/src/widgets/image.rs index fc21001..2ae3e26 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -135,11 +135,15 @@ pub enum FitMode { mod imp { use super::*; use crate::decoder::tiling::SharedFrameBuffer; + use crate::editing; #[derive(Debug, Default, Properties)] #[properties(wrapper_type = super::LpImage)] pub struct LpImage { pub(super) file: RefCell>, + /// Contains a file from which to reload the image after animation has + /// finished. + pub(super) queued_reload: RefCell>, #[property(get)] pub(super) is_deleted: Cell, #[property(get)] @@ -235,6 +239,9 @@ mod imp { /// Number of snapshots created, debug only pub(super) nth_snapshot: Cell, + + /// Editing queue + pub(super) editing_queue: editing::Queue, } #[glib::object_subclass] diff --git a/src/widgets/image/loading.rs b/src/widgets/image/loading.rs index 894a98e..109b095 100644 --- a/src/widgets/image/loading.rs +++ b/src/widgets/image/loading.rs @@ -283,6 +283,12 @@ impl LpImage { let imp = self.imp(); log::debug!("Loading file {}", file.uri()); + if imp.rotation_animation().state() == adw::AnimationState::Playing { + log::debug!("Queueing image reload due to playing rotate animation."); + self.imp().queued_reload.replace(Some(file.clone())); + return; + } + imp.metadata.replace(Metadata::default()); self.imp().emmit_metadata_changed(); self.imp().set_file_loaded(file).await; diff --git a/src/widgets/image/rotation.rs b/src/widgets/image/rotation.rs index 0fbc5db..5afea6b 100644 --- a/src/widgets/image/rotation.rs +++ b/src/widgets/image/rotation.rs @@ -46,11 +46,27 @@ impl imp::LpImage { pub(super) fn rotation_animation(&self) -> &adw::TimedAnimation { let obj = self.obj().to_owned(); self.rotation_animation.get_or_init(|| { - adw::TimedAnimation::builder() + let animation = adw::TimedAnimation::builder() .duration(ROTATION_ANIMATION_DURATION) .widget(&obj) .target(&adw::PropertyAnimationTarget::new(&obj, "rotation")) - .build() + .build(); + + animation.connect_done(glib::clone!( + #[weak] + obj, + move |_| { + let imp = obj.imp(); + if let Some(new_file) = imp.queued_reload.replace(None) { + glib::spawn_future_local(async move { + log::debug!("Animation finished: Executing delayed reload"); + obj.load(&new_file).await; + }); + } + } + )); + + animation }) } } @@ -58,6 +74,7 @@ impl imp::LpImage { impl LpImage { /// Set rotation and mirroring to the state would have after loading pub fn reset_rotation(&self) { + log::debug!("Resetting rotation"); let orientation = self.metadata().orientation(); self.imp().rotation_target.set(-orientation.rotation); self.set_mirrored(orientation.mirrored); @@ -84,5 +101,13 @@ impl LpImage { animation.set_value_to(imp.zoom_level_best_fit_for_rotation(target.get())); animation.play(); } + + if let Ok(r) = gufo_common::orientation::Rotation::try_from(angle) { + log::debug!("Editing image to rotate by {r:?}"); + let operation = glycin::Operation::Rotate(r); + let editing_queue = &self.imp().editing_queue; + editing_queue.push(operation); + editing_queue.write_to_image(self); + } } }