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:
parent
490a46d7c4
commit
42d558d22f
9 changed files with 101 additions and 265 deletions
|
|
@ -33,3 +33,10 @@ impl std::fmt::Display for ImageFormat {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub enum ImageDimensionDetails {
|
||||
Svg((rsvg::Length, rsvg::Length)),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,11 @@ impl Svg {
|
|||
|
||||
let (original_width, original_height) = svg_dimensions(&renderer);
|
||||
|
||||
tiles.set_original_dimensions((original_width, original_height));
|
||||
let intrisic_dimensions = renderer.intrinsic_dimensions();
|
||||
tiles.set_original_dimensions_full(
|
||||
(original_width, original_height),
|
||||
ImageDimensionDetails::Svg((intrisic_dimensions.width, intrisic_dimensions.height)),
|
||||
);
|
||||
|
||||
loop {
|
||||
let tile_request = {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
mod formats;
|
||||
pub mod tiling;
|
||||
|
||||
pub use formats::ImageDimensionDetails;
|
||||
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
use formats::*;
|
||||
use tiling::TilingStoreExt;
|
||||
|
|
@ -36,7 +38,7 @@ pub struct UpdateSender {
|
|||
/// Signals for renderer
|
||||
pub enum DecoderUpdate {
|
||||
/// Dimensions of image in `TilingSore` available/updated
|
||||
Dimensions,
|
||||
Dimensions(ImageDimensionDetails),
|
||||
/// Metadata available
|
||||
Metadata(ImageMetadata),
|
||||
/// Image format determined
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
///! be re-genearted for each zoom level. It can also be used to allow
|
||||
///! showing large JPEGs etc where the complete decoded image would not
|
||||
///! even fit in the VRAM.
|
||||
use super::{DecoderUpdate, UpdateSender};
|
||||
use super::{DecoderUpdate, ImageDimensionDetails, UpdateSender};
|
||||
use crate::deps::*;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
|
|
@ -134,9 +134,17 @@ impl TilingStore {
|
|||
}
|
||||
|
||||
pub fn set_original_dimensions(&mut self, dimensions: Coordinates) {
|
||||
self.set_original_dimensions_full(dimensions, Default::default());
|
||||
}
|
||||
|
||||
pub fn set_original_dimensions_full(
|
||||
&mut self,
|
||||
dimensions: Coordinates,
|
||||
dimension_details: ImageDimensionDetails,
|
||||
) {
|
||||
self.original_dimensions = Some(dimensions);
|
||||
if let Some(updater) = &self.update_sender {
|
||||
updater.send(DecoderUpdate::Dimensions);
|
||||
updater.send(DecoderUpdate::Dimensions(dimension_details));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -335,6 +343,11 @@ impl RectExt for graphene::Rect {
|
|||
pub trait TilingStoreExt {
|
||||
fn push(&self, tile: Tile);
|
||||
fn set_original_dimensions(&self, size: Coordinates);
|
||||
fn set_original_dimensions_full(
|
||||
&self,
|
||||
size: Coordinates,
|
||||
dimension_details: ImageDimensionDetails,
|
||||
);
|
||||
fn set_update_sender(&self, sender: UpdateSender);
|
||||
}
|
||||
|
||||
|
|
@ -355,6 +368,18 @@ impl TilingStoreExt for ArcSwap<TilingStore> {
|
|||
});
|
||||
}
|
||||
|
||||
fn set_original_dimensions_full(
|
||||
&self,
|
||||
size: Coordinates,
|
||||
dimension_details: ImageDimensionDetails,
|
||||
) {
|
||||
self.rcu(|tiling_store| {
|
||||
let mut new_store = (**tiling_store).clone();
|
||||
new_store.set_original_dimensions_full(size, dimension_details.clone());
|
||||
Arc::new(new_store)
|
||||
});
|
||||
}
|
||||
|
||||
fn set_update_sender(&self, sender: UpdateSender) {
|
||||
self.rcu(|tiling_store| {
|
||||
let mut new_store = (**tiling_store).clone();
|
||||
|
|
|
|||
|
|
@ -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()?;
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue