Added blurhash.rs to fascilitate blurhashing.

Signed-off-by: Niko <cnotsomark@gmail.com>
This commit is contained in:
Niko 2025-02-01 18:35:23 -05:00 committed by Jason Volk
parent 80277f6aa2
commit 62180897c0
11 changed files with 621 additions and 7 deletions

View file

@ -17,6 +17,7 @@ crate-type = [
]
[features]
blurhashing=[]
element_hacks = []
release_max_log_level = [
"tracing/max_level_trace",

View file

@ -62,6 +62,27 @@ pub(crate) async fn create_content_route(
media_id: &utils::random_string(MXC_LENGTH),
};
#[cfg(feature = "blurhashing")]
{
if body.generate_blurhash {
let (blurhash, create_media_result) = tokio::join!(
services
.media
.create_blurhash(&body.file, content_type, filename),
services.media.create(
&mxc,
Some(user),
Some(&content_disposition),
content_type,
&body.file
)
);
return create_media_result.map(|()| create_content::v3::Response {
content_uri: mxc.to_string().into(),
blurhash,
});
}
}
services
.media
.create(&mxc, Some(user), Some(&content_disposition), content_type, &body.file)

View file

@ -54,6 +54,7 @@ sentry_telemetry = []
conduwuit_mods = [
"dep:libloading"
]
blurhashing = []
[dependencies]
argon2.workspace = true

View file

@ -52,7 +52,7 @@ use crate::{err, error::Error, utils::sys, Result};
### For more information, see:
### https://conduwuit.puppyirl.gay/configuration.html
"#,
ignore = "catchall well_known tls"
ignore = "catchall well_known tls blurhashing"
)]
pub struct Config {
/// The server_name is the pretty name of this server. It is used as a
@ -1789,6 +1789,9 @@ pub struct Config {
#[serde(default = "true_fn")]
pub config_reload_signal: bool,
// external structure; separate section
#[serde(default)]
pub blurhashing: BlurhashConfig,
#[serde(flatten)]
#[allow(clippy::zero_sized_map_values)]
// this is a catchall, the map shouldn't be zero at runtime
@ -1839,6 +1842,31 @@ pub struct WellKnownConfig {
pub support_mxid: Option<OwnedUserId>,
}
#[derive(Clone, Copy, Debug, Deserialize, Default)]
#[allow(rustdoc::broken_intra_doc_links, rustdoc::bare_urls)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.blurhashing")]
pub struct BlurhashConfig {
/// blurhashing x component, 4 is recommended by https://blurha.sh/
///
/// default: 4
#[serde(default = "default_blurhash_x_component")]
pub components_x: u32,
/// blurhashing y component, 3 is recommended by https://blurha.sh/
///
/// default: 3
#[serde(default = "default_blurhash_y_component")]
pub components_y: u32,
/// Max raw size that the server will blurhash, this is the size of the
/// image after converting it to raw data, it should be higher than the
/// upload limit but not too high. The higher it is the higher the
/// potential load will be for clients requesting blurhashes. The default
/// is 33.55MB. Setting it to 0 disables blurhashing.
///
/// default: 33554432
#[serde(default = "default_blurhash_max_raw_size")]
pub blurhash_max_raw_size: u64,
}
#[derive(Deserialize, Clone, Debug)]
#[serde(transparent)]
struct ListeningPort {
@ -2210,3 +2238,13 @@ fn default_client_response_timeout() -> u64 { 120 }
fn default_client_shutdown_timeout() -> u64 { 15 }
fn default_sender_shutdown_timeout() -> u64 { 5 }
// blurhashing defaults recommended by https://blurha.sh/
// 2^25
pub(super) fn default_blurhash_max_raw_size() -> u64 { 33_554_432 }
pub(super) fn default_blurhash_x_component() -> u32 { 4 }
pub(super) fn default_blurhash_y_component() -> u32 { 3 }
// end recommended & blurhashing defaults

View file

@ -101,6 +101,7 @@ perf_measurements = [
"conduwuit-core/perf_measurements",
"conduwuit-core/sentry_telemetry",
]
blurhashing =["conduwuit-service/blurhashing","conduwuit-core/blurhashing","conduwuit-api/blurhashing"]
# increases performance, reduces build times, and reduces binary size by not compiling or
# genreating code for log level filters that users will generally not use (debug and trace)
release_max_log_level = [

View file

@ -44,6 +44,7 @@ url_preview = [
zstd_compression = [
"reqwest/zstd",
]
blurhashing = ["dep:image","dep:blurhash"]
[dependencies]
arrayvec.workspace = true
@ -82,6 +83,8 @@ tracing.workspace = true
url.workspace = true
webpage.workspace = true
webpage.optional = true
blurhash.workspace = true
blurhash.optional = true
[lints]
workspace = true

View file

@ -0,0 +1,159 @@
use std::{fmt::Display, io::Cursor, path::Path};
use blurhash::encode_image;
use conduwuit::{config::BlurhashConfig as CoreBlurhashConfig, debug_error, implement, trace};
use image::{DynamicImage, ImageDecoder, ImageError, ImageFormat, ImageReader};
use super::Service;
#[implement(Service)]
pub async fn create_blurhash(
&self,
file: &[u8],
content_type: Option<&str>,
file_name: Option<&str>,
) -> Option<String> {
let config = BlurhashConfig::from(self.services.server.config.blurhashing);
if config.size_limit == 0 {
trace!("since 0 means disabled blurhashing, skipped blurhashing logic");
return None;
}
let file_data = file.to_owned();
let content_type = content_type.map(String::from);
let file_name = file_name.map(String::from);
let blurhashing_result = tokio::task::spawn_blocking(move || {
get_blurhash_from_request(&file_data, content_type, file_name, config)
})
.await
.expect("no join error");
match blurhashing_result {
| Ok(result) => Some(result),
| Err(e) => {
debug_error!("Error when blurhashing: {e}");
None
},
}
}
/// Returns the blurhash or a blurhash error which implements Display.
fn get_blurhash_from_request(
data: &[u8],
mime: Option<String>,
filename: Option<String>,
config: BlurhashConfig,
) -> Result<String, BlurhashingError> {
// Get format image is supposed to be in
let format = get_format_from_data_mime_and_filename(data, mime, filename)?;
// Get the image reader for said image format
let decoder = get_image_decoder_with_format_and_data(format, data)?;
// Check image size makes sense before unpacking whole image
if is_image_above_size_limit(&decoder, config) {
return Err(BlurhashingError::ImageTooLarge);
}
// decode the image finally
let image = DynamicImage::from_decoder(decoder)?;
blurhash_an_image(&image, config)
}
/// Gets the Image Format value from the data,mime, and filename
/// It first checks if the mime is a valid image format
/// Then it checks if the filename has a format, otherwise just guess based on
/// the binary data Assumes that mime and filename extension won't be for a
/// different file format than file.
fn get_format_from_data_mime_and_filename(
data: &[u8],
mime: Option<String>,
filename: Option<String>,
) -> Result<ImageFormat, BlurhashingError> {
let mut image_format = None;
if let Some(mime) = mime {
image_format = ImageFormat::from_mime_type(mime);
}
if let (Some(filename), None) = (filename, image_format) {
if let Some(extension) = Path::new(&filename).extension() {
image_format = ImageFormat::from_mime_type(extension.to_string_lossy());
}
}
if let Some(format) = image_format {
Ok(format)
} else {
image::guess_format(data).map_err(Into::into)
}
}
fn get_image_decoder_with_format_and_data(
image_format: ImageFormat,
data: &[u8],
) -> Result<Box<dyn ImageDecoder + '_>, BlurhashingError> {
let mut image_reader = ImageReader::new(Cursor::new(data));
image_reader.set_format(image_format);
Ok(Box::new(image_reader.into_decoder()?))
}
fn is_image_above_size_limit<T: ImageDecoder>(
decoder: &T,
blurhash_config: BlurhashConfig,
) -> bool {
decoder.total_bytes() >= blurhash_config.size_limit
}
#[inline]
fn blurhash_an_image(
image: &DynamicImage,
blurhash_config: BlurhashConfig,
) -> Result<String, BlurhashingError> {
Ok(encode_image(
blurhash_config.components_x,
blurhash_config.components_y,
&image.to_rgba8(),
)?)
}
#[derive(Clone, Copy)]
pub struct BlurhashConfig {
components_x: u32,
components_y: u32,
/// size limit in bytes
size_limit: u64,
}
impl From<CoreBlurhashConfig> for BlurhashConfig {
fn from(value: CoreBlurhashConfig) -> Self {
Self {
components_x: value.components_x,
components_y: value.components_y,
size_limit: value.blurhash_max_raw_size,
}
}
}
#[derive(Debug)]
pub(crate) enum BlurhashingError {
ImageError(Box<ImageError>),
HashingLibError(Box<blurhash::Error>),
ImageTooLarge,
}
impl From<ImageError> for BlurhashingError {
fn from(value: ImageError) -> Self { Self::ImageError(Box::new(value)) }
}
impl From<blurhash::Error> for BlurhashingError {
fn from(value: blurhash::Error) -> Self { Self::HashingLibError(Box::new(value)) }
}
impl Display for BlurhashingError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Blurhash Error:")?;
match &self {
| Self::ImageTooLarge => write!(f, "Image was too large to blurhash")?,
| Self::HashingLibError(e) =>
write!(f, "There was an error with the blurhashing library => {e}")?,
| Self::ImageError(e) =>
write!(f, "There was an error with the image loading library => {e}")?,
};
Ok(())
}
}

View file

@ -1,10 +1,11 @@
#[cfg(feature = "blurhashing")]
pub mod blurhash;
mod data;
pub(super) mod migrations;
mod preview;
mod remote;
mod tests;
mod thumbnail;
use std::{path::PathBuf, sync::Arc, time::SystemTime};
use async_trait::async_trait;