feat(sdk): Add method to generate thumbnails from images

This commit is contained in:
Kévin Commaille
2021-12-28 18:51:30 +01:00
parent 0436780292
commit 5dc7dfd4c8
5 changed files with 187 additions and 1 deletions

View File

@@ -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"

View File

@@ -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 |

View File

@@ -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 `<Option<Thumbnail>>`.
pub const NONE_THUMBNAIL: Option<Thumbnail<&[u8]>> = 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<R: BufRead + Seek>(
content_type: &mime::Mime,
reader: &mut R,
size: Option<(u32, u32)>,
) -> Result<(Vec<u8>, 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<u8> = 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<M>(mime_type: M) -> Option<image::ImageFormat>
where
M: AsRef<str>,
{
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,
}
}

View File

@@ -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<ReqwestError> 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,
}

View File

@@ -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;