improvements on blurhashing feature
Signed-off-by: Jason Volk <jason@zemos.net>
This commit is contained in:
parent
62180897c0
commit
442bb9889c
7 changed files with 87 additions and 81 deletions
|
@ -194,8 +194,10 @@ features = [
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = [
|
features = [
|
||||||
"fast-linear-to-srgb","image"
|
"fast-linear-to-srgb",
|
||||||
|
"image",
|
||||||
]
|
]
|
||||||
|
|
||||||
# logging
|
# logging
|
||||||
[workspace.dependencies.log]
|
[workspace.dependencies.log]
|
||||||
version = "0.4.22"
|
version = "0.4.22"
|
||||||
|
|
|
@ -17,7 +17,6 @@ crate-type = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
blurhashing=[]
|
|
||||||
element_hacks = []
|
element_hacks = []
|
||||||
release_max_log_level = [
|
release_max_log_level = [
|
||||||
"tracing/max_level_trace",
|
"tracing/max_level_trace",
|
||||||
|
|
|
@ -57,39 +57,27 @@ pub(crate) async fn create_content_route(
|
||||||
let filename = body.filename.as_deref();
|
let filename = body.filename.as_deref();
|
||||||
let content_type = body.content_type.as_deref();
|
let content_type = body.content_type.as_deref();
|
||||||
let content_disposition = make_content_disposition(None, content_type, filename);
|
let content_disposition = make_content_disposition(None, content_type, filename);
|
||||||
let mxc = Mxc {
|
let ref mxc = Mxc {
|
||||||
server_name: services.globals.server_name(),
|
server_name: services.globals.server_name(),
|
||||||
media_id: &utils::random_string(MXC_LENGTH),
|
media_id: &utils::random_string(MXC_LENGTH),
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "blurhashing")]
|
|
||||||
{
|
|
||||||
if body.generate_blurhash {
|
|
||||||
let (blurhash, create_media_result) = tokio::join!(
|
|
||||||
services
|
services
|
||||||
.media
|
.media
|
||||||
.create_blurhash(&body.file, content_type, filename),
|
.create(mxc, Some(user), Some(&content_disposition), content_type, &body.file)
|
||||||
services.media.create(
|
.await?;
|
||||||
&mxc,
|
|
||||||
Some(user),
|
let blurhash = body.generate_blurhash.then(|| {
|
||||||
Some(&content_disposition),
|
services
|
||||||
content_type,
|
.media
|
||||||
&body.file
|
.create_blurhash(&body.file, content_type, filename)
|
||||||
)
|
.ok()
|
||||||
);
|
.flatten()
|
||||||
return create_media_result.map(|()| create_content::v3::Response {
|
|
||||||
content_uri: mxc.to_string().into(),
|
|
||||||
blurhash,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
Ok(create_content::v3::Response {
|
||||||
services
|
|
||||||
.media
|
|
||||||
.create(&mxc, Some(user), Some(&content_disposition), content_type, &body.file)
|
|
||||||
.await
|
|
||||||
.map(|()| create_content::v3::Response {
|
|
||||||
content_uri: mxc.to_string().into(),
|
content_uri: mxc.to_string().into(),
|
||||||
blurhash: None,
|
blurhash: blurhash.flatten(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,6 @@ sentry_telemetry = []
|
||||||
conduwuit_mods = [
|
conduwuit_mods = [
|
||||||
"dep:libloading"
|
"dep:libloading"
|
||||||
]
|
]
|
||||||
blurhashing = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
argon2.workspace = true
|
argon2.workspace = true
|
||||||
|
|
|
@ -49,6 +49,9 @@ default = [
|
||||||
"zstd_compression",
|
"zstd_compression",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
blurhashing = [
|
||||||
|
"conduwuit-service/blurhashing",
|
||||||
|
]
|
||||||
brotli_compression = [
|
brotli_compression = [
|
||||||
"conduwuit-api/brotli_compression",
|
"conduwuit-api/brotli_compression",
|
||||||
"conduwuit-core/brotli_compression",
|
"conduwuit-core/brotli_compression",
|
||||||
|
@ -101,7 +104,6 @@ perf_measurements = [
|
||||||
"conduwuit-core/perf_measurements",
|
"conduwuit-core/perf_measurements",
|
||||||
"conduwuit-core/sentry_telemetry",
|
"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
|
# 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)
|
# genreating code for log level filters that users will generally not use (debug and trace)
|
||||||
release_max_log_level = [
|
release_max_log_level = [
|
||||||
|
|
|
@ -1,56 +1,58 @@
|
||||||
use std::{fmt::Display, io::Cursor, path::Path};
|
use std::{error::Error, ffi::OsStr, fmt::Display, io::Cursor, path::Path};
|
||||||
|
|
||||||
use blurhash::encode_image;
|
use conduwuit::{config::BlurhashConfig as CoreBlurhashConfig, err, implement, Result};
|
||||||
use conduwuit::{config::BlurhashConfig as CoreBlurhashConfig, debug_error, implement, trace};
|
|
||||||
use image::{DynamicImage, ImageDecoder, ImageError, ImageFormat, ImageReader};
|
use image::{DynamicImage, ImageDecoder, ImageError, ImageFormat, ImageReader};
|
||||||
|
|
||||||
use super::Service;
|
use super::Service;
|
||||||
#[implement(Service)]
|
#[implement(Service)]
|
||||||
pub async fn create_blurhash(
|
pub fn create_blurhash(
|
||||||
&self,
|
&self,
|
||||||
file: &[u8],
|
file: &[u8],
|
||||||
content_type: Option<&str>,
|
content_type: Option<&str>,
|
||||||
file_name: Option<&str>,
|
file_name: Option<&str>,
|
||||||
) -> Option<String> {
|
) -> Result<Option<String>> {
|
||||||
|
if !cfg!(feature = "blurhashing") {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
let config = BlurhashConfig::from(self.services.server.config.blurhashing);
|
let config = BlurhashConfig::from(self.services.server.config.blurhashing);
|
||||||
|
|
||||||
|
// since 0 means disabled blurhashing, skipped blurhashing
|
||||||
if config.size_limit == 0 {
|
if config.size_limit == 0 {
|
||||||
trace!("since 0 means disabled blurhashing, skipped blurhashing logic");
|
return Ok(None);
|
||||||
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, content_type, file_name, config)
|
||||||
get_blurhash_from_request(&file_data, content_type, file_name, config)
|
.map_err(|e| err!(debug_error!("blurhashing error: {e}")))
|
||||||
})
|
.map(Some)
|
||||||
.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.
|
/// Returns the blurhash or a blurhash error which implements Display.
|
||||||
|
#[tracing::instrument(
|
||||||
|
name = "blurhash",
|
||||||
|
level = "debug",
|
||||||
|
skip(data),
|
||||||
|
fields(
|
||||||
|
bytes = data.len(),
|
||||||
|
),
|
||||||
|
)]
|
||||||
fn get_blurhash_from_request(
|
fn get_blurhash_from_request(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
mime: Option<String>,
|
mime: Option<&str>,
|
||||||
filename: Option<String>,
|
filename: Option<&str>,
|
||||||
config: BlurhashConfig,
|
config: BlurhashConfig,
|
||||||
) -> Result<String, BlurhashingError> {
|
) -> Result<String, BlurhashingError> {
|
||||||
// Get format image is supposed to be in
|
// Get format image is supposed to be in
|
||||||
let format = get_format_from_data_mime_and_filename(data, mime, filename)?;
|
let format = get_format_from_data_mime_and_filename(data, mime, filename)?;
|
||||||
|
|
||||||
// Get the image reader for said image format
|
// Get the image reader for said image format
|
||||||
let decoder = get_image_decoder_with_format_and_data(format, data)?;
|
let decoder = get_image_decoder_with_format_and_data(format, data)?;
|
||||||
|
|
||||||
// Check image size makes sense before unpacking whole image
|
// Check image size makes sense before unpacking whole image
|
||||||
if is_image_above_size_limit(&decoder, config) {
|
if is_image_above_size_limit(&decoder, config) {
|
||||||
return Err(BlurhashingError::ImageTooLarge);
|
return Err(BlurhashingError::ImageTooLarge);
|
||||||
}
|
}
|
||||||
|
|
||||||
// decode the image finally
|
// decode the image finally
|
||||||
let image = DynamicImage::from_decoder(decoder)?;
|
let image = DynamicImage::from_decoder(decoder)?;
|
||||||
|
|
||||||
|
@ -64,24 +66,17 @@ fn get_blurhash_from_request(
|
||||||
/// different file format than file.
|
/// different file format than file.
|
||||||
fn get_format_from_data_mime_and_filename(
|
fn get_format_from_data_mime_and_filename(
|
||||||
data: &[u8],
|
data: &[u8],
|
||||||
mime: Option<String>,
|
mime: Option<&str>,
|
||||||
filename: Option<String>,
|
filename: Option<&str>,
|
||||||
) -> Result<ImageFormat, BlurhashingError> {
|
) -> Result<ImageFormat, BlurhashingError> {
|
||||||
let mut image_format = None;
|
let extension = filename
|
||||||
if let Some(mime) = mime {
|
.map(Path::new)
|
||||||
image_format = ImageFormat::from_mime_type(mime);
|
.and_then(Path::extension)
|
||||||
}
|
.map(OsStr::to_string_lossy);
|
||||||
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 {
|
mime.or(extension.as_deref())
|
||||||
Ok(format)
|
.and_then(ImageFormat::from_mime_type)
|
||||||
} else {
|
.map_or_else(|| image::guess_format(data).map_err(Into::into), Ok)
|
||||||
image::guess_format(data).map_err(Into::into)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_image_decoder_with_format_and_data(
|
fn get_image_decoder_with_format_and_data(
|
||||||
|
@ -99,23 +94,37 @@ fn is_image_above_size_limit<T: ImageDecoder>(
|
||||||
) -> bool {
|
) -> bool {
|
||||||
decoder.total_bytes() >= blurhash_config.size_limit
|
decoder.total_bytes() >= blurhash_config.size_limit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "blurhashing")]
|
||||||
|
#[tracing::instrument(name = "encode", level = "debug", skip_all)]
|
||||||
#[inline]
|
#[inline]
|
||||||
fn blurhash_an_image(
|
fn blurhash_an_image(
|
||||||
image: &DynamicImage,
|
image: &DynamicImage,
|
||||||
blurhash_config: BlurhashConfig,
|
blurhash_config: BlurhashConfig,
|
||||||
) -> Result<String, BlurhashingError> {
|
) -> Result<String, BlurhashingError> {
|
||||||
Ok(encode_image(
|
Ok(blurhash::encode_image(
|
||||||
blurhash_config.components_x,
|
blurhash_config.components_x,
|
||||||
blurhash_config.components_y,
|
blurhash_config.components_y,
|
||||||
&image.to_rgba8(),
|
&image.to_rgba8(),
|
||||||
)?)
|
)?)
|
||||||
}
|
}
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
|
#[cfg(not(feature = "blurhashing"))]
|
||||||
|
#[inline]
|
||||||
|
fn blurhash_an_image(
|
||||||
|
_image: &DynamicImage,
|
||||||
|
_blurhash_config: BlurhashConfig,
|
||||||
|
) -> Result<String, BlurhashingError> {
|
||||||
|
Err(BlurhashingError::Unavailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct BlurhashConfig {
|
pub struct BlurhashConfig {
|
||||||
components_x: u32,
|
pub components_x: u32,
|
||||||
components_y: u32,
|
pub components_y: u32,
|
||||||
|
|
||||||
/// size limit in bytes
|
/// size limit in bytes
|
||||||
size_limit: u64,
|
pub size_limit: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CoreBlurhashConfig> for BlurhashConfig {
|
impl From<CoreBlurhashConfig> for BlurhashConfig {
|
||||||
|
@ -129,15 +138,20 @@ impl From<CoreBlurhashConfig> for BlurhashConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum BlurhashingError {
|
pub enum BlurhashingError {
|
||||||
|
HashingLibError(Box<dyn Error + Send>),
|
||||||
ImageError(Box<ImageError>),
|
ImageError(Box<ImageError>),
|
||||||
HashingLibError(Box<blurhash::Error>),
|
|
||||||
ImageTooLarge,
|
ImageTooLarge,
|
||||||
|
|
||||||
|
#[cfg(not(feature = "blurhashing"))]
|
||||||
|
Unavailable,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ImageError> for BlurhashingError {
|
impl From<ImageError> for BlurhashingError {
|
||||||
fn from(value: ImageError) -> Self { Self::ImageError(Box::new(value)) }
|
fn from(value: ImageError) -> Self { Self::ImageError(Box::new(value)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "blurhashing")]
|
||||||
impl From<blurhash::Error> for BlurhashingError {
|
impl From<blurhash::Error> for BlurhashingError {
|
||||||
fn from(value: blurhash::Error) -> Self { Self::HashingLibError(Box::new(value)) }
|
fn from(value: blurhash::Error) -> Self { Self::HashingLibError(Box::new(value)) }
|
||||||
}
|
}
|
||||||
|
@ -152,6 +166,9 @@ impl Display for BlurhashingError {
|
||||||
|
|
||||||
| Self::ImageError(e) =>
|
| Self::ImageError(e) =>
|
||||||
write!(f, "There was an error with the image loading library => {e}")?,
|
write!(f, "There was an error with the image loading library => {e}")?,
|
||||||
|
|
||||||
|
#[cfg(not(feature = "blurhashing"))]
|
||||||
|
| Self::Unavailable => write!(f, "Blurhashing is not supported")?,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
#[cfg(feature = "blurhashing")]
|
|
||||||
pub mod blurhash;
|
pub mod blurhash;
|
||||||
mod data;
|
mod data;
|
||||||
pub(super) mod migrations;
|
pub(super) mod migrations;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue