Browse Source

image: Use GTK APIs instead of image crate to generate thumbnails

pipelines/786320
Kévin Commaille 1 year ago
parent
commit
023494813a
No known key found for this signature in database
GPG Key ID: C971D9DBC9D678D
  1. 1
      Cargo.lock
  2. 4
      Cargo.toml
  3. 23
      src/session/view/content/room_history/message_toolbar/mod.rs
  4. 290
      src/utils/media/image/mod.rs
  5. 121
      src/utils/media/video.rs

1
Cargo.lock generated

@ -6008,7 +6008,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f53152f51fb5af0c08484c33d16cca96175881d1f3dec068c23b31a158c2d99"
dependencies = [
"image",
"libwebp-sys",
]

4
Cargo.toml

@ -49,7 +49,7 @@ tokio-stream = { version = "0.1", features = ["sync"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2"
webp = "0.3"
webp = { version = "0.3", default-features = false }
# gtk-rs project and dependents. These usually need to be updated together.
adw = { package = "libadwaita", version = "0.7", features = ["v1_6"] }
@ -61,7 +61,7 @@ gst_gtk = { version = "0.13", package = "gst-plugin-gtk4" }
gst_pbutils = { version = "0.23", package = "gstreamer-pbutils" }
gst_play = { version = "0.23", package = "gstreamer-play" }
gst_video = { version = "0.23", package = "gstreamer-video" }
gtk = { package = "gtk4", version = "0.9", features = ["gnome_45"] }
gtk = { package = "gtk4", version = "0.9", features = ["gnome_47"] }
shumate = { package = "libshumate", version = "0.6" }
sourceview = { package = "sourceview5", version = "0.9" }

23
src/session/view/content/room_history/message_toolbar/mod.rs

@ -850,6 +850,15 @@ impl MessageToolbar {
return;
}
let Some(renderer) = self
.root()
.and_downcast::<gtk::Window>()
.and_then(|w| w.renderer())
else {
error!("Could not get GdkRenderer");
return;
};
let filename = filename_for_mime(Some(mime::IMAGE_PNG.as_ref()), None);
let dialog = AttachmentDialog::new(&filename);
dialog.set_image(&image);
@ -862,7 +871,7 @@ impl MessageToolbar {
let filesize = bytes.len().try_into().ok();
let (mut base_info, thumbnail) = ImageInfoLoader::from(image)
.load_info_and_thumbnail(filesize)
.load_info_and_thumbnail(filesize, &renderer)
.await;
base_info.size = filesize.map(Into::into);
@ -922,6 +931,14 @@ impl MessageToolbar {
}
async fn send_file_inner(&self, file: gio::File) {
let Some(renderer) = self
.root()
.and_downcast::<gtk::Window>()
.and_then(|w| w.renderer())
else {
error!("Could not get GdkRenderer");
return;
};
let (bytes, file_info) = match load_file(&file).await {
Ok(data) => data,
Err(error) => {
@ -942,14 +959,14 @@ impl MessageToolbar {
let (info, thumbnail) = match file_info.mime.type_() {
mime::IMAGE => {
let (mut info, thumbnail) = ImageInfoLoader::from(file)
.load_info_and_thumbnail(file_info.size)
.load_info_and_thumbnail(file_info.size, &renderer)
.await;
info.size = size;
(AttachmentInfo::Image(info), thumbnail)
}
mime::VIDEO => {
let (mut info, thumbnail) = load_video_info(&file).await;
let (mut info, thumbnail) = load_video_info(&file, &renderer).await;
info.size = size;
(AttachmentInfo::Video(info), thumbnail)
}

290
src/utils/media/image/mod.rs

@ -3,8 +3,7 @@
use std::{error::Error, fmt, str::FromStr, sync::Arc};
use gettextrs::gettext;
use gtk::{gdk, gio, prelude::*};
use image::{ColorType, DynamicImage, ImageDecoder, ImageResult};
use gtk::{gdk, gio, graphene, gsk, prelude::*};
use matrix_sdk::{
attachment::{BaseImageInfo, BaseThumbnailInfo, Thumbnail},
media::{MediaFormat, MediaRequestParameters, MediaThumbnailSettings},
@ -21,6 +20,7 @@ use ruma::{
},
OwnedMxcUri,
};
use tracing::warn;
mod queue;
@ -153,7 +153,7 @@ impl ImageInfoLoader {
let handle = spawn_tokio!(async move { image_loader.next_frame().await });
Some(Frame::Glycin(handle.await.unwrap().ok()?))
}
Self::Texture(texture) => Some(Frame::Texture(gdk::TextureDownloader::new(&texture))),
Self::Texture(texture) => Some(Frame::Texture(texture)),
}
}
@ -170,6 +170,7 @@ impl ImageInfoLoader {
pub async fn load_info_and_thumbnail(
self,
filesize: Option<u32>,
renderer: &gsk::Renderer,
) -> (BaseImageInfo, Option<Thumbnail>) {
let Some(frame) = self.into_first_frame().await else {
return (default_base_image_info(), None);
@ -185,7 +186,7 @@ impl ImageInfoLoader {
return (info, None);
}
let thumbnail = frame.generate_thumbnail();
let thumbnail = frame.generate_thumbnail(renderer);
(info, thumbnail)
}
@ -204,139 +205,53 @@ impl From<gdk::Texture> for ImageInfoLoader {
}
/// A frame of an image.
#[derive(Debug, Clone)]
enum Frame {
/// A frame loaded via glycin.
Glycin(glycin::Frame),
/// A downloader for a texture in memory,
Texture(gdk::TextureDownloader),
/// A texture in memory,
Texture(gdk::Texture),
}
impl Frame {
/// The dimensions of the frame.
fn dimensions(&self) -> Option<FrameDimensions> {
let (width, height) = match self {
Self::Glycin(frame) => (frame.width(), frame.height()),
Self::Texture(downloader) => {
let texture = downloader.texture();
(
texture.width().try_into().ok()?,
texture.height().try_into().ok()?,
)
}
};
Some(FrameDimensions { width, height })
}
/// Whether the memory format of the frame is supported by the image crate.
fn is_memory_format_supported(&self) -> bool {
match self {
Self::Glycin(frame) => {
matches!(
frame.memory_format(),
glycin::MemoryFormat::G8
| glycin::MemoryFormat::G8a8
| glycin::MemoryFormat::R8g8b8
| glycin::MemoryFormat::R8g8b8a8
| glycin::MemoryFormat::G16
| glycin::MemoryFormat::G16a16
| glycin::MemoryFormat::R16g16b16
| glycin::MemoryFormat::R16g16b16a16
| glycin::MemoryFormat::R32g32b32Float
| glycin::MemoryFormat::R32g32b32a32Float
)
}
Self::Texture(downloader) => {
matches!(
downloader.format(),
gdk::MemoryFormat::G8
| gdk::MemoryFormat::G8a8
| gdk::MemoryFormat::R8g8b8
| gdk::MemoryFormat::R8g8b8a8
| gdk::MemoryFormat::G16
| gdk::MemoryFormat::G16a16
| gdk::MemoryFormat::R16g16b16
| gdk::MemoryFormat::R16g16b16a16
| gdk::MemoryFormat::R32g32b32Float
| gdk::MemoryFormat::R32g32b32a32Float
)
}
Self::Glycin(frame) => Some(FrameDimensions {
width: frame.width(),
height: frame.height(),
}),
Self::Texture(texture) => FrameDimensions::with_texture(texture),
}
}
/// Generate a thumbnail of this frame.
fn generate_thumbnail(self) -> Option<Thumbnail> {
if !self.is_memory_format_supported() {
return None;
}
let image = DynamicImage::from_decoder(self).ok()?;
let thumbnail = image.thumbnail(
THUMBNAIL_MAX_DIMENSIONS.width,
THUMBNAIL_MAX_DIMENSIONS.height,
);
prepare_thumbnail_for_sending(thumbnail)
}
}
fn generate_thumbnail(self, renderer: &gsk::Renderer) -> Option<Thumbnail> {
let texture = match self {
Self::Glycin(frame) => frame.texture(),
Self::Texture(texture) => texture,
};
impl ImageDecoder for Frame {
fn dimensions(&self) -> (u32, u32) {
let (width, height) = self.dimensions().map(|s| (s.width, s.height)).unzip();
(width.unwrap_or(0), height.unwrap_or(0))
}
let thumbnail = TextureThumbnailer(texture).generate_thumbnail(renderer);
fn color_type(&self) -> ColorType {
match self {
Self::Glycin(frame) => match frame.memory_format() {
glycin::MemoryFormat::G8 => ColorType::L8,
glycin::MemoryFormat::G8a8 => ColorType::La8,
glycin::MemoryFormat::R8g8b8 => ColorType::Rgb8,
glycin::MemoryFormat::R8g8b8a8 => ColorType::Rgba8,
glycin::MemoryFormat::G16 => ColorType::L16,
glycin::MemoryFormat::G16a16 => ColorType::La16,
glycin::MemoryFormat::R16g16b16 => ColorType::Rgb16,
glycin::MemoryFormat::R16g16b16a16 => ColorType::Rgba16,
glycin::MemoryFormat::R32g32b32Float => ColorType::Rgb32F,
glycin::MemoryFormat::R32g32b32a32Float => ColorType::Rgba32F,
_ => unimplemented!(),
},
Self::Texture(downloader) => match downloader.format() {
gdk::MemoryFormat::G8 => ColorType::L8,
gdk::MemoryFormat::G8a8 => ColorType::La8,
gdk::MemoryFormat::R8g8b8 => ColorType::Rgb8,
gdk::MemoryFormat::R8g8b8a8 => ColorType::Rgba8,
gdk::MemoryFormat::G16 => ColorType::L16,
gdk::MemoryFormat::G16a16 => ColorType::La16,
gdk::MemoryFormat::R16g16b16 => ColorType::Rgb16,
gdk::MemoryFormat::R16g16b16a16 => ColorType::Rgba16,
gdk::MemoryFormat::R32g32b32Float => ColorType::Rgb32F,
gdk::MemoryFormat::R32g32b32a32Float => ColorType::Rgba32F,
_ => unimplemented!(),
},
if thumbnail.is_none() {
warn!("Could not generate thumbnail from GdkTexture");
}
}
fn read_image(self, buf: &mut [u8]) -> ImageResult<()>
where
Self: Sized,
{
let bytes = match &self {
Self::Glycin(frame) => frame.buf_bytes(),
Self::Texture(texture) => texture.download_bytes().0,
};
buf.copy_from_slice(&bytes);
Ok(())
}
fn read_image_boxed(self: Box<Self>, _buf: &mut [u8]) -> ImageResult<()> {
unimplemented!()
thumbnail
}
}
/// Extensions to `FrameDimensions` for computing thumbnail dimensions.
impl FrameDimensions {
/// Construct a `FrameDimensions` for the given texture.
fn with_texture(texture: &gdk::Texture) -> Option<Self> {
Some(Self {
width: texture.width().try_into().ok()?,
height: texture.height().try_into().ok()?,
})
}
/// 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 {
@ -386,41 +301,116 @@ fn default_base_image_info() -> BaseImageInfo {
}
}
/// 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.
let thumbnail: DynamicImage = match &thumbnail {
DynamicImage::ImageLuma8(_)
| DynamicImage::ImageRgb8(_)
| DynamicImage::ImageLuma16(_)
| DynamicImage::ImageRgb16(_)
| DynamicImage::ImageRgb32F(_) => thumbnail.into_rgb8().into(),
DynamicImage::ImageLumaA8(_)
| DynamicImage::ImageRgba8(_)
| DynamicImage::ImageLumaA16(_)
| DynamicImage::ImageRgba16(_)
| DynamicImage::ImageRgba32F(_) => thumbnail.into_rgba8().into(),
_ => return None,
};
// Encode to WebP.
let encoder = webp::Encoder::from_image(&thumbnail).ok()?;
let thumbnail_bytes = encoder.encode(WEBP_DEFAULT_QUALITY).to_vec();
let thumbnail_content_type =
mime::Mime::from_str(WEBP_CONTENT_TYPE).expect("content type should be valid");
let thumbnail_info = BaseThumbnailInfo {
width: Some(thumbnail.width().into()),
height: Some(thumbnail.height().into()),
size: thumbnail_bytes.len().try_into().ok(),
};
Some(Thumbnail {
data: thumbnail_bytes,
content_type: thumbnail_content_type,
info: Some(thumbnail_info),
})
/// A thumbnailer for a `GdkTexture`.
#[derive(Debug, Clone)]
pub(super) struct TextureThumbnailer(pub(super) gdk::Texture);
impl TextureThumbnailer {
/// Downscale the texture if needed, to send it as a thumbnail.
///
/// Returns `None` if the dimensions of the texture are unknown.
fn downscale_texture_if_needed(self, renderer: &gsk::Renderer) -> Option<gdk::Texture> {
let dimensions = FrameDimensions::with_texture(&self.0)?;
let texture = if let Some(target_dimensions) = dimensions.downscale_for_thumbnail() {
let snapshot = gtk::Snapshot::new();
let bounds = graphene::Rect::new(
0.0,
0.0,
target_dimensions.width as f32,
target_dimensions.height as f32,
);
snapshot.append_texture(&self.0, &bounds);
let node = snapshot.to_node()?;
renderer.render_texture(node, None)
} else {
self.0
};
Some(texture)
}
/// Convert the given texture memory format to the format needed to make a
/// thumbnail.
///
/// The WebP encoder only supports RGB and RGBA.
///
/// Returns `None` if the format is unknown.
fn texture_format_to_thumbnail_format(
format: gdk::MemoryFormat,
) -> Option<(gdk::MemoryFormat, webp::PixelLayout)> {
match format {
gdk::MemoryFormat::B8g8r8a8Premultiplied
| gdk::MemoryFormat::A8r8g8b8Premultiplied
| gdk::MemoryFormat::R8g8b8a8Premultiplied
| gdk::MemoryFormat::B8g8r8a8
| gdk::MemoryFormat::A8r8g8b8
| gdk::MemoryFormat::R8g8b8a8
| gdk::MemoryFormat::R16g16b16a16Premultiplied
| gdk::MemoryFormat::R16g16b16a16
| gdk::MemoryFormat::R16g16b16a16FloatPremultiplied
| gdk::MemoryFormat::R16g16b16a16Float
| gdk::MemoryFormat::R32g32b32a32FloatPremultiplied
| gdk::MemoryFormat::R32g32b32a32Float
| gdk::MemoryFormat::G8a8Premultiplied
| gdk::MemoryFormat::G8a8
| gdk::MemoryFormat::G16a16Premultiplied
| gdk::MemoryFormat::G16a16
| gdk::MemoryFormat::A8
| gdk::MemoryFormat::A16
| gdk::MemoryFormat::A16Float
| gdk::MemoryFormat::A32Float
| gdk::MemoryFormat::A8b8g8r8Premultiplied
| gdk::MemoryFormat::A8b8g8r8 => {
Some((gdk::MemoryFormat::R8g8b8a8, webp::PixelLayout::Rgba))
}
gdk::MemoryFormat::R8g8b8
| gdk::MemoryFormat::B8g8r8
| gdk::MemoryFormat::R16g16b16
| gdk::MemoryFormat::R16g16b16Float
| gdk::MemoryFormat::R32g32b32Float
| gdk::MemoryFormat::G8
| gdk::MemoryFormat::G16
| gdk::MemoryFormat::B8g8r8x8
| gdk::MemoryFormat::X8r8g8b8
| gdk::MemoryFormat::R8g8b8x8
| gdk::MemoryFormat::X8b8g8r8 => {
Some((gdk::MemoryFormat::R8g8b8, webp::PixelLayout::Rgb))
}
_ => None,
}
}
/// Generate the thumbnail with the given `GskRenderer`.
pub(super) fn generate_thumbnail(self, renderer: &gsk::Renderer) -> Option<Thumbnail> {
let thumbnail = self.downscale_texture_if_needed(renderer)?;
let dimensions = FrameDimensions::with_texture(&thumbnail)?;
let (downloader_format, webp_layout) =
Self::texture_format_to_thumbnail_format(thumbnail.format())?;
let mut downloader = gdk::TextureDownloader::new(&thumbnail);
downloader.set_format(downloader_format);
let (data, _) = downloader.download_bytes();
let encoder = webp::Encoder::new(&data, webp_layout, dimensions.width, dimensions.height);
let data = encoder.encode(WEBP_DEFAULT_QUALITY).to_vec();
let content_type =
mime::Mime::from_str(WEBP_CONTENT_TYPE).expect("content type should be valid");
let thumbnail_info = BaseThumbnailInfo {
width: Some(dimensions.width.into()),
height: Some(dimensions.height.into()),
size: data.len().try_into().ok(),
};
Some(Thumbnail {
data,
content_type,
info: Some(thumbnail_info),
})
}
}
/// An API to download a thumbnail for a media.

121
src/utils/media/video.rs

@ -5,19 +5,21 @@ use std::sync::{Arc, Mutex};
use futures_channel::oneshot;
use gst::prelude::*;
use gst_video::prelude::*;
use gtk::{gio, glib, glib::clone, prelude::*};
use image::GenericImageView;
use gtk::{gdk, gio, glib, glib::clone, gsk, prelude::*};
use matrix_sdk::attachment::{BaseVideoInfo, Thumbnail};
use tracing::warn;
use super::{image::prepare_thumbnail_for_sending, load_gstreamer_media_info, FrameDimensions};
use super::{image::TextureThumbnailer, load_gstreamer_media_info};
/// A channel sender to send the result of a video thumbnail.
type ThumbnailResultSender = oneshot::Sender<Result<Thumbnail, ()>>;
type ThumbnailResultSender = oneshot::Sender<Result<gdk::Texture, ()>>;
/// Load information and try to generate a thumbnail for the video in the given
/// file.
pub async fn load_video_info(file: &gio::File) -> (BaseVideoInfo, Option<Thumbnail>) {
pub async fn load_video_info(
file: &gio::File,
renderer: &gsk::Renderer,
) -> (BaseVideoInfo, Option<Thumbnail>) {
let mut info = BaseVideoInfo {
duration: None,
width: None,
@ -41,13 +43,13 @@ pub async fn load_video_info(file: &gio::File) -> (BaseVideoInfo, Option<Thumbna
info.height = Some(stream_info.height().into());
}
let thumbnail = generate_video_thumbnail(file).await;
let thumbnail = generate_video_thumbnail(file, renderer).await;
(info, thumbnail)
}
/// Generate a thumbnail for the video in the given file.
async fn generate_video_thumbnail(file: &gio::File) -> Option<Thumbnail> {
async fn generate_video_thumbnail(file: &gio::File, renderer: &gsk::Renderer) -> Option<Thumbnail> {
let (sender, receiver) = oneshot::channel();
let sender = Arc::new(Mutex::new(Some(sender)));
@ -106,13 +108,20 @@ async fn generate_video_thumbnail(file: &gio::File) -> Option<Thumbnail> {
))
.expect("Setting bus watch succeeds");
let thumbnail = receiver.await;
let texture = receiver.await;
// Clean up.
let _ = pipeline.set_state(gst::State::Null);
bus.set_flushing(true);
thumbnail.ok().transpose().ok().flatten()
let texture = texture.ok()?.ok()?;
let thumbnail = TextureThumbnailer(texture).generate_thumbnail(renderer);
if thumbnail.is_none() {
warn!("Could not generate thumbnail from GdkTexture");
}
thumbnail
}
/// Create a pipeline to get a thumbnail of the first frame.
@ -133,7 +142,7 @@ fn create_thumbnailer_pipeline(
.downcast::<gst_app::AppSink>()
.expect("Sink element is an appsink");
// Don't synchronize on the clock, we only want a snapshot asap.
// Do not synchronize on the clock, we only want a snapshot asap.
appsink.set_property("sync", false);
// Tell the appsink what format we want, for simplicity we only accept 8-bit
@ -186,56 +195,12 @@ fn create_thumbnailer_pipeline(
gst::FlowError::Error
})?;
// Create a FlatSamples around the borrowed video frame data from GStreamer with
// the correct stride.
let img = image::FlatSamples::<&[u8]> {
samples: frame.plane_data(0).unwrap(),
layout: image::flat::SampleLayout {
channels: 3, // RGB
channel_stride: 1, // 1 byte from component to component
width: frame.width(),
width_stride: 4, // 4 bytes from pixel to pixel
height: frame.height(),
height_stride: frame.plane_stride()[0].try_into().unwrap_or_default(), // stride from line to line
},
color_hint: Some(image::ColorType::Rgb8),
};
let Ok(view) = img.as_view::<image::Rgb<u8>>() else {
warn!("Could not parse frame as view");
send_video_thumbnail_result(&sender, Err(()));
return Err(gst::FlowError::Error);
};
// Reduce the dimensions if the thumbnail is bigger than the wanted size.
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.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()) {
send_video_thumbnail_result(&sender, Ok(thumbnail));
if let Some(texture) = video_frame_to_texture(&frame) {
send_video_thumbnail_result(&sender, Ok(texture));
Err(gst::FlowError::Eos)
} else {
warn!("Failed to convert video thumbnail");
warn!("Could not convert video frame to GdkTexture");
send_video_thumbnail_result(&sender, Err(()));
Err(gst::FlowError::Error)
}
})
@ -248,7 +213,7 @@ fn create_thumbnailer_pipeline(
/// Try to send the given video thumbnail result through the given sender.
fn send_video_thumbnail_result(
sender: &Mutex<Option<ThumbnailResultSender>>,
result: Result<Thumbnail, ()>,
result: Result<gdk::Texture, ()>,
) {
let mut sender = match sender.lock() {
Ok(sender) => sender,
@ -264,3 +229,43 @@ fn send_video_thumbnail_result(
}
}
}
/// Convert the given video frame to a `GdkTexture`.
fn video_frame_to_texture(
frame: &gst_video::VideoFrameRef<&gst::BufferRef>,
) -> Option<gdk::Texture> {
let format = video_format_to_memory_format(frame.format())?;
let width = frame.width();
let height = frame.height();
let rowstride = frame.plane_stride()[0].try_into().ok()?;
let texture = gdk::MemoryTexture::new(
width.try_into().ok()?,
height.try_into().ok()?,
format,
&glib::Bytes::from(frame.plane_data(0).ok()?),
rowstride,
)
.upcast::<gdk::Texture>();
Some(texture)
}
/// Convert the given `GstVideoFormat` to a `GdkMemoryFormat`.
fn video_format_to_memory_format(format: gst_video::VideoFormat) -> Option<gdk::MemoryFormat> {
let format = match format {
gst_video::VideoFormat::Bgrx => gdk::MemoryFormat::B8g8r8x8,
gst_video::VideoFormat::Xrgb => gdk::MemoryFormat::X8r8g8b8,
gst_video::VideoFormat::Rgbx => gdk::MemoryFormat::R8g8b8x8,
gst_video::VideoFormat::Xbgr => gdk::MemoryFormat::X8b8g8r8,
gst_video::VideoFormat::Bgra => gdk::MemoryFormat::B8g8r8a8,
gst_video::VideoFormat::Argb => gdk::MemoryFormat::A8r8g8b8,
gst_video::VideoFormat::Rgba => gdk::MemoryFormat::R8g8b8a8,
gst_video::VideoFormat::Abgr => gdk::MemoryFormat::A8b8g8r8,
gst_video::VideoFormat::Rgb => gdk::MemoryFormat::R8g8b8,
gst_video::VideoFormat::Bgr => gdk::MemoryFormat::B8g8r8,
_ => return None,
};
Some(format)
}

Loading…
Cancel
Save