From c0f435386d69a882af6d7e8ea455f278aac52a15 Mon Sep 17 00:00:00 2001 From: Sophie Herold Date: Mon, 4 Dec 2023 12:52:40 +0100 Subject: [PATCH] metadata: Move file info and format to metadata --- src/decoder/formats/glycin_proxy.rs | 7 +- src/decoder/formats/mod.rs | 29 ------ src/decoder/formats/svg.rs | 1 + src/decoder/mod.rs | 41 +++----- src/file_model.rs | 2 +- src/image_metadata/obj.rs | 127 ------------------------ src/main.rs | 2 +- src/metadata/file.rs | 38 +++++++ src/metadata/format.rs | 28 ++++++ src/{image_metadata => metadata}/gps.rs | 0 src/{image_metadata => metadata}/mod.rs | 126 ++++++++++++++++------- src/widgets/image.rs | 113 ++++++++++++--------- src/widgets/print.rs | 2 +- src/widgets/properties_view.rs | 90 ++++++++--------- src/widgets/properties_view.ui | 12 ++- src/window.rs | 48 +++++---- 16 files changed, 327 insertions(+), 339 deletions(-) delete mode 100644 src/image_metadata/obj.rs create mode 100644 src/metadata/file.rs create mode 100644 src/metadata/format.rs rename src/{image_metadata => metadata}/gps.rs (100%) rename src/{image_metadata => metadata}/mod.rs (77%) diff --git a/src/decoder/formats/glycin_proxy.rs b/src/decoder/formats/glycin_proxy.rs index 8893f05..b48ffb1 100644 --- a/src/decoder/formats/glycin_proxy.rs +++ b/src/decoder/formats/glycin_proxy.rs @@ -19,7 +19,7 @@ use super::*; use crate::decoder::tiling::{self, FrameBufferExt}; use crate::deps::*; -use crate::image_metadata::ImageMetadata; +use crate::metadata::{ImageFormat, Metadata}; use arc_swap::ArcSwap; use gtk::prelude::*; @@ -62,8 +62,9 @@ impl Glycin { let image = image_request.request().await?; if let Some(exif_raw) = image.info().exif.clone().into() { - let mut metadata = ImageMetadata::from_exif_bytes(exif_raw); - metadata.heif_transform = image.info().transformations_applied; + let mut metadata = Metadata::default(); + metadata.set_exif_bytes(exif_raw); + metadata.set_transformations_applied(image.info().transformations_applied); updater.send(DecoderUpdate::Metadata(metadata)); } diff --git a/src/decoder/formats/mod.rs b/src/decoder/formats/mod.rs index c6779ff..4ab5b32 100644 --- a/src/decoder/formats/mod.rs +++ b/src/decoder/formats/mod.rs @@ -23,35 +23,6 @@ pub use svg::{Svg, RSVG_MAX_SIZE}; use super::{DecoderUpdate, UpdateSender}; -#[derive(Clone, Debug)] -pub struct ImageFormat { - mime_type: glycin::MimeType, - name: String, -} - -impl ImageFormat { - pub fn new(mime_type: glycin::MimeType, name: String) -> Self { - Self { mime_type, name } - } - - pub fn is_svg(&self) -> bool { - matches!( - self.mime_type.as_str(), - "image/svg+xml" | "image/svg+xml-compressed" - ) - } - - pub fn is_potentially_transparent(&self) -> bool { - !matches!(self.mime_type.as_str(), "image/bmp" | "image/jpeg") - } -} - -impl std::fmt::Display for ImageFormat { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - write!(f, "{}", self.name) - } -} - #[derive(Clone, Debug, Default)] pub enum ImageDimensionDetails { Svg(String), diff --git a/src/decoder/formats/svg.rs b/src/decoder/formats/svg.rs index a8a817f..f861bc4 100644 --- a/src/decoder/formats/svg.rs +++ b/src/decoder/formats/svg.rs @@ -20,6 +20,7 @@ use super::*; use crate::decoder::tiling::{self, FrameBufferExt}; use crate::decoder::TileRequest; use crate::deps::*; +use crate::metadata::ImageFormat; use anyhow::Context; use arc_swap::ArcSwap; diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index ff8e8be..52fabde 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -23,18 +23,17 @@ pub mod tiling; pub use formats::ImageDimensionDetails; use crate::deps::*; -use crate::image_metadata::ImageMetadata; +use crate::metadata::{ImageFormat, Metadata}; use formats::*; use tiling::FrameBufferExt; use anyhow::Result; use arc_swap::ArcSwap; use futures::channel::mpsc; -use gio::prelude::*; use std::sync::Arc; -pub use formats::{ImageFormat, RSVG_MAX_SIZE}; +pub use formats::RSVG_MAX_SIZE; #[derive(Clone, Copy, Debug)] /// Renderer requests new tiles @@ -58,7 +57,7 @@ pub enum DecoderUpdate { /// Dimensions of image in `TilingSore` available/updated Dimensions(ImageDimensionDetails), /// Metadata available - Metadata(ImageMetadata), + Metadata(Metadata), /// Image format determined Format(ImageFormat), /// Image format not supported or unknown @@ -120,6 +119,7 @@ impl Decoder { /// The renderer should listen to updates from the returned receiver. pub async fn new( file: gio::File, + mime_type: Option, tiles: Arc>, ) -> (Self, mpsc::UnboundedReceiver) { let (sender, receiver) = mpsc::unbounded(); @@ -127,7 +127,7 @@ impl Decoder { let update_sender = UpdateSender { sender }; tiles.set_update_sender(update_sender.clone()); - let format_decoder = Self::format_decoder(update_sender.clone(), file, tiles); + let format_decoder = Self::format_decoder(update_sender.clone(), file, mime_type, tiles); let decoder = Self { decoder: format_decoder, update_sender, @@ -139,28 +139,19 @@ impl Decoder { fn format_decoder( update_sender: UpdateSender, file: gio::File, + mime_type: Option, tiles: Arc>, ) -> FormatDecoder { - if let Ok(file_info) = file.query_info( - gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, - gio::FileQueryInfoFlags::NONE, - gio::Cancellable::NONE, - ) { - if let Some(mime_type) = file_info - .content_type() - .as_ref() - .and_then(|x| gio::content_type_get_mime_type(x)) - { - // Known things we want to match here are - // - image/svg+xml - // - image/svg+xml-compressed - if mime_type.split('+').next() == Some("image/svg") { - update_sender.send(DecoderUpdate::Format(ImageFormat::new( - mime_type.into(), - "SVG".into(), - ))); - return FormatDecoder::Svg(Svg::new(file, update_sender, tiles)); - } + if let Some(mime_type) = mime_type { + // Known things we want to match here are + // - image/svg+xml + // - image/svg+xml-compressed + if mime_type.split('+').next() == Some("image/svg") { + update_sender.send(DecoderUpdate::Format(ImageFormat::new( + mime_type, + "SVG".into(), + ))); + return FormatDecoder::Svg(Svg::new(file, update_sender, tiles)); } } diff --git a/src/file_model.rs b/src/file_model.rs index 194da07..553c963 100644 --- a/src/file_model.rs +++ b/src/file_model.rs @@ -25,6 +25,7 @@ use crate::util::gettext::*; use anyhow::Context; use gio::prelude::*; use gio::subclass::prelude::*; +use glib::subclass::Signal; use glib::GString; use indexmap::IndexMap; use once_cell::sync::Lazy; @@ -32,7 +33,6 @@ use std::cell::{OnceCell, RefCell}; mod imp { use super::*; - use gtk::glib::subclass::Signal; #[derive(Debug, Default)] pub struct LpFileModel { diff --git a/src/image_metadata/obj.rs b/src/image_metadata/obj.rs deleted file mode 100644 index 16e7b06..0000000 --- a/src/image_metadata/obj.rs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) 2022-2023 Sophie Herold -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// -// SPDX-License-Identifier: GPL-3.0-or-later - -use gio::prelude::*; - -use super::{GPSLocation, ImageMetadata, Orientation}; - -use glib::subclass::prelude::*; -use gtk::glib; -use once_cell::sync::Lazy; - -use std::cell::RefCell; - -glib::wrapper! { - pub struct LpImageMetadata(ObjectSubclass); -} - -impl LpImageMetadata { - pub fn load(file: &gio::File) -> Self { - let obj = glib::Object::new::(); - - let metadata = ImageMetadata::load(file); - obj.imp().metadata.replace(metadata); - - obj - } - - pub fn orientation(&self) -> Orientation { - self.imp().metadata.borrow().orientation() - } - - pub fn gps_location(&self) -> Option { - self.imp().metadata.borrow().gps_location() - } - - pub fn src(&self) -> std::cell::Ref { - self.imp().metadata.borrow() - } -} - -impl From for LpImageMetadata { - fn from(metadata: ImageMetadata) -> Self { - let obj = glib::Object::new::(); - - obj.imp().metadata.replace(metadata); - - obj - } -} - -impl Default for LpImageMetadata { - fn default() -> Self { - glib::Object::new() - } -} - -mod imp { - use super::*; - - #[derive(Default)] - pub struct LpImageMetadata { - pub(super) metadata: RefCell, - } - - #[glib::object_subclass] - impl ObjectSubclass for LpImageMetadata { - const NAME: &'static str = "LpImageMetadata"; - type Type = super::LpImageMetadata; - type ParentType = glib::Object; - } - - impl ObjectImpl for LpImageMetadata { - fn properties() -> &'static [glib::ParamSpec] { - static PROPERTIES: Lazy> = Lazy::new(|| { - let mut vec = vec![glib::ParamSpecBoolean::builder("has-information").build()]; - - vec.append( - &mut [ - "originally-created", - "location", - "f-number", - "exposure-time", - "iso", - "focal-length", - "maker-model", - ] - .iter() - .map(|name| glib::ParamSpecString::builder(name).read_only().build()) - .collect(), - ); - - vec - }); - - PROPERTIES.as_ref() - } - - fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { - let metadata = self.metadata.borrow(); - match pspec.name() { - "has-information" => (metadata.has_information()).to_value(), - "originally-created" => metadata.originally_created().to_value(), - "location" => metadata.gps_location().map(|x| x.display()).to_value(), - "f-number" => metadata.f_number().to_value(), - "exposure-time" => metadata.exposure_time().to_value(), - "iso" => metadata.iso().to_value(), - "focal-length" => metadata.focal_length().to_value(), - "maker-model" => metadata.maker_model().to_value(), - name => unimplemented!("property {name}"), - } - } - } -} diff --git a/src/main.rs b/src/main.rs index 20bd7ad..bea9d88 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,7 +54,7 @@ mod application; mod config; mod decoder; mod file_model; -mod image_metadata; +mod metadata; mod util; mod widgets; mod window; diff --git a/src/metadata/file.rs b/src/metadata/file.rs new file mode 100644 index 0000000..78d2b40 --- /dev/null +++ b/src/metadata/file.rs @@ -0,0 +1,38 @@ +use crate::deps::*; + +pub struct FileInfo { + pub(super) display_name: glib::GString, + pub(super) file_size: Option, + pub(super) mime_type: Option, + pub(super) created: Option, + pub(super) modified: Option, +} + +impl FileInfo { + pub async fn new(file: &gio::File) -> anyhow::Result { + let file_info = crate::util::query_attributes_future( + file, + vec![ + gio::FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME, + gio::FILE_ATTRIBUTE_STANDARD_SIZE, + gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + gio::FILE_ATTRIBUTE_TIME_CREATED, + gio::FILE_ATTRIBUTE_TIME_MODIFIED, + ], + ) + .await?; + + let size = file_info.size(); + let file_size = if size > 0 { Some(size as u64) } else { None }; + + Ok(Self { + display_name: file_info.display_name(), + file_size, + mime_type: file_info + .content_type() + .and_then(|x| gio::content_type_get_mime_type(&x)), + created: file_info.creation_date_time(), + modified: file_info.modification_date_time(), + }) + } +} diff --git a/src/metadata/format.rs b/src/metadata/format.rs new file mode 100644 index 0000000..a6d001f --- /dev/null +++ b/src/metadata/format.rs @@ -0,0 +1,28 @@ +#[derive(Clone, Debug)] +pub struct ImageFormat { + mime_type: glycin::MimeType, + name: String, +} + +impl ImageFormat { + pub fn new(mime_type: glycin::MimeType, name: String) -> Self { + Self { mime_type, name } + } + + pub fn is_svg(&self) -> bool { + matches!( + self.mime_type.as_str(), + "image/svg+xml" | "image/svg+xml-compressed" + ) + } + + pub fn is_potentially_transparent(&self) -> bool { + !matches!(self.mime_type.as_str(), "image/bmp" | "image/jpeg") + } +} + +impl std::fmt::Display for ImageFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!(f, "{}", self.name) + } +} diff --git a/src/image_metadata/gps.rs b/src/metadata/gps.rs similarity index 100% rename from src/image_metadata/gps.rs rename to src/metadata/gps.rs diff --git a/src/image_metadata/mod.rs b/src/metadata/mod.rs similarity index 77% rename from src/image_metadata/mod.rs rename to src/metadata/mod.rs index a96076e..30a3d61 100644 --- a/src/image_metadata/mod.rs +++ b/src/metadata/mod.rs @@ -21,25 +21,28 @@ - */ +mod file; +mod format; mod gps; -mod obj; +pub use file::FileInfo; +pub use format::ImageFormat; pub use gps::GPSLocation; -pub use obj::LpImageMetadata; +use crate::util; use crate::util::gettext::*; -use crate::util::{self, ToBufRead}; use crate::deps::*; -use gio::prelude::*; #[derive(Default)] -pub struct ImageMetadata { - pub exif: Option, - pub heif_transform: bool, +pub struct Metadata { + exif: Option, + file_info: Option, + format: Option, + transformations_applied: Option, } -impl std::fmt::Debug for ImageMetadata { +impl std::fmt::Debug for Metadata { fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { if let Some(exif) = &self.exif { let list = exif.fields().map(|f| { @@ -59,43 +62,65 @@ impl std::fmt::Debug for ImageMetadata { } } -impl ImageMetadata { - pub fn load(file: &gio::File) -> Self { - log::debug!("Loading metadata for {}", file.uri()); - // TODO: make possible to cancel - if let Ok(mut bufreader) = file.to_buf_read(&gio::Cancellable::new()) { - let exifreader = exif::Reader::new(); - - let exif = exifreader.read_from_container(&mut bufreader).ok(); - return Self { - exif, - ..Default::default() - }; - } - - Self::default() - } - - pub fn from_exif_bytes(bytes: Vec) -> Self { +impl Metadata { + pub fn set_exif_bytes(&mut self, bytes: Vec) { let reader = exif::Reader::new(); - match reader.read_raw(bytes) { - Ok(exif) => Self { - exif: Some(exif), - ..Default::default() - }, - Err(err) => { - log::warn!("Failed to decoder EXIF bytes: {err}"); - Self::default() - } + let exif = reader.read_raw(bytes); + + if let Err(err) = &exif { + log::warn!("Failed to decode EXIF bytes: {err}"); + } + + self.exif = exif.ok(); + } + + pub fn set_transformations_applied(&mut self, applied: bool) { + self.transformations_applied = Some(applied); + } + + pub fn set_file_info(&mut self, file_info: FileInfo) { + self.file_info = Some(file_info); + } + + pub fn set_format(&mut self, format: ImageFormat) { + self.format = Some(format); + } + + pub fn merge(&mut self, other: Self) { + if self.exif.is_none() { + self.exif = other.exif; + } + if self.file_info.is_none() { + self.file_info = other.file_info; + } + if self.transformations_applied.is_none() { + self.transformations_applied = other.transformations_applied; } } - pub fn has_information(&self) -> bool { - self.exif.is_some() + pub fn mime_type(&self) -> Option { + self.file_info + .as_ref() + .and_then(|x| x.mime_type.as_ref()) + .map(|x| x.to_string()) + } + + pub fn format(&self) -> Option<&ImageFormat> { + self.format.as_ref() + } + + pub fn format_name(&self) -> Option { + self.format() + .map(|x| x.to_string()) + .or_else(|| self.mime_type()) + } + + pub fn transformations_applied(&self) -> bool { + self.transformations_applied.unwrap_or(false) } pub fn orientation(&self) -> Orientation { - if self.heif_transform { + if self.transformations_applied() { // HEIF library already does it's transformations on its own Orientation::default() } else if let Some(exif) = &self.exif { @@ -112,6 +137,31 @@ impl ImageMetadata { } } + pub fn file_name(&self) -> Option { + self.file_info.as_ref().map(|x| x.display_name.to_string()) + } + + pub fn file_size(&self) -> Option { + self.file_info + .as_ref() + .and_then(|x| x.file_size) + .map(|x| glib::format_size(x).to_string()) + } + + pub fn file_created(&self) -> Option { + self.file_info + .as_ref() + .and_then(|x| x.created.as_ref()) + .and_then(util::datetime_fmt) + } + + pub fn file_modified(&self) -> Option { + self.file_info + .as_ref() + .and_then(|x| x.modified.as_ref()) + .and_then(util::datetime_fmt) + } + pub fn originally_created(&self) -> Option { if let Some(exif) = &self.exif { if let Some(field) = exif diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 9caab17..95171bf 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -34,17 +34,19 @@ use crate::deps::*; use crate::decoder::tiling::FrameBufferExt; use crate::decoder::{self, tiling, Decoder, DecoderUpdate}; -use crate::image_metadata::LpImageMetadata; +use crate::metadata::{self, Metadata}; use crate::util::Gesture; use adw::prelude::*; use adw::subclass::prelude::*; use arc_swap::ArcSwap; use futures::prelude::*; +use glib::subclass::Signal; use once_cell::sync::Lazy; use std::cell::{Cell, OnceCell, RefCell}; use std::marker::PhantomData; +use std::ops::Deref; use std::sync::Arc; /// Default background color around images and behind transparent images @@ -123,8 +125,7 @@ mod imp { /// Set if an error has occurred, shown on error_page #[property(get)] pub(super) error: RefCell>, - #[property(name = "format-name", type = Option, get = Self::format_name)] - pub(super) format: RefCell>, + //pub(super) format: RefCell>, pub(super) background_color: RefCell>, /// Track changes to this image @@ -177,8 +178,7 @@ mod imp { pub(super) _vscroll_policy: PhantomData, /// Currently EXIF data - #[property(name = "metadata", type=LpImageMetadata, get)] - pub(super) image_metadata: RefCell, + pub(super) metadata: RefCell, /// Image dimension details for SVGs pub(super) dimension_details: RefCell, @@ -229,6 +229,12 @@ mod imp { self.derived_property(id, pspec) } + fn signals() -> &'static [Signal] { + static SIGNALS: Lazy> = + Lazy::new(|| vec![Signal::builder("metadata-changed").build()]); + SIGNALS.as_ref() + } + fn constructed(&self) { self.parent_constructed(); @@ -604,11 +610,6 @@ mod imp { pub fn scroll_policy(&self) -> gtk::ScrollablePolicy { gtk::ScrollablePolicy::Minimum } - - /// Image format displayable name - pub fn format_name(&self) -> Option { - self.format.borrow().as_ref().map(|x| x.to_string()) - } } impl WidgetImpl for LpImage { @@ -651,18 +652,18 @@ mod imp { // called when the widget content should be re-rendered fn snapshot(&self, snapshot: >k::Snapshot) { - let widget = self.obj(); - let widget_width = widget.width() as f64; - let widget_height = widget.height() as f64; - let display_width = widget.image_displayed_width(); - let display_height = widget.image_displayed_height(); + let obj = self.obj(); + let widget_width = obj.width() as f64; + let widget_height = obj.height() as f64; + let display_width = obj.image_displayed_width(); + let display_height = obj.image_displayed_height(); // make sure the scrollbars are correct - widget.configure_adjustments(); + obj.configure_adjustments(); - let applicable_zoom = widget.applicable_zoom(); + let applicable_zoom = obj.applicable_zoom(); - let scaling_filter = if self.format.borrow().as_ref().map_or(false, |x| x.is_svg()) { + let scaling_filter = if obj.metadata().format().map_or(false, |x| x.is_svg()) { // Looks better in SVG animations and avoids rendering issues gsk::ScalingFilter::Linear } else if applicable_zoom < 1. { @@ -675,8 +676,8 @@ mod imp { let render_options = tiling::RenderOptions { scaling_filter, - scale_factor: widget.scale_factor(), - background_color: Some(widget.background_color()), + scale_factor: obj.scale_factor(), + background_color: Some(obj.background_color()), }; // Operations on snapshots are coordinate transformations @@ -685,34 +686,34 @@ mod imp { // Add background snapshot.append_color( - &widget.background_color(), + &obj.background_color(), &graphene::Rect::new(0., 0., widget_width as f32, widget_height as f32), ); // Apply the scrolling position to the image - let hadj: gtk::Adjustment = widget.hadjustment(); + let hadj: gtk::Adjustment = obj.hadjustment(); let x = -(hadj.value() - (hadj.upper() - display_width) / 2.); - snapshot.translate(&graphene::Point::new(widget.round_f64(x) as f32, 0.)); + snapshot.translate(&graphene::Point::new(obj.round_f64(x) as f32, 0.)); - let vadj = widget.vadjustment(); + let vadj = obj.vadjustment(); let y = -(vadj.value() - (vadj.upper() - display_height) / 2.); - snapshot.translate(&graphene::Point::new(0., widget.round_f64(y) as f32)); + snapshot.translate(&graphene::Point::new(0., obj.round_f64(y) as f32)); // Centering in widget when no scrolling (black bars around image) - let x = widget.round_f64(f64::max((widget_width - display_width) / 2.0, 0.)); - let y = widget.round_f64(f64::max((widget_height - display_height) / 2.0, 0.)); + let x = obj.round_f64(f64::max((widget_width - display_width) / 2.0, 0.)); + let y = obj.round_f64(f64::max((widget_height - display_height) / 2.0, 0.)); // Round to pixel values to not have a half pixel offset to physical pixels // The offset would leading to a blurry output snapshot.translate(&graphene::Point::new( - widget.round_f64(x) as f32, - widget.round_f64(y) as f32, + obj.round_f64(x) as f32, + obj.round_f64(y) as f32, )); // Apply rotation and mirroring - widget.snapshot_rotate_mirror( + obj.snapshot_rotate_mirror( snapshot, - widget.rotation() as f32, - widget.mirrored(), + obj.rotation() as f32, + obj.mirrored(), applicable_zoom, ); @@ -813,9 +814,19 @@ glib::wrapper! { impl LpImage { pub async fn load(&self, file: &gio::File) { + let imp = self.imp(); log::debug!("Loading file {}", file.uri()); self.set_file(file); + imp.metadata.replace(Metadata::default()); + self.emmit_metadata_changed(); + + let file_info = metadata::FileInfo::new(file).await; + match file_info { + Ok(file_info) => imp.metadata.borrow_mut().set_file_info(file_info), + Err(err) => log::warn!("Failed to load file information: {err}"), + } + self.emmit_metadata_changed(); let tiles = &self.imp().frame_buffer; // Delete all stored textures for reloads @@ -823,7 +834,8 @@ impl LpImage { // Reset background color for reloads self.set_background_color(None); - let (decoder, mut decoder_update) = Decoder::new(file.clone(), tiles.clone()).await; + let (decoder, mut decoder_update) = + Decoder::new(file.clone(), self.metadata().mime_type(), tiles.clone()).await; let weak_obj = self.downgrade(); glib::spawn_future_local(async move { @@ -835,7 +847,7 @@ impl LpImage { log::debug!("Stopped listening to decoder since sender is gone"); }); - self.imp().decoder.replace(Some(Arc::new(decoder))); + imp.decoder.replace(Some(Arc::new(decoder))); } /// Called when decoder sends update @@ -845,8 +857,8 @@ impl LpImage { match update { DecoderUpdate::Metadata(metadata) => { log::debug!("Received metadata"); - imp.image_metadata.replace(LpImageMetadata::from(metadata)); - self.notify_metadata(); + imp.metadata.borrow_mut().merge(metadata); + self.emmit_metadata_changed(); self.reset_rotation(); } @@ -880,8 +892,8 @@ impl LpImage { self.set_error(Some(err)); } DecoderUpdate::Format(format) => { - imp.format.replace(Some(format)); - self.notify_format_name(); + imp.metadata.borrow_mut().set_format(format); + self.emmit_metadata_changed(); } DecoderUpdate::Animated => { let callback_id = self @@ -1117,7 +1129,7 @@ impl LpImage { /// Maximal zoom allowed for this image fn max_zoom(&self) -> f64 { - if self.format().map_or(false, |x| x.is_svg()) { + if self.metadata().format().map_or(false, |x| x.is_svg()) { let (width, height) = self.original_dimensions(); // Avoid division by 0 let long_side = f64::max(1., i32::max(width, height) as f64); @@ -1392,7 +1404,7 @@ impl LpImage { /// Image size of original image with EXIF rotation applied pub fn image_size(&self) -> (i32, i32) { - let orientation = self.imp().image_metadata.borrow().orientation(); + let orientation = self.imp().metadata.borrow().orientation(); if orientation.rotation.abs() == 90. || orientation.rotation.abs() == 270. { let (x, y) = self.original_dimensions(); (y, x) @@ -1599,11 +1611,6 @@ impl LpImage { Some(gdk::ContentProvider::for_value(&list.to_value())) } - /// Image format - pub fn format(&self) -> Option { - self.imp().format.borrow().clone() - } - fn set_error(&self, err: Option) { log::debug!("Decoding error: {err:?}"); self.imp() @@ -1696,6 +1703,7 @@ impl LpImage { pub async fn background_color_guess(&self) -> Option { // Shortcut for formats that don't support transparency if !self + .metadata() .format() .map_or(true, |x| x.is_potentially_transparent()) { @@ -1809,4 +1817,19 @@ impl LpImage { .await .ok()? } + + pub fn metadata(&self) -> impl Deref + '_ { + self.imp().metadata.borrow() + } + + pub fn connect_changed(&self, f: impl Fn() + 'static) { + self.connect_local("metadata-changed", false, move |_| { + f(); + None + }); + } + + fn emmit_metadata_changed(&self) { + self.emit_by_name::<()>("metadata-changed", &[]); + } } diff --git a/src/widgets/print.rs b/src/widgets/print.rs index b0f862c..6c0c762 100644 --- a/src/widgets/print.rs +++ b/src/widgets/print.rs @@ -856,7 +856,7 @@ impl LpPrint { let texture_scale = self.user_width() / orig_width as f64; let cairo_scale = cairo_dpi / self.dpi(); - let texture = if image.format().map_or(false, |x| x.is_svg()) { + let texture = if image.metadata().format().map_or(false, |x| x.is_svg()) { // Render SVG to exact needed sizes // TODO: This should be async decoder::formats::Svg::render_print( diff --git a/src/widgets/properties_view.rs b/src/widgets/properties_view.rs index 510d458..bbb5abf 100644 --- a/src/widgets/properties_view.rs +++ b/src/widgets/properties_view.rs @@ -44,6 +44,8 @@ mod imp { #[template_child] folder: TemplateChild, + #[template_child] + uri: TemplateChild, #[template_child] image_size: TemplateChild, @@ -52,6 +54,8 @@ mod imp { #[template_child] file_size: TemplateChild, + #[template_child] + dates: TemplateChild, #[template_child] file_created: TemplateChild, #[template_child] @@ -108,11 +112,13 @@ mod imp { image_signals.connect_notify_local( Some("image-size-available"), - glib::clone!(@weak obj => move |_,_| obj.imp().update()), + glib::clone!(@weak obj => move |_, _| obj.imp().update()), ); - image_signals.connect_notify_local( - Some("metadata"), - glib::clone!(@weak obj => move |_,_| obj.imp().update()), + + image_signals.connect_local( + "metadata-changed", + false, + glib::clone!(@weak obj => @default-return None, move |_| { obj.imp().update(); None }), ); self.update(); @@ -156,28 +162,43 @@ mod imp { return; }; + let metadata = image.metadata(); + // Folder - Self::update_row(&self.folder, self.folder_name()); + let has_folder = Self::update_row(&self.folder, self.folder_name()); + // The portal only supports opening folders of files that have a path obj.action_set_enabled("properties.open-folder", file.path().is_some()); + // Only show URI if now folder available + let uri = if !has_folder { + image.file().map(|x| x.uri()) + } else { + None + }; + Self::update_row(&self.uri, uri); // Image info Self::update_row(&self.image_size, self.image_size()); - Self::update_row(&self.image_format, image.format().map(|x| x.to_string())); - //Self::update_row(&self.file_size, self.image_size()); + Self::update_row(&self.image_format, metadata.format_name()); + Self::update_row(&self.file_size, metadata.file_size()); - // Sizes + // Dates + let has_dates = [ + Self::update_row(&self.file_created, metadata.file_created()), + Self::update_row(&self.file_modified, dbg!(metadata.file_modified())), + ] + .into_iter() + .any(|x| x); + self.dates.set_visible(has_dates); - // Details - let metadata = image.metadata(); - let meta = metadata.src(); + // Details (EXIF) let has_details = [ - Self::update_row(&self.location, meta.gps_location().map(|x| x.display())), - Self::update_row(&self.originally_created, meta.originally_created()), - Self::update_row(&self.aperture, meta.f_number()), - Self::update_row(&self.exposure, meta.exposure_time()), - Self::update_row(&self.iso, meta.iso()), - Self::update_row(&self.focal_length, meta.focal_length()), - Self::update_row(&self.maker_model, meta.maker_model()), + Self::update_row(&self.location, metadata.gps_location().map(|x| x.display())), + Self::update_row(&self.originally_created, metadata.originally_created()), + Self::update_row(&self.aperture, metadata.f_number()), + Self::update_row(&self.exposure, metadata.exposure_time()), + Self::update_row(&self.iso, metadata.iso()), + Self::update_row(&self.focal_length, metadata.focal_length()), + Self::update_row(&self.maker_model, metadata.maker_model()), ] .iter() .any(|x| *x); @@ -211,7 +232,7 @@ mod imp { _ => { let (width, height) = image.image_size(); if width > 0 && height > 0 { - return Some(format!("{width}\u{202F}\u{D7}\u{202F}{height}")); + Some(format!("{width}\u{202F}\u{D7}\u{202F}{height}")) } else { None } @@ -265,34 +286,3 @@ glib::wrapper! { @extends gtk::Widget, adw::Bin, @implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable; } - -impl LpPropertiesView { - /* fn set_file(&self, file: Option<&gio::File>) { - let imp = self.imp(); - - self.action_set_enabled( - "properties.open-folder", - file.map_or(false, |x| x.is_native()), - ); - - if let Some(file) = file { - if let Some(current_file) = self.file() { - if current_file.equal(file) { - return; - } - } - - // Cancel the file info future. See `build_file_info()` for details. - if let Some(handle) = imp.info_handle.take() { - handle.abort(); - } - - self.build_file_info(file); - } - - let weak = file.map(|x| x.downgrade()); - imp.file.replace(weak); - self.notify("file"); - } - */ -} diff --git a/src/widgets/properties_view.ui b/src/widgets/properties_view.ui index de7499d..e8feaaf 100644 --- a/src/widgets/properties_view.ui +++ b/src/widgets/properties_view.ui @@ -34,6 +34,16 @@ + + + URI + True + 1 + + + @@ -68,7 +78,7 @@ - + File Created diff --git a/src/window.rs b/src/window.rs index 232f5cf..b4fef33 100644 --- a/src/window.rs +++ b/src/window.rs @@ -36,7 +36,7 @@ use std::cell::{Cell, OnceCell, RefCell}; use std::path::{Path, PathBuf}; use crate::config; -use crate::util::{self, Direction, Position}; +use crate::util::{Direction, Position}; use crate::widgets::{LpDragOverlay, LpImage, LpImageView, LpPropertiesView}; /// Show window after X milliseconds even if image dimensions are not known yet @@ -327,39 +327,47 @@ mod imp { obj.on_zoom_status_changed() })); - let win = &*obj; + current_image_signals.connect_local( + "metadata-changed", + true, + glib::clone!(@weak obj => @default-return None, move |_| { + obj.update_title(); + None + }), + ); + current_image_signals.connect_closure( "notify::best-fit", false, // `closure_local!` is similar to `clone`, but you use `@watch` instead of clone. // `@watch` means that this signal will be disconnected when the watched object // is dropped. - glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| { - win.on_zoom_status_changed(); + glib::closure_local!(@watch obj => move |_: &LpImage, _: &glib::ParamSpec| { + obj.on_zoom_status_changed(); }), ); current_image_signals.connect_closure( "notify::is-max-zoom", false, - glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| { - win.on_zoom_status_changed(); + glib::closure_local!(@watch obj => move |_: &LpImage, _: &glib::ParamSpec| { + obj.on_zoom_status_changed(); }), ); current_image_signals.connect_closure( "notify::image-size-available", false, - glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| { - win.image_size_ready(); + glib::closure_local!(@watch obj => move |_: &LpImage, _: &glib::ParamSpec| { + obj.image_size_ready(); }), ); current_image_signals.connect_closure( "notify::error", false, - glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| { - win.image_error(); + glib::closure_local!(@watch obj => move |_: &LpImage, _: &glib::ParamSpec| { + obj.image_error(); }), ); @@ -737,14 +745,7 @@ impl LpWindow { self.update_headerbar_style(); // Window title - let title = self - .imp() - .image_view - .current_file() - .and_then(|f| util::get_file_display_name(&f)) - .unwrap_or_else(|| gettext("Image Viewer")); - - self.set_title(Some(&title)); + self.update_title(); // Properties view let current_image = current_page.as_ref().map(|x| x.image()); @@ -773,6 +774,17 @@ impl LpWindow { } } + pub fn update_title(&self) { + let title = self + .imp() + .image_view + .current_image() + .and_then(|x| x.metadata().file_name()) + .unwrap_or_else(|| gettext("Image Viewer")); + + self.set_title(Some(&title)); + } + pub fn image_size_ready(&self) { // if visible for whatever reason, don't do any resize if self.is_visible() {