metadata: Move file info and format to metadata

This commit is contained in:
Sophie Herold 2023-12-04 12:52:40 +01:00
parent c36e7f4eb6
commit c0f435386d
16 changed files with 327 additions and 339 deletions

View file

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

View file

@ -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),

View file

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

View file

@ -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<String>,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> (Self, mpsc::UnboundedReceiver<DecoderUpdate>) {
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,30 +139,21 @@ impl Decoder {
fn format_decoder(
update_sender: UpdateSender,
file: gio::File,
mime_type: Option<String>,
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
) -> 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))
{
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.into(),
mime_type,
"SVG".into(),
)));
return FormatDecoder::Svg(Svg::new(file, update_sender, tiles));
}
}
}
FormatDecoder::Glycin(Glycin::new(file, update_sender, tiles))
}

View file

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

View file

@ -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 <http://www.gnu.org/licenses/>.
//
// 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<imp::LpImageMetadata>);
}
impl LpImageMetadata {
pub fn load(file: &gio::File) -> Self {
let obj = glib::Object::new::<Self>();
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<GPSLocation> {
self.imp().metadata.borrow().gps_location()
}
pub fn src(&self) -> std::cell::Ref<super::ImageMetadata> {
self.imp().metadata.borrow()
}
}
impl From<ImageMetadata> for LpImageMetadata {
fn from(metadata: ImageMetadata) -> Self {
let obj = glib::Object::new::<Self>();
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<ImageMetadata>,
}
#[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<Vec<glib::ParamSpec>> = 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}"),
}
}
}
}

View file

@ -54,7 +54,7 @@ mod application;
mod config;
mod decoder;
mod file_model;
mod image_metadata;
mod metadata;
mod util;
mod widgets;
mod window;

38
src/metadata/file.rs Normal file
View file

@ -0,0 +1,38 @@
use crate::deps::*;
pub struct FileInfo {
pub(super) display_name: glib::GString,
pub(super) file_size: Option<u64>,
pub(super) mime_type: Option<glib::GString>,
pub(super) created: Option<glib::DateTime>,
pub(super) modified: Option<glib::DateTime>,
}
impl FileInfo {
pub async fn new(file: &gio::File) -> anyhow::Result<Self> {
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(),
})
}
}

28
src/metadata/format.rs Normal file
View file

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

View file

@ -21,25 +21,28 @@
- <https://github.com/ianare/exif-samples>
*/
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<exif::Exif>,
pub heif_transform: bool,
pub struct Metadata {
exif: Option<exif::Exif>,
file_info: Option<FileInfo>,
format: Option<ImageFormat>,
transformations_applied: Option<bool>,
}
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<u8>) -> Self {
impl Metadata {
pub fn set_exif_bytes(&mut self, bytes: Vec<u8>) {
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<String> {
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<String> {
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<String> {
self.file_info.as_ref().map(|x| x.display_name.to_string())
}
pub fn file_size(&self) -> Option<String> {
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<String> {
self.file_info
.as_ref()
.and_then(|x| x.created.as_ref())
.and_then(util::datetime_fmt)
}
pub fn file_modified(&self) -> Option<String> {
self.file_info
.as_ref()
.and_then(|x| x.modified.as_ref())
.and_then(util::datetime_fmt)
}
pub fn originally_created(&self) -> Option<String> {
if let Some(exif) = &self.exif {
if let Some(field) = exif

View file

@ -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<Option<String>>,
#[property(name = "format-name", type = Option<String>, get = Self::format_name)]
pub(super) format: RefCell<Option<decoder::ImageFormat>>,
//pub(super) format: RefCell<Option<decoder::ImageFormat>>,
pub(super) background_color: RefCell<Option<gdk::RGBA>>,
/// Track changes to this image
@ -177,8 +178,7 @@ mod imp {
pub(super) _vscroll_policy: PhantomData<gtk::ScrollablePolicy>,
/// Currently EXIF data
#[property(name = "metadata", type=LpImageMetadata, get)]
pub(super) image_metadata: RefCell<LpImageMetadata>,
pub(super) metadata: RefCell<Metadata>,
/// Image dimension details for SVGs
pub(super) dimension_details: RefCell<decoder::ImageDimensionDetails>,
@ -229,6 +229,12 @@ mod imp {
self.derived_property(id, pspec)
}
fn signals() -> &'static [Signal] {
static SIGNALS: Lazy<Vec<Signal>> =
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<String> {
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: &gtk::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<decoder::ImageFormat> {
self.imp().format.borrow().clone()
}
fn set_error(&self, err: Option<anyhow::Error>) {
log::debug!("Decoding error: {err:?}");
self.imp()
@ -1696,6 +1703,7 @@ impl LpImage {
pub async fn background_color_guess(&self) -> Option<gdk::RGBA> {
// 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<Target = Metadata> + '_ {
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", &[]);
}
}

View file

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

View file

@ -44,6 +44,8 @@ mod imp {
#[template_child]
folder: TemplateChild<adw::ActionRow>,
#[template_child]
uri: TemplateChild<adw::ActionRow>,
#[template_child]
image_size: TemplateChild<adw::ActionRow>,
@ -52,6 +54,8 @@ mod imp {
#[template_child]
file_size: TemplateChild<adw::ActionRow>,
#[template_child]
dates: TemplateChild<adw::PreferencesGroup>,
#[template_child]
file_created: TemplateChild<adw::ActionRow>,
#[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");
}
*/
}

View file

@ -34,6 +34,16 @@
</style>
</object>
</child>
<child>
<object class="AdwActionRow" id="uri">
<property name="title" translatable="yes">URI</property>
<property name="subtitle-selectable">True</property>
<property name="subtitle-lines">1</property>
<style>
<class name="property" />
</style>
</object>
</child>
</object>
</child>
<child>
@ -68,7 +78,7 @@
</object>
</child>
<child>
<object class="AdwPreferencesGroup">
<object class="AdwPreferencesGroup" id="dates">
<child>
<object class="AdwActionRow" id="file_created">
<property name="title" translatable="yes">File Created</property>

View file

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