metadata: Move file info and format to metadata
This commit is contained in:
parent
c36e7f4eb6
commit
c0f435386d
16 changed files with 327 additions and 339 deletions
|
|
@ -19,7 +19,7 @@
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::decoder::tiling::{self, FrameBufferExt};
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
use crate::image_metadata::ImageMetadata;
|
use crate::metadata::{ImageFormat, Metadata};
|
||||||
|
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
|
|
@ -62,8 +62,9 @@ impl Glycin {
|
||||||
let image = image_request.request().await?;
|
let image = image_request.request().await?;
|
||||||
|
|
||||||
if let Some(exif_raw) = image.info().exif.clone().into() {
|
if let Some(exif_raw) = image.info().exif.clone().into() {
|
||||||
let mut metadata = ImageMetadata::from_exif_bytes(exif_raw);
|
let mut metadata = Metadata::default();
|
||||||
metadata.heif_transform = image.info().transformations_applied;
|
metadata.set_exif_bytes(exif_raw);
|
||||||
|
metadata.set_transformations_applied(image.info().transformations_applied);
|
||||||
updater.send(DecoderUpdate::Metadata(metadata));
|
updater.send(DecoderUpdate::Metadata(metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,35 +23,6 @@ pub use svg::{Svg, RSVG_MAX_SIZE};
|
||||||
|
|
||||||
use super::{DecoderUpdate, UpdateSender};
|
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)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub enum ImageDimensionDetails {
|
pub enum ImageDimensionDetails {
|
||||||
Svg(String),
|
Svg(String),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ use super::*;
|
||||||
use crate::decoder::tiling::{self, FrameBufferExt};
|
use crate::decoder::tiling::{self, FrameBufferExt};
|
||||||
use crate::decoder::TileRequest;
|
use crate::decoder::TileRequest;
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
|
use crate::metadata::ImageFormat;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,17 @@ pub mod tiling;
|
||||||
pub use formats::ImageDimensionDetails;
|
pub use formats::ImageDimensionDetails;
|
||||||
|
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
use crate::image_metadata::ImageMetadata;
|
use crate::metadata::{ImageFormat, Metadata};
|
||||||
use formats::*;
|
use formats::*;
|
||||||
use tiling::FrameBufferExt;
|
use tiling::FrameBufferExt;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use futures::channel::mpsc;
|
use futures::channel::mpsc;
|
||||||
use gio::prelude::*;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub use formats::{ImageFormat, RSVG_MAX_SIZE};
|
pub use formats::RSVG_MAX_SIZE;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
/// Renderer requests new tiles
|
/// Renderer requests new tiles
|
||||||
|
|
@ -58,7 +57,7 @@ pub enum DecoderUpdate {
|
||||||
/// Dimensions of image in `TilingSore` available/updated
|
/// Dimensions of image in `TilingSore` available/updated
|
||||||
Dimensions(ImageDimensionDetails),
|
Dimensions(ImageDimensionDetails),
|
||||||
/// Metadata available
|
/// Metadata available
|
||||||
Metadata(ImageMetadata),
|
Metadata(Metadata),
|
||||||
/// Image format determined
|
/// Image format determined
|
||||||
Format(ImageFormat),
|
Format(ImageFormat),
|
||||||
/// Image format not supported or unknown
|
/// Image format not supported or unknown
|
||||||
|
|
@ -120,6 +119,7 @@ impl Decoder {
|
||||||
/// The renderer should listen to updates from the returned receiver.
|
/// The renderer should listen to updates from the returned receiver.
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
file: gio::File,
|
file: gio::File,
|
||||||
|
mime_type: Option<String>,
|
||||||
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> (Self, mpsc::UnboundedReceiver<DecoderUpdate>) {
|
) -> (Self, mpsc::UnboundedReceiver<DecoderUpdate>) {
|
||||||
let (sender, receiver) = mpsc::unbounded();
|
let (sender, receiver) = mpsc::unbounded();
|
||||||
|
|
@ -127,7 +127,7 @@ impl Decoder {
|
||||||
let update_sender = UpdateSender { sender };
|
let update_sender = UpdateSender { sender };
|
||||||
tiles.set_update_sender(update_sender.clone());
|
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 {
|
let decoder = Self {
|
||||||
decoder: format_decoder,
|
decoder: format_decoder,
|
||||||
update_sender,
|
update_sender,
|
||||||
|
|
@ -139,30 +139,21 @@ impl Decoder {
|
||||||
fn format_decoder(
|
fn format_decoder(
|
||||||
update_sender: UpdateSender,
|
update_sender: UpdateSender,
|
||||||
file: gio::File,
|
file: gio::File,
|
||||||
|
mime_type: Option<String>,
|
||||||
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
tiles: Arc<ArcSwap<tiling::FrameBuffer>>,
|
||||||
) -> FormatDecoder {
|
) -> FormatDecoder {
|
||||||
if let Ok(file_info) = file.query_info(
|
if let Some(mime_type) = mime_type {
|
||||||
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
|
// Known things we want to match here are
|
||||||
// - image/svg+xml
|
// - image/svg+xml
|
||||||
// - image/svg+xml-compressed
|
// - image/svg+xml-compressed
|
||||||
if mime_type.split('+').next() == Some("image/svg") {
|
if mime_type.split('+').next() == Some("image/svg") {
|
||||||
update_sender.send(DecoderUpdate::Format(ImageFormat::new(
|
update_sender.send(DecoderUpdate::Format(ImageFormat::new(
|
||||||
mime_type.into(),
|
mime_type,
|
||||||
"SVG".into(),
|
"SVG".into(),
|
||||||
)));
|
)));
|
||||||
return FormatDecoder::Svg(Svg::new(file, update_sender, tiles));
|
return FormatDecoder::Svg(Svg::new(file, update_sender, tiles));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
FormatDecoder::Glycin(Glycin::new(file, update_sender, tiles))
|
FormatDecoder::Glycin(Glycin::new(file, update_sender, tiles))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ use crate::util::gettext::*;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use gio::prelude::*;
|
use gio::prelude::*;
|
||||||
use gio::subclass::prelude::*;
|
use gio::subclass::prelude::*;
|
||||||
|
use glib::subclass::Signal;
|
||||||
use glib::GString;
|
use glib::GString;
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
@ -32,7 +33,6 @@ use std::cell::{OnceCell, RefCell};
|
||||||
|
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gtk::glib::subclass::Signal;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct LpFileModel {
|
pub struct LpFileModel {
|
||||||
|
|
|
||||||
|
|
@ -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}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -54,7 +54,7 @@ mod application;
|
||||||
mod config;
|
mod config;
|
||||||
mod decoder;
|
mod decoder;
|
||||||
mod file_model;
|
mod file_model;
|
||||||
mod image_metadata;
|
mod metadata;
|
||||||
mod util;
|
mod util;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
mod window;
|
mod window;
|
||||||
|
|
|
||||||
38
src/metadata/file.rs
Normal file
38
src/metadata/file.rs
Normal 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
28
src/metadata/format.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,25 +21,28 @@
|
||||||
- <https://github.com/ianare/exif-samples>
|
- <https://github.com/ianare/exif-samples>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
mod file;
|
||||||
|
mod format;
|
||||||
mod gps;
|
mod gps;
|
||||||
mod obj;
|
|
||||||
|
|
||||||
|
pub use file::FileInfo;
|
||||||
|
pub use format::ImageFormat;
|
||||||
pub use gps::GPSLocation;
|
pub use gps::GPSLocation;
|
||||||
pub use obj::LpImageMetadata;
|
|
||||||
|
|
||||||
|
use crate::util;
|
||||||
use crate::util::gettext::*;
|
use crate::util::gettext::*;
|
||||||
use crate::util::{self, ToBufRead};
|
|
||||||
|
|
||||||
use crate::deps::*;
|
use crate::deps::*;
|
||||||
use gio::prelude::*;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ImageMetadata {
|
pub struct Metadata {
|
||||||
pub exif: Option<exif::Exif>,
|
exif: Option<exif::Exif>,
|
||||||
pub heif_transform: bool,
|
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> {
|
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||||
if let Some(exif) = &self.exif {
|
if let Some(exif) = &self.exif {
|
||||||
let list = exif.fields().map(|f| {
|
let list = exif.fields().map(|f| {
|
||||||
|
|
@ -59,43 +62,65 @@ impl std::fmt::Debug for ImageMetadata {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageMetadata {
|
impl Metadata {
|
||||||
pub fn load(file: &gio::File) -> Self {
|
pub fn set_exif_bytes(&mut self, bytes: Vec<u8>) {
|
||||||
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 {
|
|
||||||
let reader = exif::Reader::new();
|
let reader = exif::Reader::new();
|
||||||
match reader.read_raw(bytes) {
|
let exif = reader.read_raw(bytes);
|
||||||
Ok(exif) => Self {
|
|
||||||
exif: Some(exif),
|
if let Err(err) = &exif {
|
||||||
..Default::default()
|
log::warn!("Failed to decode EXIF bytes: {err}");
|
||||||
},
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!("Failed to decoder EXIF bytes: {err}");
|
|
||||||
Self::default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
pub fn mime_type(&self) -> Option<String> {
|
||||||
self.exif.is_some()
|
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 {
|
pub fn orientation(&self) -> Orientation {
|
||||||
if self.heif_transform {
|
if self.transformations_applied() {
|
||||||
// HEIF library already does it's transformations on its own
|
// HEIF library already does it's transformations on its own
|
||||||
Orientation::default()
|
Orientation::default()
|
||||||
} else if let Some(exif) = &self.exif {
|
} 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> {
|
pub fn originally_created(&self) -> Option<String> {
|
||||||
if let Some(exif) = &self.exif {
|
if let Some(exif) = &self.exif {
|
||||||
if let Some(field) = exif
|
if let Some(field) = exif
|
||||||
|
|
@ -34,17 +34,19 @@ use crate::deps::*;
|
||||||
|
|
||||||
use crate::decoder::tiling::FrameBufferExt;
|
use crate::decoder::tiling::FrameBufferExt;
|
||||||
use crate::decoder::{self, tiling, Decoder, DecoderUpdate};
|
use crate::decoder::{self, tiling, Decoder, DecoderUpdate};
|
||||||
use crate::image_metadata::LpImageMetadata;
|
use crate::metadata::{self, Metadata};
|
||||||
use crate::util::Gesture;
|
use crate::util::Gesture;
|
||||||
|
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
use adw::subclass::prelude::*;
|
use adw::subclass::prelude::*;
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
|
use glib::subclass::Signal;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
|
||||||
use std::cell::{Cell, OnceCell, RefCell};
|
use std::cell::{Cell, OnceCell, RefCell};
|
||||||
use std::marker::PhantomData;
|
use std::marker::PhantomData;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Default background color around images and behind transparent images
|
/// 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
|
/// Set if an error has occurred, shown on error_page
|
||||||
#[property(get)]
|
#[property(get)]
|
||||||
pub(super) error: RefCell<Option<String>>,
|
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>>,
|
pub(super) background_color: RefCell<Option<gdk::RGBA>>,
|
||||||
|
|
||||||
/// Track changes to this image
|
/// Track changes to this image
|
||||||
|
|
@ -177,8 +178,7 @@ mod imp {
|
||||||
pub(super) _vscroll_policy: PhantomData<gtk::ScrollablePolicy>,
|
pub(super) _vscroll_policy: PhantomData<gtk::ScrollablePolicy>,
|
||||||
|
|
||||||
/// Currently EXIF data
|
/// Currently EXIF data
|
||||||
#[property(name = "metadata", type=LpImageMetadata, get)]
|
pub(super) metadata: RefCell<Metadata>,
|
||||||
pub(super) image_metadata: RefCell<LpImageMetadata>,
|
|
||||||
/// Image dimension details for SVGs
|
/// Image dimension details for SVGs
|
||||||
pub(super) dimension_details: RefCell<decoder::ImageDimensionDetails>,
|
pub(super) dimension_details: RefCell<decoder::ImageDimensionDetails>,
|
||||||
|
|
||||||
|
|
@ -229,6 +229,12 @@ mod imp {
|
||||||
self.derived_property(id, pspec)
|
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) {
|
fn constructed(&self) {
|
||||||
self.parent_constructed();
|
self.parent_constructed();
|
||||||
|
|
||||||
|
|
@ -604,11 +610,6 @@ mod imp {
|
||||||
pub fn scroll_policy(&self) -> gtk::ScrollablePolicy {
|
pub fn scroll_policy(&self) -> gtk::ScrollablePolicy {
|
||||||
gtk::ScrollablePolicy::Minimum
|
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 {
|
impl WidgetImpl for LpImage {
|
||||||
|
|
@ -651,18 +652,18 @@ mod imp {
|
||||||
|
|
||||||
// called when the widget content should be re-rendered
|
// called when the widget content should be re-rendered
|
||||||
fn snapshot(&self, snapshot: >k::Snapshot) {
|
fn snapshot(&self, snapshot: >k::Snapshot) {
|
||||||
let widget = self.obj();
|
let obj = self.obj();
|
||||||
let widget_width = widget.width() as f64;
|
let widget_width = obj.width() as f64;
|
||||||
let widget_height = widget.height() as f64;
|
let widget_height = obj.height() as f64;
|
||||||
let display_width = widget.image_displayed_width();
|
let display_width = obj.image_displayed_width();
|
||||||
let display_height = widget.image_displayed_height();
|
let display_height = obj.image_displayed_height();
|
||||||
|
|
||||||
// make sure the scrollbars are correct
|
// 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
|
// Looks better in SVG animations and avoids rendering issues
|
||||||
gsk::ScalingFilter::Linear
|
gsk::ScalingFilter::Linear
|
||||||
} else if applicable_zoom < 1. {
|
} else if applicable_zoom < 1. {
|
||||||
|
|
@ -675,8 +676,8 @@ mod imp {
|
||||||
|
|
||||||
let render_options = tiling::RenderOptions {
|
let render_options = tiling::RenderOptions {
|
||||||
scaling_filter,
|
scaling_filter,
|
||||||
scale_factor: widget.scale_factor(),
|
scale_factor: obj.scale_factor(),
|
||||||
background_color: Some(widget.background_color()),
|
background_color: Some(obj.background_color()),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Operations on snapshots are coordinate transformations
|
// Operations on snapshots are coordinate transformations
|
||||||
|
|
@ -685,34 +686,34 @@ mod imp {
|
||||||
|
|
||||||
// Add background
|
// Add background
|
||||||
snapshot.append_color(
|
snapshot.append_color(
|
||||||
&widget.background_color(),
|
&obj.background_color(),
|
||||||
&graphene::Rect::new(0., 0., widget_width as f32, widget_height as f32),
|
&graphene::Rect::new(0., 0., widget_width as f32, widget_height as f32),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply the scrolling position to the image
|
// 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.);
|
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.);
|
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)
|
// 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 x = obj.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 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
|
// Round to pixel values to not have a half pixel offset to physical pixels
|
||||||
// The offset would leading to a blurry output
|
// The offset would leading to a blurry output
|
||||||
snapshot.translate(&graphene::Point::new(
|
snapshot.translate(&graphene::Point::new(
|
||||||
widget.round_f64(x) as f32,
|
obj.round_f64(x) as f32,
|
||||||
widget.round_f64(y) as f32,
|
obj.round_f64(y) as f32,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Apply rotation and mirroring
|
// Apply rotation and mirroring
|
||||||
widget.snapshot_rotate_mirror(
|
obj.snapshot_rotate_mirror(
|
||||||
snapshot,
|
snapshot,
|
||||||
widget.rotation() as f32,
|
obj.rotation() as f32,
|
||||||
widget.mirrored(),
|
obj.mirrored(),
|
||||||
applicable_zoom,
|
applicable_zoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -813,9 +814,19 @@ glib::wrapper! {
|
||||||
|
|
||||||
impl LpImage {
|
impl LpImage {
|
||||||
pub async fn load(&self, file: &gio::File) {
|
pub async fn load(&self, file: &gio::File) {
|
||||||
|
let imp = self.imp();
|
||||||
log::debug!("Loading file {}", file.uri());
|
log::debug!("Loading file {}", file.uri());
|
||||||
|
|
||||||
self.set_file(file);
|
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;
|
let tiles = &self.imp().frame_buffer;
|
||||||
// Delete all stored textures for reloads
|
// Delete all stored textures for reloads
|
||||||
|
|
@ -823,7 +834,8 @@ impl LpImage {
|
||||||
// Reset background color for reloads
|
// Reset background color for reloads
|
||||||
self.set_background_color(None);
|
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();
|
let weak_obj = self.downgrade();
|
||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
|
|
@ -835,7 +847,7 @@ impl LpImage {
|
||||||
log::debug!("Stopped listening to decoder since sender is gone");
|
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
|
/// Called when decoder sends update
|
||||||
|
|
@ -845,8 +857,8 @@ impl LpImage {
|
||||||
match update {
|
match update {
|
||||||
DecoderUpdate::Metadata(metadata) => {
|
DecoderUpdate::Metadata(metadata) => {
|
||||||
log::debug!("Received metadata");
|
log::debug!("Received metadata");
|
||||||
imp.image_metadata.replace(LpImageMetadata::from(metadata));
|
imp.metadata.borrow_mut().merge(metadata);
|
||||||
self.notify_metadata();
|
self.emmit_metadata_changed();
|
||||||
|
|
||||||
self.reset_rotation();
|
self.reset_rotation();
|
||||||
}
|
}
|
||||||
|
|
@ -880,8 +892,8 @@ impl LpImage {
|
||||||
self.set_error(Some(err));
|
self.set_error(Some(err));
|
||||||
}
|
}
|
||||||
DecoderUpdate::Format(format) => {
|
DecoderUpdate::Format(format) => {
|
||||||
imp.format.replace(Some(format));
|
imp.metadata.borrow_mut().set_format(format);
|
||||||
self.notify_format_name();
|
self.emmit_metadata_changed();
|
||||||
}
|
}
|
||||||
DecoderUpdate::Animated => {
|
DecoderUpdate::Animated => {
|
||||||
let callback_id = self
|
let callback_id = self
|
||||||
|
|
@ -1117,7 +1129,7 @@ impl LpImage {
|
||||||
|
|
||||||
/// Maximal zoom allowed for this image
|
/// Maximal zoom allowed for this image
|
||||||
fn max_zoom(&self) -> f64 {
|
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();
|
let (width, height) = self.original_dimensions();
|
||||||
// Avoid division by 0
|
// Avoid division by 0
|
||||||
let long_side = f64::max(1., i32::max(width, height) as f64);
|
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
|
/// Image size of original image with EXIF rotation applied
|
||||||
pub fn image_size(&self) -> (i32, i32) {
|
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. {
|
if orientation.rotation.abs() == 90. || orientation.rotation.abs() == 270. {
|
||||||
let (x, y) = self.original_dimensions();
|
let (x, y) = self.original_dimensions();
|
||||||
(y, x)
|
(y, x)
|
||||||
|
|
@ -1599,11 +1611,6 @@ impl LpImage {
|
||||||
Some(gdk::ContentProvider::for_value(&list.to_value()))
|
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>) {
|
fn set_error(&self, err: Option<anyhow::Error>) {
|
||||||
log::debug!("Decoding error: {err:?}");
|
log::debug!("Decoding error: {err:?}");
|
||||||
self.imp()
|
self.imp()
|
||||||
|
|
@ -1696,6 +1703,7 @@ impl LpImage {
|
||||||
pub async fn background_color_guess(&self) -> Option<gdk::RGBA> {
|
pub async fn background_color_guess(&self) -> Option<gdk::RGBA> {
|
||||||
// Shortcut for formats that don't support transparency
|
// Shortcut for formats that don't support transparency
|
||||||
if !self
|
if !self
|
||||||
|
.metadata()
|
||||||
.format()
|
.format()
|
||||||
.map_or(true, |x| x.is_potentially_transparent())
|
.map_or(true, |x| x.is_potentially_transparent())
|
||||||
{
|
{
|
||||||
|
|
@ -1809,4 +1817,19 @@ impl LpImage {
|
||||||
.await
|
.await
|
||||||
.ok()?
|
.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", &[]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -856,7 +856,7 @@ impl LpPrint {
|
||||||
let texture_scale = self.user_width() / orig_width as f64;
|
let texture_scale = self.user_width() / orig_width as f64;
|
||||||
let cairo_scale = cairo_dpi / self.dpi();
|
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
|
// Render SVG to exact needed sizes
|
||||||
// TODO: This should be async
|
// TODO: This should be async
|
||||||
decoder::formats::Svg::render_print(
|
decoder::formats::Svg::render_print(
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ mod imp {
|
||||||
|
|
||||||
#[template_child]
|
#[template_child]
|
||||||
folder: TemplateChild<adw::ActionRow>,
|
folder: TemplateChild<adw::ActionRow>,
|
||||||
|
#[template_child]
|
||||||
|
uri: TemplateChild<adw::ActionRow>,
|
||||||
|
|
||||||
#[template_child]
|
#[template_child]
|
||||||
image_size: TemplateChild<adw::ActionRow>,
|
image_size: TemplateChild<adw::ActionRow>,
|
||||||
|
|
@ -52,6 +54,8 @@ mod imp {
|
||||||
#[template_child]
|
#[template_child]
|
||||||
file_size: TemplateChild<adw::ActionRow>,
|
file_size: TemplateChild<adw::ActionRow>,
|
||||||
|
|
||||||
|
#[template_child]
|
||||||
|
dates: TemplateChild<adw::PreferencesGroup>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
file_created: TemplateChild<adw::ActionRow>,
|
file_created: TemplateChild<adw::ActionRow>,
|
||||||
#[template_child]
|
#[template_child]
|
||||||
|
|
@ -110,9 +114,11 @@ mod imp {
|
||||||
Some("image-size-available"),
|
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"),
|
image_signals.connect_local(
|
||||||
glib::clone!(@weak obj => move |_,_| obj.imp().update()),
|
"metadata-changed",
|
||||||
|
false,
|
||||||
|
glib::clone!(@weak obj => @default-return None, move |_| { obj.imp().update(); None }),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.update();
|
self.update();
|
||||||
|
|
@ -156,28 +162,43 @@ mod imp {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let metadata = image.metadata();
|
||||||
|
|
||||||
// Folder
|
// 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());
|
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
|
// Image info
|
||||||
Self::update_row(&self.image_size, self.image_size());
|
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.image_format, metadata.format_name());
|
||||||
//Self::update_row(&self.file_size, self.image_size());
|
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
|
// Details (EXIF)
|
||||||
let metadata = image.metadata();
|
|
||||||
let meta = metadata.src();
|
|
||||||
let has_details = [
|
let has_details = [
|
||||||
Self::update_row(&self.location, meta.gps_location().map(|x| x.display())),
|
Self::update_row(&self.location, metadata.gps_location().map(|x| x.display())),
|
||||||
Self::update_row(&self.originally_created, meta.originally_created()),
|
Self::update_row(&self.originally_created, metadata.originally_created()),
|
||||||
Self::update_row(&self.aperture, meta.f_number()),
|
Self::update_row(&self.aperture, metadata.f_number()),
|
||||||
Self::update_row(&self.exposure, meta.exposure_time()),
|
Self::update_row(&self.exposure, metadata.exposure_time()),
|
||||||
Self::update_row(&self.iso, meta.iso()),
|
Self::update_row(&self.iso, metadata.iso()),
|
||||||
Self::update_row(&self.focal_length, meta.focal_length()),
|
Self::update_row(&self.focal_length, metadata.focal_length()),
|
||||||
Self::update_row(&self.maker_model, meta.maker_model()),
|
Self::update_row(&self.maker_model, metadata.maker_model()),
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.any(|x| *x);
|
.any(|x| *x);
|
||||||
|
|
@ -211,7 +232,7 @@ mod imp {
|
||||||
_ => {
|
_ => {
|
||||||
let (width, height) = image.image_size();
|
let (width, height) = image.image_size();
|
||||||
if width > 0 && height > 0 {
|
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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -265,34 +286,3 @@ glib::wrapper! {
|
||||||
@extends gtk::Widget, adw::Bin,
|
@extends gtk::Widget, adw::Bin,
|
||||||
@implements gtk::Buildable, gtk::ConstraintTarget, gtk::Orientable;
|
@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");
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,16 @@
|
||||||
</style>
|
</style>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</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>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
|
|
@ -68,7 +78,7 @@
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="AdwPreferencesGroup">
|
<object class="AdwPreferencesGroup" id="dates">
|
||||||
<child>
|
<child>
|
||||||
<object class="AdwActionRow" id="file_created">
|
<object class="AdwActionRow" id="file_created">
|
||||||
<property name="title" translatable="yes">File Created</property>
|
<property name="title" translatable="yes">File Created</property>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ use std::cell::{Cell, OnceCell, RefCell};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use crate::config;
|
use crate::config;
|
||||||
use crate::util::{self, Direction, Position};
|
use crate::util::{Direction, Position};
|
||||||
use crate::widgets::{LpDragOverlay, LpImage, LpImageView, LpPropertiesView};
|
use crate::widgets::{LpDragOverlay, LpImage, LpImageView, LpPropertiesView};
|
||||||
|
|
||||||
/// Show window after X milliseconds even if image dimensions are not known yet
|
/// Show window after X milliseconds even if image dimensions are not known yet
|
||||||
|
|
@ -327,39 +327,47 @@ mod imp {
|
||||||
obj.on_zoom_status_changed()
|
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(
|
current_image_signals.connect_closure(
|
||||||
"notify::best-fit",
|
"notify::best-fit",
|
||||||
false,
|
false,
|
||||||
// `closure_local!` is similar to `clone`, but you use `@watch` instead of clone.
|
// `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
|
// `@watch` means that this signal will be disconnected when the watched object
|
||||||
// is dropped.
|
// is dropped.
|
||||||
glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| {
|
glib::closure_local!(@watch obj => move |_: &LpImage, _: &glib::ParamSpec| {
|
||||||
win.on_zoom_status_changed();
|
obj.on_zoom_status_changed();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
current_image_signals.connect_closure(
|
current_image_signals.connect_closure(
|
||||||
"notify::is-max-zoom",
|
"notify::is-max-zoom",
|
||||||
false,
|
false,
|
||||||
glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| {
|
glib::closure_local!(@watch obj => move |_: &LpImage, _: &glib::ParamSpec| {
|
||||||
win.on_zoom_status_changed();
|
obj.on_zoom_status_changed();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
current_image_signals.connect_closure(
|
current_image_signals.connect_closure(
|
||||||
"notify::image-size-available",
|
"notify::image-size-available",
|
||||||
false,
|
false,
|
||||||
glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| {
|
glib::closure_local!(@watch obj => move |_: &LpImage, _: &glib::ParamSpec| {
|
||||||
win.image_size_ready();
|
obj.image_size_ready();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
current_image_signals.connect_closure(
|
current_image_signals.connect_closure(
|
||||||
"notify::error",
|
"notify::error",
|
||||||
false,
|
false,
|
||||||
glib::closure_local!(@watch win => move |_: &LpImage, _: &glib::ParamSpec| {
|
glib::closure_local!(@watch obj => move |_: &LpImage, _: &glib::ParamSpec| {
|
||||||
win.image_error();
|
obj.image_error();
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -737,14 +745,7 @@ impl LpWindow {
|
||||||
self.update_headerbar_style();
|
self.update_headerbar_style();
|
||||||
|
|
||||||
// Window title
|
// Window title
|
||||||
let title = self
|
self.update_title();
|
||||||
.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));
|
|
||||||
|
|
||||||
// Properties view
|
// Properties view
|
||||||
let current_image = current_page.as_ref().map(|x| x.image());
|
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) {
|
pub fn image_size_ready(&self) {
|
||||||
// if visible for whatever reason, don't do any resize
|
// if visible for whatever reason, don't do any resize
|
||||||
if self.is_visible() {
|
if self.is_visible() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue