propertiesView: Use image dimensions from decoder

This also shows the units for SVG dimensions if not pixels.

Issue #43

Part-of: <https://gitlab.gnome.org/Incubator/loupe/-/merge_requests/136>
This commit is contained in:
Sophie Herold 2023-03-10 19:08:26 +01:00 • committed by Marge Bot
parent 490a46d7c4
commit 42d558d22f
9 changed files with 101 additions and 265 deletions

View file

@ -126,6 +126,8 @@ mod imp {
/// Currently EXIF data
pub image_metadata: RefCell<LpImageMetadata>,
/// Image dimension details for SVGs
pub dimension_details: RefCell<decoder::ImageDimensionDetails>,
/// Current pointer position
pub pointer_position: Cell<Option<(f64, f64)>>,
@ -692,8 +694,9 @@ impl LpImage {
self.reset_rotation();
}
DecoderUpdate::Dimensions => {
DecoderUpdate::Dimensions(dimension_details) => {
log::debug!("Received dimensions: {:?}", self.original_dimensions());
self.imp().dimension_details.replace(dimension_details);
self.notify("image-size");
self.configure_best_fit();
self.request_tiles();
@ -1428,6 +1431,10 @@ impl LpImage {
self.imp().image_metadata.borrow().clone()
}
pub fn dimension_details(&self) -> decoder::ImageDimensionDetails {
self.imp().dimension_details.borrow().clone()
}
/// Drag and drop content provider
pub fn content_provider(&self) -> Option<gdk::ContentProvider> {
let file = self.file()?;

View file

@ -17,6 +17,7 @@
//
// SPDX-License-Identifier: GPL-3.0-or-later
use crate::decoder::ImageDimensionDetails;
use crate::deps::*;
use crate::util;
@ -25,7 +26,6 @@ use adw::subclass::prelude::*;
use glib::clone;
use gtk::CompositeTemplate;
use anyhow::Context;
use futures::future::{AbortHandle, Abortable};
use once_cell::sync::Lazy;
use std::cell::RefCell;
@ -49,10 +49,7 @@ mod imp {
pub metadata: RefCell<LpImageMetadata>,
pub file_info: RefCell<Option<gio::FileInfo>>,
pub dimensions: RefCell<Option<String>>,
pub info_handle: RefCell<Option<AbortHandle>>,
pub dimensions_handle: RefCell<Option<AbortHandle>>,
}
#[glib::object_subclass]
@ -94,11 +91,6 @@ mod imp {
.read_only()
.explicit_notify()
.build(),
glib::ParamSpecString::builder("dimensions")
.default_value(Some(FALLBACK))
.read_only()
.explicit_notify()
.build(),
glib::ParamSpecObject::builder::<LpImage>("image")
.explicit_notify()
.build(),
@ -120,7 +112,6 @@ mod imp {
"image" => obj.image().to_value(),
"metadata" => self.metadata.borrow().to_value(),
"file-info" => obj.file_info().to_value(),
"dimensions" => self.dimensions.borrow().to_value(),
_ => unimplemented!("Failed to get property \"{prop_name}\""),
}
}
@ -174,10 +165,6 @@ impl LpPropertiesView {
handle.abort();
}
if let Some(handle) = imp.dimensions_handle.take() {
handle.abort();
}
self.build_file_info(file);
}
@ -228,7 +215,6 @@ impl LpPropertiesView {
let info_result = util::query_attributes_future(
&file,
vec![
gio::FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
gio::FILE_ATTRIBUTE_STANDARD_SIZE,
gio::FILE_ATTRIBUTE_TIME_CREATED,
gio::FILE_ATTRIBUTE_TIME_MODIFIED,
@ -237,7 +223,7 @@ impl LpPropertiesView {
.await;
match info_result {
Ok(info) => view.on_info_loaded(info, &file),
Ok(info) => view.on_info_loaded(info),
Err(err) => log::warn!("Failed to load file info: {err}"),
}
}),
@ -262,78 +248,12 @@ impl LpPropertiesView {
// This is where we handle the results of the future.
// This could technically be part of the future, but we keep
// them separate for a bit of cleanliness.
fn on_info_loaded(&self, info: gio::FileInfo, file: &gio::File) {
fn on_info_loaded(&self, info: gio::FileInfo) {
let imp = self.imp();
self.build_dimensions(&info, file);
imp.file_info.replace(Some(info));
self.notify("file-info");
}
// Determines the dimensions of the file asynchronously. We do this
// asynchronously to avoid blocking the UI as we parse the file info.
// In addition, we're loading the image metadata separately from the file
// here so that the properties sidebar's information can load separately
// from the current file.
fn build_dimensions(&self, info: &gio::FileInfo, file: &gio::File) {
let imp = self.imp();
// Here we do the same thing we do for the rest of the file info.
let (handle, reg) = AbortHandle::new_pair();
let fut = Abortable::new(
clone!(@weak self as view, @weak file, @weak info => async move {
let is_svg = info.content_type()
.map(|t| t.to_string())
.map(|t| t == "image/svg+xml")
.unwrap_or_default();
let path = file.peek_path();
if path.is_none() {
return;
}
let dimensions = if is_svg {
parse_svg_file_future(path.unwrap())
.await
.ok()
.and_then(|m| {
match (m.width(), m.height()) {
(Some(w), Some(h)) => Some((w, h)),
_ => None::<(f64, f64)>,
}
})
.map(|(w, h)| {
if w.fract() != 0.0 || h.fract() != 0.0 {
format!("{w:.1} \u{D7} {h:.1}")
} else {
format!("{w} \u{D7} {h}")
}
})
} else {
gdk_pixbuf::Pixbuf::file_info_future(path.unwrap())
.await
.ok()
.flatten()
.map(|(_, w, h)| format!("{w} \u{D7} {h}"))
};
view.on_dimensions_loaded(dimensions.unwrap_or_else(|| FALLBACK.to_string()));
}),
reg,
);
imp.dimensions_handle.replace(Some(handle));
spawn!(async {
let _ = fut.await;
});
}
fn on_dimensions_loaded(&self, dimensions: String) {
let imp = self.imp();
imp.dimensions.replace(Some(dimensions));
self.notify("dimensions");
}
async fn open_directory(&self) -> anyhow::Result<()> {
let launcher = gtk::FileLauncher::new(self.file().as_ref());
let win = self.native().and_downcast::<gtk::Window>();
@ -380,6 +300,49 @@ impl LpPropertiesView {
.unwrap_or_else(|| FALLBACK.to_owned()) // and if we get nothing, use `FALLBACK`
}
#[template_callback]
fn image_size(&self, _image_size: Option<glib::Variant>) -> String {
if let Some(image) = self.image() {
match image.dimension_details() {
ImageDimensionDetails::Svg((width, height))
if width.unit != rsvg::LengthUnit::Px
&& height.unit != rsvg::LengthUnit::Px =>
{
// Percent is not stored as percentile
let width_factor = if width.unit == rsvg::LengthUnit::Percent {
100.
} else {
1.
};
let height_factor = if height.unit == rsvg::LengthUnit::Percent {
100.
} else {
1.
};
// Only show two digits
let width_n = (width.length * width_factor * 100.).round() / 100.;
let height_n = (height.length * height_factor * 100.).round() / 100.;
let width_unit = width.unit;
let height_unit = height.unit;
format!("{width_n}\u{202F}{width_unit} \u{D7} {height_n}\u{202F}{height_unit}")
}
_ => {
let (width, height) = image.image_size();
if width > 0 && height > 0 {
format!("{width}\u{202F}\u{D7}\u{202F}{height}")
} else {
FALLBACK.to_owned()
}
}
}
} else {
FALLBACK.to_owned()
}
}
#[template_callback]
fn file_size(&self, info: Option<gio::FileInfo>) -> String {
info.map(|info| {
@ -410,16 +373,3 @@ impl LpPropertiesView {
content.map_or(false, |x| !x.is_empty())
}
}
async fn parse_svg_file_future(path: std::path::PathBuf) -> anyhow::Result<svg_metadata::Metadata> {
let (sender, receiver) = futures_channel::oneshot::channel();
let _ = std::thread::Builder::new()
.name("Parse SVG".to_string())
.spawn(move || {
let result = svg_metadata::Metadata::parse_file(path);
let _ = sender.send(result);
});
receiver.await.unwrap().context("Could not load metadata")
}