Browse Source

chore: Rename ImageDimensions to FrameDimensions and other refactoring

pipelines/767384
Kévin Commaille 1 year ago
parent
commit
1f2dd7a8cf
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 9
      src/components/avatar/editable.rs
  2. 10
      src/components/avatar/image.rs
  3. 7
      src/session/view/content/room_details/history_viewer/visual_media_item.rs
  4. 27
      src/session/view/content/room_history/message_row/visual_media.rs
  5. 30
      src/utils/matrix/media_message.rs
  6. 214
      src/utils/media/image/mod.rs
  7. 19
      src/utils/media/image/queue.rs
  8. 74
      src/utils/media/mod.rs
  9. 30
      src/utils/media/video.rs

9
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,
}

10
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,
};

7
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,
};

27
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<FrameDimensions> = 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,

30
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<FrameDimensions> {
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

214
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::Image<'static>, glycin:
/// To show the image at its natural size, set it to `None`.
async fn load_image(
file: gio::File,
request_dimensions: Option<ImageDimensions>,
request_dimensions: Option<FrameDimensions>,
) -> Result<Image, glycin::ErrorCtx> {
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<ImageDimensions> {
fn dimensions(&self) -> Option<FrameDimensions> {
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<u32>, height: Option<u32>) -> Option<Self> {
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<Self> {
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<Self> {
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<ImageDimensions> for BaseImageInfo {
fn from(value: ImageDimensions) -> Self {
let ImageDimensions { width, height } = value;
impl From<FrameDimensions> 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<Thumbnail> {
// 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<u32> {
self.info.and_then(|i| i.size)
self.info.and_then(|i| i.filesize)
}
/// The dimensions of this source.
fn dimensions(&self) -> Option<ImageDimensions> {
fn dimensions(&self) -> Option<FrameDimensions> {
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<ImageDimensions>,
dimensions: Option<FrameDimensions>,
/// The MIME type of the image.
mimetype: Option<&'a str>,
/// The file size of the image.
size: Option<u32>,
filesize: Option<u32>,
}
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.

19
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<ImageDimensions>,
dimensions: Option<FrameDimensions>,
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<ImageDimensions>,
dimensions: Option<FrameDimensions>,
) -> 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<ImageDimensions>,
dimensions: Option<FrameDimensions>,
priority: ImageRequestPriority,
) -> ImageRequestHandle {
let data = DownloadRequestData {
@ -225,7 +228,7 @@ impl ImageRequestQueueInner {
fn add_file_request(
&mut self,
file: gio::File,
dimensions: Option<ImageDimensions>,
dimensions: Option<FrameDimensions>,
) -> 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<ImageDimensions>,
dimensions: Option<FrameDimensions>,
}
impl DownloadRequestData {
@ -543,7 +546,7 @@ struct FileRequestData {
/// The image file to load.
file: gio::File,
/// The dimensions to request.
dimensions: Option<ImageDimensions>,
dimensions: Option<FrameDimensions>,
}
impl FileRequestData {

74
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<UInt>, height: Option<UInt>) -> Option<Self> {
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 }
}
}

30
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<Result<Thumbnail, ()>>;
@ -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()) {

Loading…
Cancel
Save