mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-05-06 23:15:08 -04:00
feat(sdk): Add method to generate thumbnails from images
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user