diff --git a/src/components/avatar/editable.rs b/src/components/avatar/editable.rs index 8f2c8d77..160f769e 100644 --- a/src/components/avatar/editable.rs +++ b/src/components/avatar/editable.rs @@ -16,7 +16,10 @@ use crate::{ toast, utils::{ expression, - media::image::{ImageDimensions, ImageError, IMAGE_QUEUE}, + media::{ + image::{ImageError, IMAGE_QUEUE}, + FrameDimensions, + }, BoundObject, BoundObjectWeakRef, CountedRef, }, }; @@ -307,14 +310,14 @@ mod imp { } /// The dimensions of the avatar in this widget. - fn avatar_dimensions(&self) -> ImageDimensions { + fn avatar_dimensions(&self) -> FrameDimensions { let scale_factor = self.obj().scale_factor(); let avatar_size = self.temp_avatar.size(); let size = (avatar_size * scale_factor) .try_into() .expect("size and scale factor are positive integers"); - ImageDimensions { + FrameDimensions { width: size, height: size, } diff --git a/src/components/avatar/image.rs b/src/components/avatar/image.rs index 98746a5c..c378448c 100644 --- a/src/components/avatar/image.rs +++ b/src/components/avatar/image.rs @@ -12,9 +12,11 @@ use ruma::{ use crate::{ session::model::Session, spawn, - utils::media::image::{ - ImageDimensions, ImageError, ImageRequestPriority, ImageSource, ThumbnailDownloader, - ThumbnailSettings, + utils::media::{ + image::{ + ImageError, ImageRequestPriority, ImageSource, ThumbnailDownloader, ThumbnailSettings, + }, + FrameDimensions, }, }; @@ -189,7 +191,7 @@ mod imp { let info = self.info(); let needed_size = self.needed_size.get(); - let dimensions = ImageDimensions { + let dimensions = FrameDimensions { width: needed_size, height: needed_size, }; diff --git a/src/session/view/content/room_details/history_viewer/visual_media_item.rs b/src/session/view/content/room_details/history_viewer/visual_media_item.rs index df17c117..eb839389 100644 --- a/src/session/view/content/room_details/history_viewer/visual_media_item.rs +++ b/src/session/view/content/room_details/history_viewer/visual_media_item.rs @@ -7,7 +7,10 @@ use crate::{ utils::{ add_activate_binding_action, matrix::VisualMediaMessage, - media::image::{ImageDimensions, ImageRequestPriority, ThumbnailSettings}, + media::{ + image::{ImageRequestPriority, ThumbnailSettings}, + FrameDimensions, + }, }, }; @@ -161,7 +164,7 @@ mod imp { let scale_factor = u32::try_from(self.obj().scale_factor()).unwrap_or(1); let size = THUMBNAIL_SIZE * scale_factor; - let dimensions = ImageDimensions { + let dimensions = FrameDimensions { width: size, height: size, }; diff --git a/src/session/view/content/room_history/message_row/visual_media.rs b/src/session/view/content/room_history/message_row/visual_media.rs index 8bf61bfe..44e29049 100644 --- a/src/session/view/content/room_history/message_row/visual_media.rs +++ b/src/session/view/content/room_history/message_row/visual_media.rs @@ -17,7 +17,10 @@ use crate::{ spawn, utils::{ matrix::VisualMediaMessage, - media::image::{ImageDimensions, ImageRequestPriority, ThumbnailSettings}, + media::{ + image::{ImageRequestPriority, ThumbnailSettings}, + FrameDimensions, + }, CountedRef, LoadingState, }, }; @@ -282,12 +285,20 @@ mod imp { session: &Session, format: ContentFormat, ) { - let (width, height) = media_message.dimensions().unzip(); + let dimensions: Option = media_message.dimensions(); let filename = media_message.filename(); let compact = matches!(format, ContentFormat::Compact | ContentFormat::Ellipsized); - self.set_width(width.and_then(|w| w.try_into().ok()).unwrap_or(-1)); - self.set_height(height.and_then(|h| h.try_into().ok()).unwrap_or(-1)); + self.set_width( + dimensions + .and_then(|d| d.width.try_into().ok()) + .unwrap_or(-1), + ); + self.set_height( + dimensions + .and_then(|d| d.height.try_into().ok()) + .unwrap_or(-1), + ); self.set_compact(compact); let accessible_label = if filename.is_empty() { @@ -333,9 +344,11 @@ mod imp { let scale_factor = self.obj().scale_factor(); let settings = ThumbnailSettings { - dimensions: ImageDimensions { - width: u32::try_from(MAX_WIDTH * scale_factor).unwrap_or_default(), - height: u32::try_from(MAX_HEIGHT * scale_factor).unwrap_or_default(), + dimensions: FrameDimensions { + width: u32::try_from(MAX_WIDTH.saturating_mul(scale_factor)) + .unwrap_or_default(), + height: u32::try_from(MAX_HEIGHT.saturating_mul(scale_factor)) + .unwrap_or_default(), }, method: Method::Scale, animated: true, diff --git a/src/utils/matrix/media_message.rs b/src/utils/matrix/media_message.rs index b193ae57..ac852c2f 100644 --- a/src/utils/matrix/media_message.rs +++ b/src/utils/matrix/media_message.rs @@ -1,15 +1,12 @@ use gettextrs::gettext; use gtk::{gio, prelude::*}; use matrix_sdk::Client; -use ruma::{ - events::{ - room::message::{ - AudioMessageEventContent, FileMessageEventContent, FormattedBody, - ImageMessageEventContent, MessageType, VideoMessageEventContent, - }, - sticker::StickerEventContent, +use ruma::events::{ + room::message::{ + AudioMessageEventContent, FileMessageEventContent, FormattedBody, ImageMessageEventContent, + MessageType, VideoMessageEventContent, }, - UInt, + sticker::StickerEventContent, }; use tracing::{debug, error}; @@ -22,7 +19,7 @@ use crate::{ Image, ImageError, ImageRequestPriority, ImageSource, ThumbnailDownloader, ThumbnailSettings, }, - MediaFileError, + FrameDimensions, MediaFileError, }, save_data_to_tmp_file, }, @@ -249,14 +246,13 @@ impl VisualMediaMessage { } /// The dimensions of the media, if any. - /// - /// Returns a `(width, height)` tuple. - pub fn dimensions(&self) -> Option<(UInt, UInt)> { - match self { - Self::Image(c) => c.info.as_ref().and_then(|i| i.width.zip(i.height)), - Self::Video(c) => c.info.as_ref().and_then(|i| i.width.zip(i.height)), - Self::Sticker(c) => c.info.width.zip(c.info.height), - } + pub fn dimensions(&self) -> Option { + let (width, height) = match self { + Self::Image(c) => c.info.as_ref().map(|i| (i.width, i.height))?, + Self::Video(c) => c.info.as_ref().map(|i| (i.width, i.height))?, + Self::Sticker(c) => (c.info.width, c.info.height), + }; + FrameDimensions::from_options(width, height) } /// Fetch a thumbnail of the media with the given client and thumbnail diff --git a/src/utils/media/image/mod.rs b/src/utils/media/image/mod.rs index c5a6f9bc..fab13b3d 100644 --- a/src/utils/media/image/mod.rs +++ b/src/utils/media/image/mod.rs @@ -26,17 +26,13 @@ mod queue; pub(crate) use queue::{ImageRequestPriority, IMAGE_QUEUE}; -use super::MediaFileError; +use super::{FrameDimensions, MediaFileError}; use crate::{components::AnimatedImagePaintable, spawn_tokio, DISABLE_GLYCIN_SANDBOX}; -/// The default width of a generated thumbnail. -const THUMBNAIL_DEFAULT_WIDTH: u32 = 800; -/// The default height of a generated thumbnail. -const THUMBNAIL_DEFAULT_HEIGHT: u32 = 600; -/// The default dimensions of a generated thumbnail. -const THUMBNAIL_DEFAULT_DIMENSIONS: ImageDimensions = ImageDimensions { - width: THUMBNAIL_DEFAULT_WIDTH, - height: THUMBNAIL_DEFAULT_HEIGHT, +/// The maximum dimensions of a generated thumbnail. +const THUMBNAIL_MAX_DIMENSIONS: FrameDimensions = FrameDimensions { + width: 800, + height: 600, }; /// The content type of SVG. const SVG_CONTENT_TYPE: &str = "image/svg+xml"; @@ -44,19 +40,20 @@ const SVG_CONTENT_TYPE: &str = "image/svg+xml"; const WEBP_CONTENT_TYPE: &str = "image/webp"; /// The default WebP quality used for a generated thumbnail. const WEBP_DEFAULT_QUALITY: f32 = 60.0; -/// The maximum file size threshold in bytes for generating a thumbnail. +/// The maximum file size threshold in bytes for requesting or generating a +/// thumbnail. /// /// If the file size of the original image is larger than this, we assume it is -/// worth it to generate a thumbnail, even if its dimensions are smaller than -/// wanted. This is particularly helpful for some image formats that can take up -/// a lot of space. +/// worth it to request or generate a thumbnail, even if its dimensions are +/// smaller than wanted. This is particularly helpful for some image formats +/// that can take up a lot of space. /// /// This is 1MB. const THUMBNAIL_MAX_FILESIZE_THRESHOLD: u32 = 1024 * 1024; -/// The dimension threshold in pixels before we start to generate a thumbnail. +/// The size threshold in pixels for requesting or generating a thumbnail. /// -/// If the original image is larger than the thumbnail dimensions + threshold, -/// we assume it is worth it to generate a thumbnail. +/// If the original image is larger than dimensions + threshold, we assume it is +/// worth it to request or generate a thumbnail. const THUMBNAIL_DIMENSIONS_THRESHOLD: u32 = 200; /// Get an image loader for the given file. @@ -78,14 +75,14 @@ async fn image_loader(file: gio::File) -> Result, glycin: /// To show the image at its natural size, set it to `None`. async fn load_image( file: gio::File, - request_dimensions: Option, + request_dimensions: Option, ) -> Result { let image_loader = image_loader(file).await?; let frame_request = request_dimensions.map(|request| { let image_info = image_loader.info(); - let original_dimensions = ImageDimensions { + let original_dimensions = FrameDimensions { width: image_info.width, height: image_info.height, }; @@ -179,8 +176,7 @@ impl ImageInfoLoader { let info = dimensions.map_or_else(default_base_image_info, Into::into); if !filesize_is_too_big(filesize) - && !dimensions - .is_some_and(|d| d.should_resize_for_thumbnail(THUMBNAIL_DEFAULT_DIMENSIONS)) + && !dimensions.is_some_and(|d| d.needs_thumbnail(THUMBNAIL_MAX_DIMENSIONS)) { // It is not worth it to generate a thumbnail. return (info, None); @@ -214,7 +210,7 @@ enum Frame { impl Frame { /// The dimensions of the frame. - fn dimensions(&self) -> Option { + fn dimensions(&self) -> Option { let (width, height) = match self { Self::Glycin(frame) => (frame.width(), frame.height()), Self::Texture(downloader) => { @@ -226,7 +222,7 @@ impl Frame { } }; - Some(ImageDimensions { width, height }) + Some(FrameDimensions { width, height }) } /// Whether the memory format of the frame is supported by the image crate. @@ -272,7 +268,10 @@ impl Frame { } let image = DynamicImage::from_decoder(self).ok()?; - let thumbnail = image.thumbnail(THUMBNAIL_DEFAULT_WIDTH, THUMBNAIL_DEFAULT_HEIGHT); + let thumbnail = image.thumbnail( + THUMBNAIL_MAX_DIMENSIONS.width, + THUMBNAIL_MAX_DIMENSIONS.height, + ); prepare_thumbnail_for_sending(thumbnail) } @@ -280,7 +279,7 @@ impl Frame { impl ImageDecoder for Frame { fn dimensions(&self) -> (u32, u32) { - let (width, height) = self.dimensions().map(|d| (d.width, d.height)).unzip(); + let (width, height) = self.dimensions().map(|s| (s.width, s.height)).unzip(); (width.unwrap_or(0), height.unwrap_or(0)) } @@ -333,106 +332,43 @@ impl ImageDecoder for Frame { } } -/// Dimensions of an image. -#[derive(Debug, Clone, Copy)] -pub struct ImageDimensions { - /// The width of the image. - pub width: u32, - /// The height of the image. - pub height: u32, -} - -impl ImageDimensions { - /// Construct an `ImageDimensions` from the given optional values. - /// - /// Returns `None` if either of the values are `None`. - fn from_options(width: Option, height: Option) -> Option { - Some(Self { - width: width?, - height: height?, - }) - } - - /// Whether these dimensions are bigger than the given dimensions. - /// - /// Returns `true` if either `width` or `height` is bigger than or equal to - /// the given dimensions. - fn is_bigger_than(self, other: ImageDimensions) -> bool { - self.width >= other.width || self.height >= other.height - } - - /// Whether these dimensions should be resized to generate a thumbnail. - fn should_resize_for_thumbnail(self, thumbnail_dimensions: ImageDimensions) -> bool { - self.is_bigger_than(thumbnail_dimensions.increase_by(THUMBNAIL_DIMENSIONS_THRESHOLD)) - } - - /// Increase both these dimensions by the given value. - const fn increase_by(mut self, value: u32) -> Self { - self.width = self.width.saturating_add(value); - self.height = self.height.saturating_add(value); - self - } - - /// Compute the new dimensions for resizing to the requested dimensions - /// while preserving the aspect ratio of these dimensions and respecting - /// the given strategy. - fn resize(self, requested_dimensions: ImageDimensions, strategy: ResizeStrategy) -> Self { - let w_ratio = f64::from(self.width) / f64::from(requested_dimensions.width); - let h_ratio = f64::from(self.height) / f64::from(requested_dimensions.height); - - let resize_from_width = match strategy { - // The largest ratio wins so the frame fits into the requested dimensions. - ResizeStrategy::Contain => w_ratio > h_ratio, - // The smallest ratio wins so the frame fills the requested dimensions. - ResizeStrategy::Cover => w_ratio < h_ratio, - }; - - #[allow(clippy::cast_sign_loss)] // We need to convert the f64 to a u32. - let (width, height) = if resize_from_width { - let new_height = f64::from(self.height) / w_ratio; - (requested_dimensions.width, new_height as u32) - } else { - let new_width = f64::from(self.width) / h_ratio; - (new_width as u32, requested_dimensions.height) - }; - - Self { width, height } +/// Extensions to `FrameDimensions` for computing thumbnail dimensions. +impl FrameDimensions { + /// Whether we should generate or request a thumbnail for these dimensions, + /// given the wanted thumbnail dimensions. + pub(super) fn needs_thumbnail(self, thumbnail_dimensions: FrameDimensions) -> bool { + self.ge(thumbnail_dimensions.increase_by(THUMBNAIL_DIMENSIONS_THRESHOLD)) } - /// Compute the dimensions for a thumbnail while preserving the aspect ratio - /// of these dimensions. + /// Downscale these dimensions for a thumbnail while preserving the aspect + /// ratio. /// - /// Returns `None` if these dimensions are smaller than the wanted - /// dimensions. - pub(super) fn resize_for_thumbnail(self) -> Option { - let thumbnail_dimensions = THUMBNAIL_DEFAULT_DIMENSIONS; - - if !self.should_resize_for_thumbnail(thumbnail_dimensions) { + /// Returns `None` if these dimensions are smaller than the dimensions of a + /// thumbnail. + pub(super) fn downscale_for_thumbnail(self) -> Option { + if !self.needs_thumbnail(THUMBNAIL_MAX_DIMENSIONS) { + // We do not need to generate a thumbnail. return None; } - Some(self.resize(thumbnail_dimensions, ResizeStrategy::Contain)) + Some(self.scale_to_fit(THUMBNAIL_MAX_DIMENSIONS, gtk::ContentFit::ScaleDown)) } /// Convert these dimensions to a request for the image loader with the /// requested dimensions. - fn to_image_loader_request( - self, - requested_dimensions: ImageDimensions, - ) -> glycin::FrameRequest { - let resized_dimensions = self.resize(requested_dimensions, ResizeStrategy::Cover); - glycin::FrameRequest::new().scale(resized_dimensions.width, resized_dimensions.height) + fn to_image_loader_request(self, requested: Self) -> glycin::FrameRequest { + let scaled = self.scale_to_fit(requested, gtk::ContentFit::Cover); + glycin::FrameRequest::new().scale(scaled.width, scaled.height) } } -impl From for BaseImageInfo { - fn from(value: ImageDimensions) -> Self { - let ImageDimensions { width, height } = value; +impl From for BaseImageInfo { + fn from(value: FrameDimensions) -> Self { + let FrameDimensions { width, height } = value; BaseImageInfo { height: Some(height.into()), width: Some(width.into()), - size: None, - blurhash: None, + ..default_base_image_info() } } } @@ -447,23 +383,6 @@ fn default_base_image_info() -> BaseImageInfo { } } -/// The strategy to use when computing the new dimensions for resizing an image -/// while maintaining its aspect ratio. -#[derive(Debug, Clone, Copy)] -pub enum ResizeStrategy { - /// The image is scaled to fit completely within the new dimensions. - /// - /// This is useful if we do not want the image to be cropped to fit inside - /// maximum dimensions. - Contain, - /// The image is sized to maintain its aspect ratio while filling the new - /// dimensions. - /// - /// This is useful if we want the image to be cropped to fit inside the new - /// dimensions. - Cover, -} - /// Prepare the given thumbnail to send it. pub(super) fn prepare_thumbnail_for_sending(thumbnail: image::DynamicImage) -> Option { // Convert to RGB8/RGBA8 since those are the only formats supported by WebP. @@ -517,8 +436,8 @@ pub struct ThumbnailDownloader<'a> { impl<'a> ThumbnailDownloader<'a> { /// Download the thumbnail of the media. /// - /// This might not return a thumbnail at the requested size, depending on - /// the sources and the homeserver. + /// This might not return a thumbnail at the requested dimensions, depending + /// on the sources and the homeserver. pub async fn download( self, client: Client, @@ -531,9 +450,7 @@ impl<'a> ThumbnailDownloader<'a> { let source = if let Some(alt) = self.alt { if !self.main.can_be_thumbnailed() && (filesize_is_too_big(self.main.filesize()) - || alt - .dimensions() - .is_some_and(|d| d.is_bigger_than(settings.dimensions))) + || alt.dimensions().is_some_and(|s| s.ge(settings.dimensions))) { // Use the alternative source to save bandwidth. alt @@ -587,7 +504,7 @@ impl<'a> ImageSource<'a> { fn should_thumbnail( &self, prefer_thumbnail: bool, - thumbnail_dimensions: ImageDimensions, + thumbnail_dimensions: FrameDimensions, ) -> bool { if !self.can_be_thumbnailed() { return false; @@ -599,7 +516,7 @@ impl<'a> ImageSource<'a> { return true; } - dimensions.is_some_and(|d| d.should_resize_for_thumbnail(thumbnail_dimensions)) + dimensions.is_some_and(|d| d.needs_thumbnail(thumbnail_dimensions)) || filesize_is_too_big(self.filesize()) } @@ -620,11 +537,11 @@ impl<'a> ImageSource<'a> { /// The filesize of this source. fn filesize(&self) -> Option { - self.info.and_then(|i| i.size) + self.info.and_then(|i| i.filesize) } /// The dimensions of this source. - fn dimensions(&self) -> Option { + fn dimensions(&self) -> Option { self.info.and_then(|i| i.dimensions) } } @@ -688,22 +605,19 @@ impl<'a> From<&'a OwnedMxcUri> for MediaSource<'a> { #[derive(Debug, Clone, Copy, Default)] pub struct ImageSourceInfo<'a> { /// The dimensions of the image. - dimensions: Option, + dimensions: Option, /// The MIME type of the image. mimetype: Option<&'a str>, /// The file size of the image. - size: Option, + filesize: Option, } impl<'a> From<&'a ImageInfo> for ImageSourceInfo<'a> { fn from(value: &'a ImageInfo) -> Self { Self { - dimensions: ImageDimensions::from_options( - value.width.and_then(|u| u.try_into().ok()), - value.height.and_then(|u| u.try_into().ok()), - ), + dimensions: FrameDimensions::from_options(value.width, value.height), mimetype: value.mimetype.as_deref(), - size: value.size.and_then(|u| u.try_into().ok()), + filesize: value.size.and_then(|u| u.try_into().ok()), } } } @@ -711,12 +625,9 @@ impl<'a> From<&'a ImageInfo> for ImageSourceInfo<'a> { impl<'a> From<&'a ThumbnailInfo> for ImageSourceInfo<'a> { fn from(value: &'a ThumbnailInfo) -> Self { Self { - dimensions: ImageDimensions::from_options( - value.width.and_then(|u| u.try_into().ok()), - value.height.and_then(|u| u.try_into().ok()), - ), + dimensions: FrameDimensions::from_options(value.width, value.height), mimetype: value.mimetype.as_deref(), - size: value.size.and_then(|u| u.try_into().ok()), + filesize: value.size.and_then(|u| u.try_into().ok()), } } } @@ -724,12 +635,9 @@ impl<'a> From<&'a ThumbnailInfo> for ImageSourceInfo<'a> { impl<'a> From<&'a AvatarImageInfo> for ImageSourceInfo<'a> { fn from(value: &'a AvatarImageInfo) -> Self { Self { - dimensions: ImageDimensions::from_options( - value.width.and_then(|u| u.try_into().ok()), - value.height.and_then(|u| u.try_into().ok()), - ), + dimensions: FrameDimensions::from_options(value.width, value.height), mimetype: value.mimetype.as_deref(), - size: value.size.and_then(|u| u.try_into().ok()), + filesize: value.size.and_then(|u| u.try_into().ok()), } } } @@ -737,8 +645,8 @@ impl<'a> From<&'a AvatarImageInfo> for ImageSourceInfo<'a> { /// The settings for downloading a thumbnail. #[derive(Debug, Clone)] pub struct ThumbnailSettings { - /// The resquested dimensions of the thumbnail. - pub dimensions: ImageDimensions, + /// The requested dimensions of the thumbnail. + pub dimensions: FrameDimensions, /// The method to use to resize the thumbnail. pub method: Method, /// Whether to request an animated thumbnail. diff --git a/src/utils/media/image/queue.rs b/src/utils/media/image/queue.rs index 921b41e1..0012de9b 100644 --- a/src/utils/media/image/queue.rs +++ b/src/utils/media/image/queue.rs @@ -19,10 +19,13 @@ use tokio::{ }; use tracing::{debug, trace, warn}; -use super::{load_image, Image, ImageDimensions, ImageError}; +use super::{load_image, Image, ImageError}; use crate::{ spawn_tokio, - utils::{media::MediaFileError, save_data_to_tmp_file}, + utils::{ + media::{FrameDimensions, MediaFileError}, + save_data_to_tmp_file, + }, }; /// The default image request queue. @@ -95,7 +98,7 @@ impl ImageRequestQueue { &self, client: Client, settings: MediaRequest, - dimensions: Option, + dimensions: Option, priority: ImageRequestPriority, ) -> ImageRequestHandle { let inner = self.inner.clone(); @@ -116,7 +119,7 @@ impl ImageRequestQueue { pub async fn add_file_request( &self, file: gio::File, - dimensions: Option, + dimensions: Option, ) -> ImageRequestHandle { let inner = self.inner.clone(); spawn_tokio!(async move { inner.lock().await.add_file_request(file, dimensions) }) @@ -191,7 +194,7 @@ impl ImageRequestQueueInner { &mut self, client: Client, settings: MediaRequest, - dimensions: Option, + dimensions: Option, priority: ImageRequestPriority, ) -> ImageRequestHandle { let data = DownloadRequestData { @@ -225,7 +228,7 @@ impl ImageRequestQueueInner { fn add_file_request( &mut self, file: gio::File, - dimensions: Option, + dimensions: Option, ) -> ImageRequestHandle { let data = FileRequestData { file, dimensions }; let request_id = data.request_id(); @@ -501,7 +504,7 @@ struct DownloadRequestData { /// The settings of the request. settings: MediaRequest, /// The dimensions to request. - dimensions: Option, + dimensions: Option, } impl DownloadRequestData { @@ -543,7 +546,7 @@ struct FileRequestData { /// The image file to load. file: gio::File, /// The dimensions to request. - dimensions: Option, + dimensions: Option, } impl FileRequestData { diff --git a/src/utils/media/mod.rs b/src/utils/media/mod.rs index 5913e2c9..48675766 100644 --- a/src/utils/media/mod.rs +++ b/src/utils/media/mod.rs @@ -6,6 +6,7 @@ use gettextrs::gettext; use gtk::{gio, glib, prelude::*}; use matrix_sdk::attachment::BaseAudioInfo; use mime::Mime; +use ruma::UInt; pub mod image; pub mod video; @@ -143,3 +144,76 @@ pub enum MediaFileError { /// An error occurred when writing the media to a file. File(#[from] glib::Error), } + +/// The dimensions of a frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FrameDimensions { + /// The width of the frame. + pub width: u32, + /// The height of the frame. + pub height: u32, +} + +impl FrameDimensions { + /// Construct a `FrameDimensions` from the given optional dimensions. + pub(crate) fn from_options(width: Option, height: Option) -> Option { + Some(Self { + width: width?.try_into().ok()?, + height: height?.try_into().ok()?, + }) + } + + /// Whether these dimensions are greater than or equal to the given + /// dimensions. + /// + /// Returns `true` if either `width` or `height` is bigger than or equal to + /// the one in the other dimensions. + pub(crate) fn ge(self, other: Self) -> bool { + self.width >= other.width || self.height >= other.height + } + + /// Increase both of these dimensions by the given value. + pub(crate) const fn increase_by(mut self, value: u32) -> Self { + self.width = self.width.saturating_add(value); + self.height = self.height.saturating_add(value); + self + } + + /// Scale these dimensions to fit into the requested dimensions while + /// preserving the aspect ratio and respecting the given content fit. + pub(crate) fn scale_to_fit(self, requested: Self, content_fit: gtk::ContentFit) -> Self { + let w_ratio = f64::from(self.width) / f64::from(requested.width); + let h_ratio = f64::from(self.height) / f64::from(requested.height); + + let resize_from_width = match content_fit { + // The largest ratio wins so the frame fits into the requested dimensions. + gtk::ContentFit::Contain | gtk::ContentFit::ScaleDown => w_ratio > h_ratio, + // The smallest ratio wins so the frame fills the requested dimensions. + gtk::ContentFit::Cover => w_ratio < h_ratio, + // We just return the requested dimensions since we do not care about the ratio. + _ => return requested, + }; + let downscale_only = content_fit == gtk::ContentFit::ScaleDown; + + #[allow(clippy::cast_sign_loss)] // We need to convert the f64 to a u32. + let (width, height) = if resize_from_width { + if downscale_only && w_ratio <= 1.0 { + // We do not want to upscale. + return self; + } + + let new_height = f64::from(self.height) / w_ratio; + (requested.width, new_height as u32) + } else { + if downscale_only && h_ratio <= 1.0 { + // We do not want to upscale. + return self; + } + + let new_width = f64::from(self.width) / h_ratio; + (new_width as u32, requested.height) + }; + + Self { width, height } + } +} diff --git a/src/utils/media/video.rs b/src/utils/media/video.rs index a3d2975a..5a6aaa5d 100644 --- a/src/utils/media/video.rs +++ b/src/utils/media/video.rs @@ -10,10 +10,7 @@ use image::GenericImageView; use matrix_sdk::attachment::{BaseVideoInfo, Thumbnail}; use tracing::warn; -use super::{ - image::{prepare_thumbnail_for_sending, ImageDimensions}, - load_gstreamer_media_info, -}; +use super::{image::prepare_thumbnail_for_sending, load_gstreamer_media_info, FrameDimensions}; /// A channel sender to send the result of a video thumbnail. type ThumbnailResultSender = oneshot::Sender>; @@ -212,22 +209,23 @@ fn create_thumbnailer_pipeline( }; // Reduce the dimensions if the thumbnail is bigger than the wanted size. - let dimensions = ImageDimensions { + let dimensions = FrameDimensions { width: frame.width() * u32::try_from(info.par().numer()).unwrap_or_default(), height: frame.height() * u32::try_from(info.par().denom()).unwrap_or_default(), }; - let thumbnail = if let Some(target_dimensions) = dimensions.resize_for_thumbnail() { - image::imageops::thumbnail( - &view, - target_dimensions.width, - target_dimensions.height, - ) - } else { - image::ImageBuffer::from_fn(view.width(), view.height(), |x, y| { - view.get_pixel(x, y) - }) - }; + let thumbnail = + if let Some(target_dimensions) = dimensions.downscale_for_thumbnail() { + image::imageops::thumbnail( + &view, + target_dimensions.width, + target_dimensions.height, + ) + } else { + image::ImageBuffer::from_fn(view.width(), view.height(), |x, y| { + view.get_pixel(x, y) + }) + }; // Prepare it. if let Some(thumbnail) = prepare_thumbnail_for_sending(thumbnail.into()) {