1use std::{cmp, num::Saturating as Sat, sync::Arc, time::Duration};
9
10use futures::{StreamExt, pin_mut};
11use ruma::{Mxc, UInt, UserId, http_headers::ContentDisposition, media::Method};
12use tokio::sync::Notify;
13use tuwunel_core::{
14 Err, Result, checked, err, implement,
15 utils::{result::LogDebugErr, stream::IterStream},
16};
17
18use super::{Media, data::Metadata};
19
20#[derive(Debug)]
22pub struct Dim {
23 pub width: u32,
24 pub height: u32,
25 pub method: Method,
26}
27
28impl super::Service {
29 #[tracing::instrument(
31 level = "debug",
32 ret(level = "debug")
33 skip(self, file),
34 )]
35 pub async fn upload_thumbnail(
36 &self,
37 mxc: &Mxc<'_>,
38 content_disposition: Option<&ContentDisposition>,
39 content_type: Option<&str>,
40 dim: &Dim,
41 file: &[u8],
42 ) -> Result {
43 let key =
44 self.db
45 .create_file_metadata(mxc, None, dim, content_disposition, content_type)?;
46
47 self.create_media_file(&key, file).await?;
49 Ok(())
50 }
51
52 #[tracing::instrument(
53 level = "debug",
54 err(level = "debug")
55 skip(self),
56 )]
57 pub async fn get_or_fetch_thumbnail(
58 &self,
59 mxc: &Mxc<'_>,
60 dim: &Dim,
61 timeout_ms: Duration,
62 user: &UserId,
63 ) -> Result<Media> {
64 if let Ok(media) = self
65 .get_thumbnail(mxc, dim, Some(timeout_ms))
66 .await
67 {
68 return Ok(media);
69 }
70
71 if self
72 .services
73 .globals
74 .server_is_ours(mxc.server_name)
75 {
76 return Err!(Request(NotFound("Local thumbnail not found.")));
77 }
78
79 let lock = self.federation_mutex.lock(&mxc.to_string()).await;
80
81 if self
82 .db
83 .file_metadata_exists(mxc, &dim.normalized())
84 .await
85 {
86 drop(lock);
87 return self.get_thumbnail(mxc, dim, None).await;
88 }
89
90 self.fetch_remote_thumbnail(mxc, None, timeout_ms, dim)
91 .await
92 }
93
94 #[tracing::instrument(
96 level = "debug",
97 err(level = "debug")
98 skip(self),
99 )]
100 pub async fn get_thumbnail(
101 &self,
102 mxc: &Mxc<'_>,
103 dim: &Dim,
104 timeout_duration: Option<Duration>,
105 ) -> Result<Media> {
106 if let Ok(meta) = self.get_stored_thumbnail(mxc, dim).await {
107 return Ok(meta);
108 }
109
110 let Some(timeout_duration) = timeout_duration else {
111 return Err!(Request(NotFound("Media thumbnail not found.")));
112 };
113
114 let Ok(_pending) = self.db.search_pending_mxc(mxc).await else {
115 return Err!(Request(NotFound("Media thumbnail not found.")));
116 };
117
118 let notifier = self
119 .mxc_state
120 .notifiers
121 .lock()?
122 .entry(mxc.to_string().into())
123 .or_insert_with(|| Arc::new(Notify::new()))
124 .clone();
125
126 if tokio::time::timeout(timeout_duration, notifier.notified())
127 .await
128 .is_err()
129 {
130 return Err!(Request(NotYetUploaded("Media has not been uploaded yet")));
131 }
132
133 self.get_stored_thumbnail(mxc, dim).await
134 }
135
136 #[tracing::instrument(
150 name = "thumbnail",
151 level = "debug",
152 err(level = "trace")
153 skip(self),
154 )]
155 pub async fn get_stored_thumbnail(&self, mxc: &Mxc<'_>, dim: &Dim) -> Result<Media> {
156 let dim = dim.normalized();
158
159 if let Ok(metadata) = self.db.search_file_metadata(mxc, &dim).await {
160 return self.get_thumbnail_saved(metadata).await;
161 }
162
163 let metadata = self
164 .db
165 .search_file_metadata(mxc, &Dim::default())
166 .await?;
167
168 self.get_thumbnail_generate(mxc, &dim, metadata)
169 .await
170 }
171}
172
173#[implement(super::Service)]
175#[tracing::instrument(name = "saved", level = "debug", skip_all)]
176async fn get_thumbnail_saved(&self, data: Metadata) -> Result<Media> {
177 let path = self.get_media_name_sha256(&data.key);
178 let fetch = self
179 .storage_providers()
180 .stream()
181 .filter_map(async |provider| {
182 provider
183 .get(path.as_str())
184 .await
185 .log_debug_err()
186 .ok()
187 });
188
189 pin_mut!(fetch);
190 let Some(bytes) = fetch.next().await else {
191 return Err!(Request(NotFound("Media thumbnail not found.")));
192 };
193
194 Ok(into_media(data, bytes.to_vec()))
195}
196
197#[cfg(feature = "media_thumbnail")]
199#[implement(super::Service)]
200#[tracing::instrument(name = "generate", level = "debug", skip(self, data))]
201async fn get_thumbnail_generate(
202 &self,
203 mxc: &Mxc<'_>,
204 dim: &Dim,
205 data: Metadata,
206) -> Result<Media> {
207 let Ok(media) = self.get_stored(mxc).await else {
208 return Err!("Could not find original media.");
209 };
210
211 let Ok(image) = image::load_from_memory(&media.content) else {
212 return Ok(into_media(data, media.content));
214 };
215
216 if dim.width > image.width() || dim.height > image.height() {
217 return Ok(into_media(data, media.content));
218 }
219
220 let mut thumbnail_bytes = Vec::new();
221 let thumbnail = thumbnail_generate(&image, dim)?;
222 let mut cursor = std::io::Cursor::new(&mut thumbnail_bytes);
223 thumbnail
224 .write_to(&mut cursor, image::ImageFormat::Png)
225 .map_err(|error| err!(error!(?error, "Error writing PNG thumbnail.")))?;
226
227 let thumbnail_key = self.db.create_file_metadata(
229 mxc,
230 None,
231 dim,
232 data.content_disposition.as_ref(),
233 data.content_type.as_deref(),
234 )?;
235
236 self.create_media_file(&thumbnail_key, &thumbnail_bytes)
237 .await?;
238
239 Ok(into_media(data, thumbnail_bytes))
240}
241
242#[cfg(not(feature = "media_thumbnail"))]
243#[implement(super::Service)]
244#[tracing::instrument(name = "fallback", level = "debug", skip_all)]
245async fn get_thumbnail_generate(
246 &self,
247 _mxc: &Mxc<'_>,
248 _dim: &Dim,
249 data: Metadata,
250) -> Result<Media> {
251 self.get_thumbnail_saved(data).await
252}
253
254#[cfg(feature = "media_thumbnail")]
255fn thumbnail_generate(
256 image: &image::DynamicImage,
257 requested: &Dim,
258) -> Result<image::DynamicImage> {
259 use image::imageops::FilterType;
260
261 let thumbnail = if !requested.crop() {
262 let Dim { width, height, .. } = requested.scaled(&Dim {
263 width: image.width(),
264 height: image.height(),
265 ..Dim::default()
266 })?;
267 image.thumbnail_exact(width, height)
268 } else {
269 image.resize_to_fill(requested.width, requested.height, FilterType::CatmullRom)
270 };
271
272 Ok(thumbnail)
273}
274
275fn into_media(data: Metadata, content: Vec<u8>) -> Media {
276 Media {
277 content,
278 content_type: data.content_type,
279 content_disposition: data.content_disposition,
280 }
281}
282
283impl Dim {
284 pub fn from_ruma(width: UInt, height: UInt, method: Option<Method>) -> Result<Self> {
286 let width = width
287 .try_into()
288 .map_err(|e| err!(Request(InvalidParam("Width is invalid: {e:?}"))))?;
289 let height = height
290 .try_into()
291 .map_err(|e| err!(Request(InvalidParam("Height is invalid: {e:?}"))))?;
292
293 Ok(Self::new(width, height, method))
294 }
295
296 #[inline]
298 #[must_use]
299 pub fn new(width: u32, height: u32, method: Option<Method>) -> Self {
300 Self {
301 width,
302 height,
303 method: method.unwrap_or(Method::Scale),
304 }
305 }
306
307 pub fn scaled(&self, image: &Self) -> Result<Self> {
308 let image_width = image.width;
309 let image_height = image.height;
310
311 let width = cmp::min(self.width, image_width);
312 let height = cmp::min(self.height, image_height);
313
314 let use_width = Sat(width) * Sat(image_height) < Sat(height) * Sat(image_width);
315
316 let x = if use_width {
317 let dividend = (Sat(height) * Sat(image_width)).0;
318 checked!(dividend / image_height)?
319 } else {
320 width
321 };
322
323 let y = if !use_width {
324 let dividend = (Sat(width) * Sat(image_height)).0;
325 checked!(dividend / image_width)?
326 } else {
327 height
328 };
329
330 Ok(Self {
331 width: x,
332 height: y,
333 method: Method::Scale,
334 })
335 }
336
337 #[must_use]
341 pub fn normalized(&self) -> Self {
342 match (self.width, self.height) {
343 | (0..=32, 0..=32) => Self::new(32, 32, Some(Method::Crop)),
344 | (0..=96, 0..=96) => Self::new(96, 96, Some(Method::Crop)),
345 | (0..=320, 0..=240) => Self::new(320, 240, Some(Method::Scale)),
346 | (0..=640, 0..=480) => Self::new(640, 480, Some(Method::Scale)),
347 | (0..=800, 0..=600) => Self::new(800, 600, Some(Method::Scale)),
348 | _ => Self::default(),
349 }
350 }
351
352 #[inline]
354 #[must_use]
355 pub fn crop(&self) -> bool { self.method == Method::Crop }
356}
357
358impl Default for Dim {
359 #[inline]
360 fn default() -> Self {
361 Self {
362 width: 0,
363 height: 0,
364 method: Method::Scale,
365 }
366 }
367}