add thumbnail dimension structure

Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
Jason Volk 2024-08-27 01:15:31 +00:00
parent 7b0e830f4c
commit 4d42a29c51
7 changed files with 155 additions and 114 deletions

26
Cargo.lock generated
View file

@ -2975,7 +2975,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.10.1" version = "0.10.1"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"assign", "assign",
"js_int", "js_int",
@ -2997,7 +2997,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-appservice-api" name = "ruma-appservice-api"
version = "0.10.0" version = "0.10.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3009,7 +3009,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-client-api" name = "ruma-client-api"
version = "0.18.0" version = "0.18.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@ -3032,7 +3032,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-common" name = "ruma-common"
version = "0.13.0" version = "0.13.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@ -3062,7 +3062,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-events" name = "ruma-events"
version = "0.28.1" version = "0.28.1"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap 2.4.0", "indexmap 2.4.0",
@ -3086,7 +3086,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-federation-api" name = "ruma-federation-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http",
@ -3104,7 +3104,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identifiers-validation" name = "ruma-identifiers-validation"
version = "0.9.5" version = "0.9.5"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror", "thiserror",
@ -3113,7 +3113,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identity-service-api" name = "ruma-identity-service-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3123,7 +3123,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-macros" name = "ruma-macros"
version = "0.13.0" version = "0.13.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"once_cell", "once_cell",
"proc-macro-crate", "proc-macro-crate",
@ -3138,7 +3138,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-push-gateway-api" name = "ruma-push-gateway-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@ -3150,7 +3150,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-server-util" name = "ruma-server-util"
version = "0.3.0" version = "0.3.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"headers", "headers",
"http", "http",
@ -3163,7 +3163,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-signatures" name = "ruma-signatures"
version = "0.15.0" version = "0.15.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",
@ -3179,7 +3179,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-state-res" name = "ruma-state-res"
version = "0.11.0" version = "0.11.0"
source = "git+https://github.com/girlbossceo/ruwuma?rev=25fbd64b968c5d5088c07750aaa4873e072831b0#25fbd64b968c5d5088c07750aaa4873e072831b0" source = "git+https://github.com/girlbossceo/ruwuma?rev=a0cc9a80dd5da700fb9b992b6f92cb6be4c27487#a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
dependencies = [ dependencies = [
"itertools 0.12.1", "itertools 0.12.1",
"js_int", "js_int",

View file

@ -314,7 +314,7 @@ version = "0.1.2"
[workspace.dependencies.ruma] [workspace.dependencies.ruma]
git = "https://github.com/girlbossceo/ruwuma" git = "https://github.com/girlbossceo/ruwuma"
#branch = "conduwuit-changes" #branch = "conduwuit-changes"
rev = "25fbd64b968c5d5088c07750aaa4873e072831b0" rev = "a0cc9a80dd5da700fb9b992b6f92cb6be4c27487"
features = [ features = [
"compat", "compat",
"rand", "rand",

View file

@ -14,7 +14,7 @@ use ruma::{
}, },
Mxc, Mxc,
}; };
use service::media::{FileMeta, MXC_LENGTH}; use service::media::{Dim, FileMeta, MXC_LENGTH};
use crate::{Ruma, RumaResponse}; use crate::{Ruma, RumaResponse};
@ -326,22 +326,12 @@ pub(crate) async fn get_content_thumbnail_route(
media_id: &body.media_id, media_id: &body.media_id,
}; };
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
if let Some(FileMeta { if let Some(FileMeta {
content, content,
content_type, content_type,
content_disposition, content_disposition,
}) = services }) = services.media.get_thumbnail(&mxc, &dim).await?
.media
.get_thumbnail(
&mxc,
body.width
.try_into()
.map_err(|e| err!(Request(InvalidParam("Width is invalid: {e:?}"))))?,
body.height
.try_into()
.map_err(|e| err!(Request(InvalidParam("Height is invalid: {e:?}"))))?,
)
.await?
{ {
let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None); let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None);

View file

@ -8,7 +8,7 @@ use conduit::{
use database::{Database, Map}; use database::{Database, Map};
use ruma::{api::client::error::ErrorKind, http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId}; use ruma::{api::client::error::ErrorKind, http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId};
use super::preview::UrlPreviewData; use super::{preview::UrlPreviewData, thumbnail::Dim};
pub(crate) struct Data { pub(crate) struct Data {
mediaid_file: Arc<Map>, mediaid_file: Arc<Map>,
@ -33,8 +33,8 @@ impl Data {
} }
pub(super) fn create_file_metadata( pub(super) fn create_file_metadata(
&self, mxc: &Mxc<'_>, user: Option<&UserId>, width: u32, height: u32, &self, mxc: &Mxc<'_>, user: Option<&UserId>, dim: &Dim, content_disposition: Option<&ContentDisposition>,
content_disposition: Option<&ContentDisposition>, content_type: Option<&str>, content_type: Option<&str>,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
let mut key: Vec<u8> = Vec::new(); let mut key: Vec<u8> = Vec::new();
key.extend_from_slice(b"mxc://"); key.extend_from_slice(b"mxc://");
@ -42,8 +42,8 @@ impl Data {
key.extend_from_slice(b"/"); key.extend_from_slice(b"/");
key.extend_from_slice(mxc.media_id.as_bytes()); key.extend_from_slice(mxc.media_id.as_bytes());
key.push(0xFF); key.push(0xFF);
key.extend_from_slice(&width.to_be_bytes()); key.extend_from_slice(&dim.width.to_be_bytes());
key.extend_from_slice(&height.to_be_bytes()); key.extend_from_slice(&dim.height.to_be_bytes());
key.push(0xFF); key.push(0xFF);
key.extend_from_slice( key.extend_from_slice(
content_disposition content_disposition
@ -128,15 +128,15 @@ impl Data {
Ok(keys) Ok(keys)
} }
pub(super) fn search_file_metadata(&self, mxc: &Mxc<'_>, width: u32, height: u32) -> Result<Metadata> { pub(super) fn search_file_metadata(&self, mxc: &Mxc<'_>, dim: &Dim) -> Result<Metadata> {
let mut prefix: Vec<u8> = Vec::new(); let mut prefix: Vec<u8> = Vec::new();
prefix.extend_from_slice(b"mxc://"); prefix.extend_from_slice(b"mxc://");
prefix.extend_from_slice(mxc.server_name.as_bytes()); prefix.extend_from_slice(mxc.server_name.as_bytes());
prefix.extend_from_slice(b"/"); prefix.extend_from_slice(b"/");
prefix.extend_from_slice(mxc.media_id.as_bytes()); prefix.extend_from_slice(mxc.media_id.as_bytes());
prefix.push(0xFF); prefix.push(0xFF);
prefix.extend_from_slice(&width.to_be_bytes()); prefix.extend_from_slice(&dim.width.to_be_bytes());
prefix.extend_from_slice(&height.to_be_bytes()); prefix.extend_from_slice(&dim.height.to_be_bytes());
prefix.push(0xFF); prefix.push(0xFF);
let (key, _) = self let (key, _) = self

View file

@ -13,13 +13,14 @@ use conduit::{
utils::{self, MutexMap}, utils::{self, MutexMap},
warn, Err, Result, Server, warn, Err, Result, Server,
}; };
use data::{Data, Metadata};
use ruma::{http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId}; use ruma::{http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId};
use tokio::{ use tokio::{
fs, fs,
io::{AsyncReadExt, AsyncWriteExt, BufReader}, io::{AsyncReadExt, AsyncWriteExt, BufReader},
}; };
use self::data::{Data, Metadata};
pub use self::thumbnail::Dim;
use crate::{client, globals, sending, Dep}; use crate::{client, globals, sending, Dep};
#[derive(Debug)] #[derive(Debug)]
@ -78,7 +79,7 @@ impl Service {
// Width, Height = 0 if it's not a thumbnail // Width, Height = 0 if it's not a thumbnail
let key = self let key = self
.db .db
.create_file_metadata(mxc, user, 0, 0, content_disposition, content_type)?; .create_file_metadata(mxc, user, &Dim::default(), content_disposition, content_type)?;
//TODO: Dangling metadata in database if creation fails //TODO: Dangling metadata in database if creation fails
let mut f = self.create_media_file(&key).await?; let mut f = self.create_media_file(&key).await?;
@ -141,7 +142,7 @@ impl Service {
content_disposition, content_disposition,
content_type, content_type,
key, key,
}) = self.db.search_file_metadata(mxc, 0, 0) }) = self.db.search_file_metadata(mxc, &Dim::default())
{ {
let mut content = Vec::new(); let mut content = Vec::new();
let path = self.get_media_file(&key); let path = self.get_media_file(&key);
@ -350,7 +351,7 @@ impl Service {
#[inline] #[inline]
pub fn get_metadata(&self, mxc: &Mxc<'_>) -> Option<FileMeta> { pub fn get_metadata(&self, mxc: &Mxc<'_>) -> Option<FileMeta> {
self.db self.db
.search_file_metadata(mxc, 0, 0) .search_file_metadata(mxc, &Dim::default())
.map(|metadata| FileMeta { .map(|metadata| FileMeta {
content_disposition: metadata.content_disposition, content_disposition: metadata.content_disposition,
content_type: metadata.content_type, content_type: metadata.content_type,

View file

@ -3,6 +3,8 @@ use std::time::Duration;
use conduit::{debug_warn, err, implement, utils::content_disposition::make_content_disposition, Err, Error, Result}; use conduit::{debug_warn, err, implement, utils::content_disposition::make_content_disposition, Err, Error, Result};
use ruma::{api::client::media, Mxc}; use ruma::{api::client::media, Mxc};
use super::Dim;
#[implement(super::Service)] #[implement(super::Service)]
#[allow(deprecated)] #[allow(deprecated)]
pub async fn fetch_remote_thumbnail_legacy( pub async fn fetch_remote_thumbnail_legacy(
@ -33,19 +35,8 @@ pub async fn fetch_remote_thumbnail_legacy(
) )
.await?; .await?;
self.upload_thumbnail( let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
&mxc, self.upload_thumbnail(&mxc, None, None, reponse.content_type.as_deref(), &dim, &reponse.file)
None,
None,
reponse.content_type.as_deref(),
body.width
.try_into()
.map_err(|e| err!(Request(InvalidParam("Width is invalid: {e:?}"))))?,
body.height
.try_into()
.map_err(|e| err!(Request(InvalidParam("Height is invalid: {e:?}"))))?,
&reponse.file,
)
.await?; .await?;
Ok(reponse) Ok(reponse)

View file

@ -1,8 +1,8 @@
use std::{cmp, io::Cursor, num::Saturating as Sat}; use std::{cmp, io::Cursor, num::Saturating as Sat};
use conduit::{checked, Result}; use conduit::{checked, err, Result};
use image::{imageops::FilterType, DynamicImage}; use image::{imageops::FilterType, DynamicImage};
use ruma::{http_headers::ContentDisposition, Mxc, UserId}; use ruma::{http_headers::ContentDisposition, media::Method, Mxc, UInt, UserId};
use tokio::{ use tokio::{
fs, fs,
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
@ -10,16 +10,24 @@ use tokio::{
use super::{data::Metadata, FileMeta}; use super::{data::Metadata, FileMeta};
/// Dimension specification for a thumbnail.
#[derive(Debug)]
pub struct Dim {
pub width: u32,
pub height: u32,
pub method: Method,
}
impl super::Service { impl super::Service {
/// Uploads or replaces a file thumbnail. /// Uploads or replaces a file thumbnail.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn upload_thumbnail( pub async fn upload_thumbnail(
&self, mxc: &Mxc<'_>, user: Option<&UserId>, content_disposition: Option<&ContentDisposition>, &self, mxc: &Mxc<'_>, user: Option<&UserId>, content_disposition: Option<&ContentDisposition>,
content_type: Option<&str>, width: u32, height: u32, file: &[u8], content_type: Option<&str>, dim: &Dim, file: &[u8],
) -> Result<()> { ) -> Result<()> {
let key = self let key = self
.db .db
.create_file_metadata(mxc, user, width, height, content_disposition, content_type)?; .create_file_metadata(mxc, user, dim, content_disposition, content_type)?;
//TODO: Dangling metadata in database if creation fails //TODO: Dangling metadata in database if creation fails
let mut f = self.create_media_file(&key).await?; let mut f = self.create_media_file(&key).await?;
@ -42,15 +50,14 @@ impl super::Service {
/// For width,height <= 96 the server uses another thumbnailing algorithm /// For width,height <= 96 the server uses another thumbnailing algorithm
/// which crops the image afterwards. /// which crops the image afterwards.
#[tracing::instrument(skip(self), name = "thumbnail", level = "debug")] #[tracing::instrument(skip(self), name = "thumbnail", level = "debug")]
pub async fn get_thumbnail(&self, mxc: &Mxc<'_>, width: u32, height: u32) -> Result<Option<FileMeta>> { pub async fn get_thumbnail(&self, mxc: &Mxc<'_>, dim: &Dim) -> Result<Option<FileMeta>> {
// 0, 0 because that's the original file // 0, 0 because that's the original file
let (width, height, crop) = thumbnail_properties(width, height).unwrap_or((0, 0, false)); let dim = dim.normalized();
if let Ok(metadata) = self.db.search_file_metadata(mxc, width, height) { if let Ok(metadata) = self.db.search_file_metadata(mxc, &dim) {
self.get_thumbnail_saved(metadata).await self.get_thumbnail_saved(metadata).await
} else if let Ok(metadata) = self.db.search_file_metadata(mxc, 0, 0) { } else if let Ok(metadata) = self.db.search_file_metadata(mxc, &Dim::default()) {
self.get_thumbnail_generate(mxc, width, height, crop, metadata) self.get_thumbnail_generate(mxc, &dim, metadata).await
.await
} else { } else {
Ok(None) Ok(None)
} }
@ -71,9 +78,7 @@ impl super::Service {
/// Generate a thumbnail /// Generate a thumbnail
#[tracing::instrument(skip(self), name = "generate", level = "debug")] #[tracing::instrument(skip(self), name = "generate", level = "debug")]
async fn get_thumbnail_generate( async fn get_thumbnail_generate(&self, mxc: &Mxc<'_>, dim: &Dim, data: Metadata) -> Result<Option<FileMeta>> {
&self, mxc: &Mxc<'_>, width: u32, height: u32, crop: bool, data: Metadata,
) -> Result<Option<FileMeta>> {
let mut content = Vec::new(); let mut content = Vec::new();
let path = self.get_media_file(&data.key); let path = self.get_media_file(&data.key);
fs::File::open(path) fs::File::open(path)
@ -86,20 +91,19 @@ impl super::Service {
return Ok(Some(into_filemeta(data, content))); return Ok(Some(into_filemeta(data, content)));
}; };
if width > image.width() || height > image.height() { if dim.width > image.width() || dim.height > image.height() {
return Ok(Some(into_filemeta(data, content))); return Ok(Some(into_filemeta(data, content)));
} }
let mut thumbnail_bytes = Vec::new(); let mut thumbnail_bytes = Vec::new();
let thumbnail = thumbnail_generate(&image, width, height, crop)?; let thumbnail = thumbnail_generate(&image, dim)?;
thumbnail.write_to(&mut Cursor::new(&mut thumbnail_bytes), image::ImageFormat::Png)?; thumbnail.write_to(&mut Cursor::new(&mut thumbnail_bytes), image::ImageFormat::Png)?;
// Save thumbnail in database so we don't have to generate it again next time // Save thumbnail in database so we don't have to generate it again next time
let thumbnail_key = self.db.create_file_metadata( let thumbnail_key = self.db.create_file_metadata(
mxc, mxc,
None, None,
width, dim,
height,
data.content_disposition.as_ref(), data.content_disposition.as_ref(),
data.content_type.as_deref(), data.content_type.as_deref(),
)?; )?;
@ -111,23 +115,63 @@ impl super::Service {
} }
} }
fn thumbnail_generate(image: &DynamicImage, width: u32, height: u32, crop: bool) -> Result<DynamicImage> { fn thumbnail_generate(image: &DynamicImage, requested: &Dim) -> Result<DynamicImage> {
let thumbnail = if crop { let thumbnail = if !requested.crop() {
image.resize_to_fill(width, height, FilterType::CatmullRom) let Dim {
width,
height,
..
} = requested.scaled(&Dim {
width: image.width(),
height: image.height(),
..Dim::default()
})?;
image.thumbnail_exact(width, height)
} else { } else {
let (exact_width, exact_height) = thumbnail_dimension(image, width, height)?; image.resize_to_fill(requested.width, requested.height, FilterType::CatmullRom)
image.thumbnail_exact(exact_width, exact_height)
}; };
Ok(thumbnail) Ok(thumbnail)
} }
fn thumbnail_dimension(image: &DynamicImage, width: u32, height: u32) -> Result<(u32, u32)> { fn into_filemeta(data: Metadata, content: Vec<u8>) -> FileMeta {
let image_width = image.width(); FileMeta {
let image_height = image.height(); content: Some(content),
content_type: data.content_type,
content_disposition: data.content_disposition,
}
}
let width = cmp::min(width, image_width); impl Dim {
let height = cmp::min(height, image_height); /// Instantiate a Dim from Ruma integers with optional method.
pub fn from_ruma(width: UInt, height: UInt, method: Option<Method>) -> Result<Self> {
let width = width
.try_into()
.map_err(|e| err!(Request(InvalidParam("Width is invalid: {e:?}"))))?;
let height = height
.try_into()
.map_err(|e| err!(Request(InvalidParam("Height is invalid: {e:?}"))))?;
Ok(Self::new(width, height, method))
}
/// Instantiate a Dim with optional method
#[inline]
#[must_use]
pub fn new(width: u32, height: u32, method: Option<Method>) -> Self {
Self {
width,
height,
method: method.unwrap_or(Method::Scale),
}
}
pub fn scaled(&self, image: &Self) -> Result<Self> {
let image_width = image.width;
let image_height = image.height;
let width = cmp::min(self.width, image_width);
let height = cmp::min(self.height, image_height);
let use_width = Sat(width) * Sat(image_height) < Sat(height) * Sat(image_width); let use_width = Sat(width) * Sat(image_height) < Sat(height) * Sat(image_width);
@ -145,26 +189,41 @@ fn thumbnail_dimension(image: &DynamicImage, width: u32, height: u32) -> Result<
height height
}; };
Ok((x, y)) Ok(Self {
width: x,
height: y,
method: Method::Scale,
})
}
/// Returns width, height of the thumbnail and whether it should be cropped.
/// Returns None when the server should send the original file.
/// Ignores the input Method.
#[must_use]
pub fn normalized(&self) -> Self {
match (self.width, self.height) {
(0..=32, 0..=32) => Self::new(32, 32, Some(Method::Crop)),
(0..=96, 0..=96) => Self::new(96, 96, Some(Method::Crop)),
(0..=320, 0..=240) => Self::new(320, 240, Some(Method::Scale)),
(0..=640, 0..=480) => Self::new(640, 480, Some(Method::Scale)),
(0..=800, 0..=600) => Self::new(800, 600, Some(Method::Scale)),
_ => Self::default(),
}
}
/// Returns true if the method is Crop.
#[inline]
#[must_use]
pub fn crop(&self) -> bool { self.method == Method::Crop }
} }
/// Returns width, height of the thumbnail and whether it should be cropped. impl Default for Dim {
/// Returns None when the server should send the original file. #[inline]
fn thumbnail_properties(width: u32, height: u32) -> Option<(u32, u32, bool)> { fn default() -> Self {
match (width, height) { Self {
(0..=32, 0..=32) => Some((32, 32, true)), width: 0,
(0..=96, 0..=96) => Some((96, 96, true)), height: 0,
(0..=320, 0..=240) => Some((320, 240, false)), method: Method::Scale,
(0..=640, 0..=480) => Some((640, 480, false)), }
(0..=800, 0..=600) => Some((800, 600, false)),
_ => None,
}
}
fn into_filemeta(data: Metadata, content: Vec<u8>) -> FileMeta {
FileMeta {
content: Some(content),
content_type: data.content_type,
content_disposition: data.content_disposition,
} }
} }