Skip to main content

tuwunel_service/media/
blurhash.rs

1#[cfg(feature = "blurhashing")]
2use tuwunel_core::config::BlurhashConfig as CoreBlurhashConfig;
3use tuwunel_core::{Result, implement};
4
5use super::Service;
6
7#[implement(Service)]
8#[cfg(not(feature = "blurhashing"))]
9pub fn create_blurhash(
10	&self,
11	_file: &[u8],
12	_content_type: Option<&str>,
13	_file_name: Option<&str>,
14) -> Result<Option<String>> {
15	tuwunel_core::debug_warn!("blurhashing on upload support was not compiled");
16
17	Ok(None)
18}
19
20#[implement(Service)]
21#[cfg(feature = "blurhashing")]
22pub fn create_blurhash(
23	&self,
24	file: &[u8],
25	content_type: Option<&str>,
26	file_name: Option<&str>,
27) -> Result<Option<String>> {
28	let config = BlurhashConfig::from(self.services.server.config.blurhashing);
29
30	// since 0 means disabled blurhashing, skipped blurhashing
31	if config.size_limit == 0 {
32		return Ok(None);
33	}
34
35	get_blurhash_from_request(file, content_type, file_name, config)
36		.map_err(|e| tuwunel_core::err!(debug_error!("blurhashing error: {e}")))
37		.map(Some)
38}
39
40/// Returns the blurhash or a blurhash error which implements Display.
41#[tracing::instrument(
42	name = "blurhash",
43	level = "debug",
44	skip(data),
45	fields(
46		bytes = data.len(),
47	),
48)]
49#[cfg(feature = "blurhashing")]
50fn get_blurhash_from_request(
51	data: &[u8],
52	mime: Option<&str>,
53	filename: Option<&str>,
54	config: BlurhashConfig,
55) -> Result<String, BlurhashingError> {
56	// Get format image is supposed to be in
57	let format = get_format_from_data_mime_and_filename(data, mime, filename)?;
58
59	// Get the image reader for said image format
60	let decoder = get_image_decoder_with_format_and_data(format, data)?;
61
62	// Check image size makes sense before unpacking whole image
63	if is_image_above_size_limit(&decoder, config) {
64		return Err(BlurhashingError::ImageTooLarge);
65	}
66
67	let image = image::DynamicImage::from_decoder(decoder)?;
68
69	blurhash_an_image(&image, config)
70}
71
72/// Gets the Image Format value from the data,mime, and filename
73/// It first checks if the mime is a valid image format
74/// Then it checks if the filename has a format, otherwise just guess based on
75/// the binary data Assumes that mime and filename extension won't be for a
76/// different file format than file.
77#[cfg(feature = "blurhashing")]
78fn get_format_from_data_mime_and_filename(
79	data: &[u8],
80	mime: Option<&str>,
81	filename: Option<&str>,
82) -> Result<image::ImageFormat, BlurhashingError> {
83	let extension = filename
84		.map(std::path::Path::new)
85		.and_then(std::path::Path::extension)
86		.map(std::ffi::OsStr::to_string_lossy);
87
88	mime.or(extension.as_deref())
89		.and_then(image::ImageFormat::from_mime_type)
90		.map_or_else(|| image::guess_format(data).map_err(Into::into), Ok)
91}
92
93#[cfg(feature = "blurhashing")]
94fn get_image_decoder_with_format_and_data(
95	image_format: image::ImageFormat,
96	data: &[u8],
97) -> Result<Box<dyn image::ImageDecoder + '_>, BlurhashingError> {
98	let mut image_reader = image::ImageReader::new(std::io::Cursor::new(data));
99	image_reader.set_format(image_format);
100	Ok(Box::new(image_reader.into_decoder()?))
101}
102
103#[cfg(feature = "blurhashing")]
104fn is_image_above_size_limit<T: image::ImageDecoder>(
105	decoder: &T,
106	blurhash_config: BlurhashConfig,
107) -> bool {
108	decoder.total_bytes() >= blurhash_config.size_limit
109}
110
111#[cfg(feature = "blurhashing")]
112#[tracing::instrument(name = "encode", level = "debug", skip_all)]
113#[inline]
114fn blurhash_an_image(
115	image: &image::DynamicImage,
116	blurhash_config: BlurhashConfig,
117) -> Result<String, BlurhashingError> {
118	Ok(blurhash::encode_image(
119		blurhash_config.components_x,
120		blurhash_config.components_y,
121		&image.to_rgba8(),
122	)?)
123}
124
125#[derive(Clone, Copy, Debug)]
126pub struct BlurhashConfig {
127	pub components_x: u32,
128	pub components_y: u32,
129
130	/// size limit in bytes
131	pub size_limit: u64,
132}
133
134#[cfg(feature = "blurhashing")]
135impl From<CoreBlurhashConfig> for BlurhashConfig {
136	fn from(value: CoreBlurhashConfig) -> Self {
137		Self {
138			components_x: value.components_x,
139			components_y: value.components_y,
140			size_limit: value.blurhash_max_raw_size,
141		}
142	}
143}
144
145#[derive(Debug)]
146#[cfg(feature = "blurhashing")]
147pub enum BlurhashingError {
148	HashingLibError(Box<dyn std::error::Error + Send>),
149	#[cfg(feature = "blurhashing")]
150	ImageError(Box<image::ImageError>),
151	ImageTooLarge,
152}
153
154#[cfg(feature = "blurhashing")]
155impl From<image::ImageError> for BlurhashingError {
156	fn from(value: image::ImageError) -> Self { Self::ImageError(Box::new(value)) }
157}
158
159#[cfg(feature = "blurhashing")]
160impl From<blurhash::Error> for BlurhashingError {
161	fn from(value: blurhash::Error) -> Self { Self::HashingLibError(Box::new(value)) }
162}
163
164#[cfg(feature = "blurhashing")]
165impl std::fmt::Display for BlurhashingError {
166	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167		write!(f, "Blurhash Error:")?;
168		match &self {
169			| Self::ImageTooLarge => write!(f, "Image was too large to blurhash")?,
170			| Self::HashingLibError(e) =>
171				write!(f, "There was an error with the blurhashing library => {e}")?,
172			#[cfg(feature = "blurhashing")]
173			| Self::ImageError(e) =>
174				write!(f, "There was an error with the image loading library => {e}")?,
175		}
176
177		Ok(())
178	}
179}