diff --git a/Cargo.lock b/Cargo.lock index ef18182c..fabb9b09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2975,7 +2975,7 @@ dependencies = [ [[package]] name = "ruma" 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 = [ "assign", "js_int", @@ -2997,7 +2997,7 @@ dependencies = [ [[package]] name = "ruma-appservice-api" 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 = [ "js_int", "ruma-common", @@ -3009,7 +3009,7 @@ dependencies = [ [[package]] name = "ruma-client-api" 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 = [ "as_variant", "assign", @@ -3032,7 +3032,7 @@ dependencies = [ [[package]] name = "ruma-common" 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 = [ "as_variant", "base64 0.22.1", @@ -3062,7 +3062,7 @@ dependencies = [ [[package]] name = "ruma-events" 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 = [ "as_variant", "indexmap 2.4.0", @@ -3086,7 +3086,7 @@ dependencies = [ [[package]] name = "ruma-federation-api" 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 = [ "bytes", "http", @@ -3104,7 +3104,7 @@ dependencies = [ [[package]] name = "ruma-identifiers-validation" 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 = [ "js_int", "thiserror", @@ -3113,7 +3113,7 @@ dependencies = [ [[package]] name = "ruma-identity-service-api" 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 = [ "js_int", "ruma-common", @@ -3123,7 +3123,7 @@ dependencies = [ [[package]] name = "ruma-macros" 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 = [ "once_cell", "proc-macro-crate", @@ -3138,7 +3138,7 @@ dependencies = [ [[package]] name = "ruma-push-gateway-api" 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 = [ "js_int", "ruma-common", @@ -3150,7 +3150,7 @@ dependencies = [ [[package]] name = "ruma-server-util" 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 = [ "headers", "http", @@ -3163,7 +3163,7 @@ dependencies = [ [[package]] name = "ruma-signatures" 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 = [ "base64 0.22.1", "ed25519-dalek", @@ -3179,7 +3179,7 @@ dependencies = [ [[package]] name = "ruma-state-res" 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 = [ "itertools 0.12.1", "js_int", diff --git a/Cargo.toml b/Cargo.toml index a9269d08..c6793c44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -314,7 +314,7 @@ version = "0.1.2" [workspace.dependencies.ruma] git = "https://github.com/girlbossceo/ruwuma" #branch = "conduwuit-changes" -rev = "25fbd64b968c5d5088c07750aaa4873e072831b0" +rev = "a0cc9a80dd5da700fb9b992b6f92cb6be4c27487" features = [ "compat", "rand", diff --git a/src/api/client/media.rs b/src/api/client/media.rs index 9553d425..41a34d75 100644 --- a/src/api/client/media.rs +++ b/src/api/client/media.rs @@ -14,7 +14,7 @@ use ruma::{ }, Mxc, }; -use service::media::{FileMeta, MXC_LENGTH}; +use service::media::{Dim, FileMeta, MXC_LENGTH}; use crate::{Ruma, RumaResponse}; @@ -326,22 +326,12 @@ pub(crate) async fn get_content_thumbnail_route( media_id: &body.media_id, }; + let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?; if let Some(FileMeta { content, content_type, content_disposition, - }) = services - .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? + }) = services.media.get_thumbnail(&mxc, &dim).await? { let content_disposition = make_content_disposition(content_disposition.as_ref(), content_type.as_deref(), None); diff --git a/src/service/media/data.rs b/src/service/media/data.rs index 06cd6cc7..e5d6d20b 100644 --- a/src/service/media/data.rs +++ b/src/service/media/data.rs @@ -8,7 +8,7 @@ use conduit::{ use database::{Database, Map}; 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 { mediaid_file: Arc, @@ -33,8 +33,8 @@ impl Data { } pub(super) fn create_file_metadata( - &self, mxc: &Mxc<'_>, user: Option<&UserId>, width: u32, height: u32, - content_disposition: Option<&ContentDisposition>, content_type: Option<&str>, + &self, mxc: &Mxc<'_>, user: Option<&UserId>, dim: &Dim, content_disposition: Option<&ContentDisposition>, + content_type: Option<&str>, ) -> Result> { let mut key: Vec = Vec::new(); key.extend_from_slice(b"mxc://"); @@ -42,8 +42,8 @@ impl Data { key.extend_from_slice(b"/"); key.extend_from_slice(mxc.media_id.as_bytes()); key.push(0xFF); - key.extend_from_slice(&width.to_be_bytes()); - key.extend_from_slice(&height.to_be_bytes()); + key.extend_from_slice(&dim.width.to_be_bytes()); + key.extend_from_slice(&dim.height.to_be_bytes()); key.push(0xFF); key.extend_from_slice( content_disposition @@ -128,15 +128,15 @@ impl Data { Ok(keys) } - pub(super) fn search_file_metadata(&self, mxc: &Mxc<'_>, width: u32, height: u32) -> Result { + pub(super) fn search_file_metadata(&self, mxc: &Mxc<'_>, dim: &Dim) -> Result { let mut prefix: Vec = Vec::new(); prefix.extend_from_slice(b"mxc://"); prefix.extend_from_slice(mxc.server_name.as_bytes()); prefix.extend_from_slice(b"/"); prefix.extend_from_slice(mxc.media_id.as_bytes()); prefix.push(0xFF); - prefix.extend_from_slice(&width.to_be_bytes()); - prefix.extend_from_slice(&height.to_be_bytes()); + prefix.extend_from_slice(&dim.width.to_be_bytes()); + prefix.extend_from_slice(&dim.height.to_be_bytes()); prefix.push(0xFF); let (key, _) = self diff --git a/src/service/media/mod.rs b/src/service/media/mod.rs index 5b12a94c..5e3f82b3 100644 --- a/src/service/media/mod.rs +++ b/src/service/media/mod.rs @@ -13,13 +13,14 @@ use conduit::{ utils::{self, MutexMap}, warn, Err, Result, Server, }; -use data::{Data, Metadata}; use ruma::{http_headers::ContentDisposition, Mxc, OwnedMxcUri, UserId}; use tokio::{ fs, io::{AsyncReadExt, AsyncWriteExt, BufReader}, }; +use self::data::{Data, Metadata}; +pub use self::thumbnail::Dim; use crate::{client, globals, sending, Dep}; #[derive(Debug)] @@ -78,7 +79,7 @@ impl Service { // Width, Height = 0 if it's not a thumbnail let key = self .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 let mut f = self.create_media_file(&key).await?; @@ -141,7 +142,7 @@ impl Service { content_disposition, content_type, key, - }) = self.db.search_file_metadata(mxc, 0, 0) + }) = self.db.search_file_metadata(mxc, &Dim::default()) { let mut content = Vec::new(); let path = self.get_media_file(&key); @@ -350,7 +351,7 @@ impl Service { #[inline] pub fn get_metadata(&self, mxc: &Mxc<'_>) -> Option { self.db - .search_file_metadata(mxc, 0, 0) + .search_file_metadata(mxc, &Dim::default()) .map(|metadata| FileMeta { content_disposition: metadata.content_disposition, content_type: metadata.content_type, diff --git a/src/service/media/remote.rs b/src/service/media/remote.rs index d4c458e2..41e87dbb 100644 --- a/src/service/media/remote.rs +++ b/src/service/media/remote.rs @@ -3,6 +3,8 @@ use std::time::Duration; use conduit::{debug_warn, err, implement, utils::content_disposition::make_content_disposition, Err, Error, Result}; use ruma::{api::client::media, Mxc}; +use super::Dim; + #[implement(super::Service)] #[allow(deprecated)] pub async fn fetch_remote_thumbnail_legacy( @@ -33,20 +35,9 @@ pub async fn fetch_remote_thumbnail_legacy( ) .await?; - self.upload_thumbnail( - &mxc, - 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?; + let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?; + self.upload_thumbnail(&mxc, None, None, reponse.content_type.as_deref(), &dim, &reponse.file) + .await?; Ok(reponse) } diff --git a/src/service/media/thumbnail.rs b/src/service/media/thumbnail.rs index 3aadcfc0..630f7b3b 100644 --- a/src/service/media/thumbnail.rs +++ b/src/service/media/thumbnail.rs @@ -1,8 +1,8 @@ use std::{cmp, io::Cursor, num::Saturating as Sat}; -use conduit::{checked, Result}; +use conduit::{checked, err, Result}; use image::{imageops::FilterType, DynamicImage}; -use ruma::{http_headers::ContentDisposition, Mxc, UserId}; +use ruma::{http_headers::ContentDisposition, media::Method, Mxc, UInt, UserId}; use tokio::{ fs, io::{AsyncReadExt, AsyncWriteExt}, @@ -10,16 +10,24 @@ use tokio::{ 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 { /// Uploads or replaces a file thumbnail. #[allow(clippy::too_many_arguments)] pub async fn upload_thumbnail( &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<()> { let key = self .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 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 /// which crops the image afterwards. #[tracing::instrument(skip(self), name = "thumbnail", level = "debug")] - pub async fn get_thumbnail(&self, mxc: &Mxc<'_>, width: u32, height: u32) -> Result> { + pub async fn get_thumbnail(&self, mxc: &Mxc<'_>, dim: &Dim) -> Result> { // 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 - } else if let Ok(metadata) = self.db.search_file_metadata(mxc, 0, 0) { - self.get_thumbnail_generate(mxc, width, height, crop, metadata) - .await + } else if let Ok(metadata) = self.db.search_file_metadata(mxc, &Dim::default()) { + self.get_thumbnail_generate(mxc, &dim, metadata).await } else { Ok(None) } @@ -71,9 +78,7 @@ impl super::Service { /// Generate a thumbnail #[tracing::instrument(skip(self), name = "generate", level = "debug")] - async fn get_thumbnail_generate( - &self, mxc: &Mxc<'_>, width: u32, height: u32, crop: bool, data: Metadata, - ) -> Result> { + async fn get_thumbnail_generate(&self, mxc: &Mxc<'_>, dim: &Dim, data: Metadata) -> Result> { let mut content = Vec::new(); let path = self.get_media_file(&data.key); fs::File::open(path) @@ -86,20 +91,19 @@ impl super::Service { 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))); } 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)?; // Save thumbnail in database so we don't have to generate it again next time let thumbnail_key = self.db.create_file_metadata( mxc, None, - width, - height, + dim, data.content_disposition.as_ref(), data.content_type.as_deref(), )?; @@ -111,56 +115,25 @@ impl super::Service { } } -fn thumbnail_generate(image: &DynamicImage, width: u32, height: u32, crop: bool) -> Result { - let thumbnail = if crop { - image.resize_to_fill(width, height, FilterType::CatmullRom) +fn thumbnail_generate(image: &DynamicImage, requested: &Dim) -> Result { + let thumbnail = if !requested.crop() { + let Dim { + width, + height, + .. + } = requested.scaled(&Dim { + width: image.width(), + height: image.height(), + ..Dim::default() + })?; + image.thumbnail_exact(width, height) } else { - let (exact_width, exact_height) = thumbnail_dimension(image, width, height)?; - image.thumbnail_exact(exact_width, exact_height) + image.resize_to_fill(requested.width, requested.height, FilterType::CatmullRom) }; Ok(thumbnail) } -fn thumbnail_dimension(image: &DynamicImage, width: u32, height: u32) -> Result<(u32, u32)> { - let image_width = image.width(); - let image_height = image.height(); - - let width = cmp::min(width, image_width); - let height = cmp::min(height, image_height); - - let use_width = Sat(width) * Sat(image_height) < Sat(height) * Sat(image_width); - - let x = if use_width { - let dividend = (Sat(height) * Sat(image_width)).0; - checked!(dividend / image_height)? - } else { - width - }; - - let y = if !use_width { - let dividend = (Sat(width) * Sat(image_height)).0; - checked!(dividend / image_width)? - } else { - height - }; - - Ok((x, y)) -} - -/// Returns width, height of the thumbnail and whether it should be cropped. -/// Returns None when the server should send the original file. -fn thumbnail_properties(width: u32, height: u32) -> Option<(u32, u32, bool)> { - match (width, height) { - (0..=32, 0..=32) => Some((32, 32, true)), - (0..=96, 0..=96) => Some((96, 96, true)), - (0..=320, 0..=240) => Some((320, 240, false)), - (0..=640, 0..=480) => Some((640, 480, false)), - (0..=800, 0..=600) => Some((800, 600, false)), - _ => None, - } -} - fn into_filemeta(data: Metadata, content: Vec) -> FileMeta { FileMeta { content: Some(content), @@ -168,3 +141,89 @@ fn into_filemeta(data: Metadata, content: Vec) -> FileMeta { content_disposition: data.content_disposition, } } + +impl Dim { + /// Instantiate a Dim from Ruma integers with optional method. + pub fn from_ruma(width: UInt, height: UInt, method: Option) -> Result { + 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) -> Self { + Self { + width, + height, + method: method.unwrap_or(Method::Scale), + } + } + + pub fn scaled(&self, image: &Self) -> Result { + 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 x = if use_width { + let dividend = (Sat(height) * Sat(image_width)).0; + checked!(dividend / image_height)? + } else { + width + }; + + let y = if !use_width { + let dividend = (Sat(width) * Sat(image_height)).0; + checked!(dividend / image_width)? + } else { + height + }; + + 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 } +} + +impl Default for Dim { + #[inline] + fn default() -> Self { + Self { + width: 0, + height: 0, + method: Method::Scale, + } + } +}