editor: Apply rotation to files

This commit is contained in:
Sophie Herold 2024-06-19 00:36:54 +02:00
parent 4164fa61c3
commit afcc24e235
10 changed files with 116 additions and 3 deletions

2
Cargo.lock generated
View file

@ -1584,6 +1584,7 @@ dependencies = [
"arc-swap", "arc-swap",
"ashpd", "ashpd",
"async-channel", "async-channel",
"async-lock",
"cairo-rs", "cairo-rs",
"env_logger", "env_logger",
"futures-channel", "futures-channel",
@ -1593,6 +1594,7 @@ dependencies = [
"gio 0.20.0", "gio 0.20.0",
"glycin", "glycin",
"gtk4", "gtk4",
"gufo-common",
"gvdb-macros", "gvdb-macros",
"indexmap", "indexmap",
"kamadak-exif", "kamadak-exif",

View file

@ -23,6 +23,7 @@ ashpd = { version = "0.9.0", features = [
"gtk4_wayland", "gtk4_wayland",
], git = "https://github.com/sophie-h/ashpd.git", branch = "wip/sophie-h/gtk-rs-0.20" } ], git = "https://github.com/sophie-h/ashpd.git", branch = "wip/sophie-h/gtk-rs-0.20" }
async-channel = "2.1.0" async-channel = "2.1.0"
async-lock = "3.4.0"
cairo-rs = { version = "*", features = [ cairo-rs = { version = "*", features = [
"pdf", "pdf",
], git = "https://github.com/gtk-rs/gtk-rs-core.git", branch = "master" } ], 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" once_cell = "1.16.0"
strum = { version = "0.26.2", features = ["derive"] } strum = { version = "0.26.2", features = ["derive"] }
url = "2.3.1" url = "2.3.1"
gufo-common = { version = "0.1.0", git = "https://github.com/gufo-rs/gufo.git", features = [
"serde",
] }

View file

@ -152,11 +152,14 @@
{ {
"name": "glycin", "name": "glycin",
"buildsystem": "meson", "buildsystem": "meson",
"config-opts": [
"-Dlibglycin=false"
],
"sources": [ "sources": [
{ {
"type": "git", "type": "git",
"url": "https://gitlab.gnome.org/sophie-h/glycin.git", "url": "https://gitlab.gnome.org/sophie-h/glycin.git",
"commit": "4b5d7f06ae23bb8f5045ce069c687c3aa05c5f84" "commit": "acdca33b6ba9541959fd9688697ac32617437c1b"
} }
] ]
}, },

View file

@ -9,6 +9,7 @@ src/decoder/formats.rs
src/decoder/formats/glycin_proxy.rs src/decoder/formats/glycin_proxy.rs
src/decoder/formats/svg.rs src/decoder/formats/svg.rs
src/decoder/tiling.rs src/decoder/tiling.rs
src/editing.rs
src/file_model.rs src/file_model.rs
src/main.rs src/main.rs
src/metadata.rs src/metadata.rs

View file

@ -50,6 +50,7 @@ impl Glycin {
updater.clone().spawn_error_handled(async move { updater.clone().spawn_error_handled(async move {
log::trace!("Setting up loader"); log::trace!("Setting up loader");
let mut loader = glycin::Loader::new(file); let mut loader = glycin::Loader::new(file);
loader.apply_transformations(false);
#[cfg(feature = "disable-glycin-sandbox")] #[cfg(feature = "disable-glycin-sandbox")]
loader.sandbox_mechanism(Some(glycin::SandboxMechanism::NotSandboxed)); loader.sandbox_mechanism(Some(glycin::SandboxMechanism::NotSandboxed));

63
src/editing.rs Normal file
View file

@ -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<ArcSwap<Vec<glycin::Operation>>>,
/// Lock for in progress image operation
lock: Arc<async_lock::Mutex<()>>,
}
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(())
}
}

View file

@ -55,6 +55,7 @@ mod about;
mod application; mod application;
mod config; mod config;
mod decoder; mod decoder;
mod editing;
mod file_model; mod file_model;
mod metadata; mod metadata;
mod util; mod util;

View file

@ -135,11 +135,15 @@ pub enum FitMode {
mod imp { mod imp {
use super::*; use super::*;
use crate::decoder::tiling::SharedFrameBuffer; use crate::decoder::tiling::SharedFrameBuffer;
use crate::editing;
#[derive(Debug, Default, Properties)] #[derive(Debug, Default, Properties)]
#[properties(wrapper_type = super::LpImage)] #[properties(wrapper_type = super::LpImage)]
pub struct LpImage { pub struct LpImage {
pub(super) file: RefCell<Option<gio::File>>, pub(super) file: RefCell<Option<gio::File>>,
/// Contains a file from which to reload the image after animation has
/// finished.
pub(super) queued_reload: RefCell<Option<gio::File>>,
#[property(get)] #[property(get)]
pub(super) is_deleted: Cell<bool>, pub(super) is_deleted: Cell<bool>,
#[property(get)] #[property(get)]
@ -235,6 +239,9 @@ mod imp {
/// Number of snapshots created, debug only /// Number of snapshots created, debug only
pub(super) nth_snapshot: Cell<u8>, pub(super) nth_snapshot: Cell<u8>,
/// Editing queue
pub(super) editing_queue: editing::Queue,
} }
#[glib::object_subclass] #[glib::object_subclass]

View file

@ -283,6 +283,12 @@ impl LpImage {
let imp = self.imp(); let imp = self.imp();
log::debug!("Loading file {}", file.uri()); 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()); imp.metadata.replace(Metadata::default());
self.imp().emmit_metadata_changed(); self.imp().emmit_metadata_changed();
self.imp().set_file_loaded(file).await; self.imp().set_file_loaded(file).await;

View file

@ -46,11 +46,27 @@ impl imp::LpImage {
pub(super) fn rotation_animation(&self) -> &adw::TimedAnimation { pub(super) fn rotation_animation(&self) -> &adw::TimedAnimation {
let obj = self.obj().to_owned(); let obj = self.obj().to_owned();
self.rotation_animation.get_or_init(|| { self.rotation_animation.get_or_init(|| {
adw::TimedAnimation::builder() let animation = adw::TimedAnimation::builder()
.duration(ROTATION_ANIMATION_DURATION) .duration(ROTATION_ANIMATION_DURATION)
.widget(&obj) .widget(&obj)
.target(&adw::PropertyAnimationTarget::new(&obj, "rotation")) .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 { impl LpImage {
/// Set rotation and mirroring to the state would have after loading /// Set rotation and mirroring to the state would have after loading
pub fn reset_rotation(&self) { pub fn reset_rotation(&self) {
log::debug!("Resetting rotation");
let orientation = self.metadata().orientation(); let orientation = self.metadata().orientation();
self.imp().rotation_target.set(-orientation.rotation); self.imp().rotation_target.set(-orientation.rotation);
self.set_mirrored(orientation.mirrored); 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.set_value_to(imp.zoom_level_best_fit_for_rotation(target.get()));
animation.play(); 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);
}
} }
} }