From 5dc7dfd4c86a0449fd4ce814084c0578bd7dd301 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 28 Dec 2021 18:51:30 +0100 Subject: [PATCH] feat(sdk): Add method to generate thumbnails from images --- crates/matrix-sdk/Cargo.toml | 25 +++++- crates/matrix-sdk/README.md | 2 + crates/matrix-sdk/src/attachment.rs | 131 ++++++++++++++++++++++++++++ crates/matrix-sdk/src/error.rs | 22 +++++ crates/matrix-sdk/src/lib.rs | 8 ++ 5 files changed, 187 insertions(+), 1 deletion(-) diff --git a/crates/matrix-sdk/Cargo.toml b/crates/matrix-sdk/Cargo.toml index 2adc756dc..c3177ac79 100644 --- a/crates/matrix-sdk/Cargo.toml +++ b/crates/matrix-sdk/Cargo.toml @@ -36,13 +36,16 @@ rustls-tls = ["reqwest/rustls-tls"] socks = ["reqwest/socks"] sso_login = ["warp", "rand", "tokio-stream"] appservice = ["ruma/appservice-api-s", "ruma/appservice-api-helper"] +image_proc = ["image"] +image_rayon = ["image/jpeg_rayon"] docsrs = [ "encryption", "sled_cryptostore", "sled_state_store", "sso_login", - "qrcode" + "qrcode", + "image_proc", ] [dependencies] @@ -66,6 +69,26 @@ url = "2.2.2" zeroize = "1.3.0" async-stream = "0.3.2" +[dependencies.image] +version = "0.23.14" +default-features = false +features = [ + "gif", + "jpeg", + "ico", + "png", + "pnm", + "tga", + "tiff", + "webp", + "bmp", + "hdr", + "dxt", + "dds", + "farbfeld", +] +optional = true + [dependencies.matrix-sdk-base] version = "0.4.0" path = "../matrix-sdk-base" diff --git a/crates/matrix-sdk/README.md b/crates/matrix-sdk/README.md index 0d2a98dc0..634f0a935 100644 --- a/crates/matrix-sdk/README.md +++ b/crates/matrix-sdk/README.md @@ -64,6 +64,8 @@ The following crate feature flags are available: | `anyhow` | No | Better logging for event handlers that return `anyhow::Result` | | `encryption` | Yes | End-to-end encryption support | | `eyre` | No | Better logging for event handlers that return `eyre::Result` | +| `image_proc` | No | Enables image processing to generate thumbnails | +| `image_rayon` | No | Enables faster image processing | | `markdown` | No | Support to send Markdown-formatted messages | | `qrcode` | Yes | QR code verification support | | `sled_cryptostore` | Yes | Persistent storage for E2EE related data | diff --git a/crates/matrix-sdk/src/attachment.rs b/crates/matrix-sdk/src/attachment.rs index a33c5556f..369223389 100644 --- a/crates/matrix-sdk/src/attachment.rs +++ b/crates/matrix-sdk/src/attachment.rs @@ -1,5 +1,9 @@ use std::io::Read; +#[cfg(feature = "image_proc")] +use std::io::{BufRead, Seek}; +#[cfg(feature = "image_proc")] +use image::GenericImageView; use ruma::{ assign, events::room::{ @@ -9,6 +13,9 @@ use ruma::{ UInt, }; +#[cfg(feature = "image_proc")] +use crate::ImageError; + /// Base metadata about an image. #[derive(Debug, Clone)] pub struct BaseImageInfo { @@ -152,3 +159,127 @@ pub struct Thumbnail<'a, R: Read> { /// Typed `None` for an `>`. pub const NONE_THUMBNAIL: Option> = None; + +/// Generate a thumbnail for an image. +/// +/// This is a convenience method that uses the +/// [image](https://github.com/image-rs/image) crate. +/// +/// # Arguments +/// * `content_type` - The type of the media, this will be used as the +/// content-type header. +/// +/// * `reader` - A `Reader` that will be used to fetch the raw bytes of the +/// media. +/// +/// * `size` - The size of the thumbnail in pixels as a `(width, height)` tuple. +/// If set to `None`, defaults to `(800, 600)`. +/// +/// # Examples +/// +/// ```no_run +/// # use std::{path::PathBuf, fs::File, io::{BufReader, Read, Seek}}; +/// # use matrix_sdk::{Client, attachment::{Thumbnail, generate_image_thumbnail}, ruma::room_id}; +/// # use url::Url; +/// # use mime; +/// # use futures::executor::block_on; +/// # block_on(async { +/// # let homeserver = Url::parse("http://localhost:8080")?; +/// # let mut client = Client::new(homeserver)?; +/// # let room_id = room_id!("!test:localhost"); +/// let path = PathBuf::from("/home/example/my-cat.jpg"); +/// let mut image = BufReader::new(File::open(path)?); +/// +/// let (thumbnail_data, thumbnail_info) = generate_image_thumbnail( +/// &mime::IMAGE_JPEG, +/// &mut image, +/// None +/// )?; +/// let thumbnail = Thumbnail { +/// reader: &mut thumbnail_data.as_slice(), +/// content_type: &mime::IMAGE_JPEG, +/// info: Some(thumbnail_info), +/// }; +/// +/// image.rewind()?; +/// +/// if let Some(room) = client.get_joined_room(&room_id) { +/// room.send_attachment( +/// "My favorite cat", +/// &mime::IMAGE_JPEG, +/// &mut image, +/// None, +/// Some(thumbnail), +/// None, +/// ).await?; +/// } +/// # Result::<_, matrix_sdk::Error>::Ok(()) }); +/// ``` +#[cfg(feature = "image_proc")] +pub fn generate_image_thumbnail( + content_type: &mime::Mime, + reader: &mut R, + size: Option<(u32, u32)>, +) -> Result<(Vec, BaseThumbnailInfo), ImageError> { + let image_format = image_format_from_mime_type(content_type); + if image_format.is_none() { + return Err(ImageError::FormatNotSupported); + } + + let image_format = image_format.unwrap(); + + let image = image::load(reader, image_format)?; + let (original_width, original_height) = image.dimensions(); + + let (width, height) = size.unwrap_or((800, 600)); + + // Don't generate a thumbnail if it would be bigger than or equal to the + // original. + if height >= original_height && width >= original_width { + return Err(ImageError::ThumbnailBiggerThanOriginal); + } + + let thumbnail = image.thumbnail(width, height); + let (thumbnail_width, thumbnail_height) = thumbnail.dimensions(); + + let mut data: Vec = vec![]; + thumbnail.write_to(&mut data, image_format)?; + let data_size = data.len() as u32; + + Ok(( + data, + BaseThumbnailInfo { + width: Some(thumbnail_width.into()), + height: Some(thumbnail_height.into()), + size: Some(data_size.into()), + }, + )) +} + +// FIXME: Replace this method by ImageFormat::from_mime_type after "image" +// crate's next release. +/// Return the image format specified by a MIME type. +#[cfg(feature = "image_proc")] +fn image_format_from_mime_type(mime_type: M) -> Option +where + M: AsRef, +{ + match mime_type.as_ref() { + "image/avif" => Some(image::ImageFormat::Avif), + "image/jpeg" => Some(image::ImageFormat::Jpeg), + "image/png" => Some(image::ImageFormat::Png), + "image/gif" => Some(image::ImageFormat::Gif), + "image/webp" => Some(image::ImageFormat::WebP), + "image/tiff" => Some(image::ImageFormat::Tiff), + "image/x-targa" | "image/x-tga" => Some(image::ImageFormat::Tga), + "image/vnd-ms.dds" => Some(image::ImageFormat::Dds), + "image/bmp" => Some(image::ImageFormat::Bmp), + "image/x-icon" => Some(image::ImageFormat::Ico), + "image/vnd.radiance" => Some(image::ImageFormat::Hdr), + "image/x-portable-bitmap" + | "image/x-portable-graymap" + | "image/x-portable-pixmap" + | "image/x-portable-anymap" => Some(image::ImageFormat::Pnm), + _ => None, + } +} diff --git a/crates/matrix-sdk/src/error.rs b/crates/matrix-sdk/src/error.rs index 423563984..00a0e7595 100644 --- a/crates/matrix-sdk/src/error.rs +++ b/crates/matrix-sdk/src/error.rs @@ -157,6 +157,11 @@ pub enum Error { /// An error encountered when trying to parse a user tag name. #[error(transparent)] UserTagName(#[from] InvalidUserTagName), + + /// An error while processing images. + #[cfg(feature = "image_proc")] + #[error(transparent)] + ImageError(#[from] ImageError), } /// Error for the room key importing functionality. @@ -257,3 +262,20 @@ impl From for Error { Error::Http(HttpError::Reqwest(e)) } } + +/// All possible errors that can happen during image processing. +#[cfg(feature = "image_proc")] +#[derive(Error, Debug)] +pub enum ImageError { + /// Error processing the image data. + #[error(transparent)] + Proc(#[from] image::ImageError), + + /// The image format is not supported. + #[error("the image format is not supported")] + FormatNotSupported, + + /// The thumbnail size is bigger than the original image. + #[error("the thumbnail size is bigger than the original image size")] + ThumbnailBiggerThanOriginal, +} diff --git a/crates/matrix-sdk/src/lib.rs b/crates/matrix-sdk/src/lib.rs index 48b7df5f8..9860e8a3a 100644 --- a/crates/matrix-sdk/src/lib.rs +++ b/crates/matrix-sdk/src/lib.rs @@ -36,6 +36,12 @@ compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled #[cfg(all(feature = "sso_login", target_arch = "wasm32"))] compile_error!("'sso_login' cannot be enabled on 'wasm32' arch"); +#[cfg(all(feature = "image_rayon", target_arch = "wasm32"))] +compile_error!("'image_rayon' cannot be enabled on 'wasm32' arch"); + +#[cfg(all(feature = "image_rayon", not(feature = "image_proc")))] +compile_error!("'image_rayon' only works with 'image_proc' feature"); + pub use bytes; pub use matrix_sdk_base::{ media, Room as BaseRoom, RoomInfo, RoomMember as BaseRoomMember, RoomType, Session, @@ -62,6 +68,8 @@ mod sync; pub mod encryption; pub use client::{Client, LoopCtrl}; +#[cfg(feature = "image_proc")] +pub use error::ImageError; pub use error::{Error, HttpError, HttpResult, Result}; pub use http_client::HttpSend; pub use room_member::RoomMember;