tuwunel_service/media/thumbnail/
encode.rs1use std::{
13 cmp::min,
14 io::Cursor,
15 panic::{AssertUnwindSafe, catch_unwind},
16 result::Result as StdResult,
17};
18
19use bytes::Bytes;
20use image::{
21 AnimationDecoder, DynamicImage, Frame, Frames, ImageDecoder, ImageFormat, Limits,
22 codecs::{
23 gif::{GifDecoder, GifEncoder, Repeat},
24 png::PngDecoder,
25 webp::WebPDecoder,
26 },
27};
28use ruma::Mxc;
29use tokio::sync::OwnedSemaphorePermit;
30use tuwunel_core::{Err, Error, Result, defer, err, implement};
31
32use super::{
33 super::Media,
34 Dim,
35 animate::GIF,
36 generate::{BYTES_PER_PIXEL, reader, thumbnail_generate},
37};
38
39const ANIMATED_NAME: &str = "thumbnail.gif";
40
41const GIF_SPEED: i32 = 10;
47
48pub(super) struct Output {
49 pub(super) media: Result<Media>,
50 pub(super) admission: OwnedSemaphorePermit,
51 pub(super) source: Bytes,
52}
53
54#[implement(super::super::Service)]
61#[tracing::instrument(
62 name = "animate",
63 level = "debug",
64 skip(self, source, admission)
65)]
66pub(super) async fn store_animated(
67 &self,
68 mxc: &Mxc<'_>,
69 dim: &Dim,
70 source: Bytes,
71 admission: OwnedSemaphorePermit,
72) -> Result<Output> {
73 let config = &self.services.config;
74 let max_frames = config.media_thumbnail_max_frames;
75 let budget = config.media_thumbnail_max_pixels;
76 let requested = Dim::new(dim.width, dim.height, Some(dim.method.clone()));
77
78 #[cfg(test)]
79 super::tests::before_animation_submit().await;
80 #[cfg(test)]
81 let control = super::tests::animation_control();
82
83 let encode = self
84 .services
85 .server
86 .runtime()
87 .spawn_blocking(move || {
88 #[cfg(test)]
89 if let Some(control) = control.as_ref() {
90 control.worker_started();
91 }
92
93 let content = catch_unwind(AssertUnwindSafe(|| {
94 encode_frames(&source, &requested, max_frames, budget)
95 }))
96 .map_err(Error::from_panic)
97 .unwrap_or_else(Err);
98
99 #[cfg(test)]
100 if let Some(control) = control.as_ref() {
101 control.worker_finished();
102 }
103
104 (content, admission, source)
105 });
106
107 #[cfg(test)]
108 super::tests::animation_submitted();
109
110 let abort = encode.abort_handle();
113
114 defer! {{ abort.abort(); }}
115
116 let (content, admission, source) = encode.await?;
117 let media = match content {
118 | Err(error) => Err(error),
119 | Ok(content) =>
120 self.store_encoded(mxc, dim, content, GIF, ANIMATED_NAME)
121 .await,
122 };
123
124 Ok(Output { media, admission, source })
125}
126
127pub(in super::super) fn encode_frames(
134 bytes: &[u8],
135 dim: &Dim,
136 max_frames: usize,
137 budget: u64,
138) -> Result<Vec<u8>> {
139 let (frames, canvas) = source_frames(bytes, budget)?;
140
141 let afforded = budget
144 .checked_div(canvas)
145 .and_then(|afforded| usize::try_from(afforded).ok())
146 .unwrap_or(max_frames);
147
148 let limit = min(max_frames, afforded);
149 let (content, count) = encode_sequence(frames, dim, limit)?;
150
151 if count < 2 {
152 return Err!(debug_warn!(%count, "Picture carries no frame sequence."));
153 }
154
155 Ok(content)
156}
157
158fn encode_sequence(frames: Frames<'_>, dim: &Dim, limit: usize) -> Result<(Vec<u8>, usize)> {
163 let mut content = Vec::new();
164 let count = {
165 let mut encoder = GifEncoder::new_with_speed(&mut content, GIF_SPEED);
166
167 encoder
168 .set_repeat(Repeat::Infinite)
169 .map_err(|error| err!(debug_warn!(?error, "Failed to set the GIF loop count.")))?;
170
171 let count = frames
172 .take(limit)
173 .map_while(StdResult::ok)
174 .try_fold(0_usize, |count, frame| -> Result<usize> {
175 let delay = frame.delay();
176 let image = DynamicImage::ImageRgba8(frame.into_buffer());
177 let scaled = thumbnail_generate(&image, dim)?;
178
179 drop(image);
180 encoder
181 .encode_frame(Frame::from_parts(scaled.into_rgba8(), 0, 0, delay))
182 .map_err(|error| err!(debug_warn!(?error, "Failed to encode a frame.")))?;
183
184 Ok(count.saturating_add(1))
185 })?;
186
187 drop(encoder);
190
191 count
192 };
193
194 Ok((content, count))
195}
196
197fn source_frames(bytes: &[u8], budget: u64) -> Result<(Frames<'_>, u64)> {
206 let format = reader(bytes)?
207 .format()
208 .ok_or_else(|| err!(debug_warn!("Picture names no format.")))?;
209
210 let source = Cursor::new(bytes);
211
212 match format {
213 | ImageFormat::Gif => gif_frames(source, budget),
214 | ImageFormat::Png => png_frames(source, budget),
215 | ImageFormat::WebP => webp_frames(source, budget),
216 | _ => Err!(debug_warn!(?format, "Format carries no frame sequence.")),
217 }
218}
219
220fn gif_frames(source: Cursor<&[u8]>, budget: u64) -> Result<(Frames<'_>, u64)> {
224 let format = ImageFormat::Gif;
225 let failed = |error| err!(debug_warn!(?error, ?format, "Failed to read a frame sequence."));
226 let mut decoder = GifDecoder::new(source).map_err(failed)?;
227 let canvas = canvas_pixels(&decoder, budget)?;
228
229 decoder
230 .set_limits(decoder_limits(budget))
231 .map_err(failed)?;
232
233 Ok((decoder.into_frames(), canvas))
234}
235
236fn png_frames(source: Cursor<&[u8]>, budget: u64) -> Result<(Frames<'_>, u64)> {
240 let format = ImageFormat::Png;
241 let failed = |error| err!(debug_warn!(?error, ?format, "Failed to read a frame sequence."));
242 let mut decoder = PngDecoder::new(source).map_err(failed)?;
243 let canvas = canvas_pixels(&decoder, budget)?;
244
245 if !decoder.is_apng().map_err(failed)? {
246 return Err!(debug_warn!("PNG carries a single frame."));
247 }
248
249 decoder
250 .set_limits(decoder_limits(budget))
251 .map_err(failed)?;
252
253 Ok((decoder.apng().map_err(failed)?.into_frames(), canvas))
254}
255
256fn webp_frames(source: Cursor<&[u8]>, budget: u64) -> Result<(Frames<'_>, u64)> {
260 let format = ImageFormat::WebP;
261 let failed = |error| err!(debug_warn!(?error, ?format, "Failed to read a frame sequence."));
262 let mut decoder = WebPDecoder::new(source).map_err(failed)?;
263 let canvas = canvas_pixels(&decoder, budget)?;
264
265 if !decoder.has_animation() {
266 return Err!(debug_warn!("WebP carries a single frame."));
267 }
268
269 decoder
270 .set_limits(decoder_limits(budget))
271 .map_err(failed)?;
272
273 Ok((decoder.into_frames(), canvas))
274}
275
276fn decoder_limits(budget: u64) -> Limits {
280 let mut limits = Limits::no_limits();
281
282 limits.max_alloc = Some(budget.saturating_mul(BYTES_PER_PIXEL));
283
284 limits
285}
286
287fn canvas_pixels(decoder: &impl ImageDecoder, budget: u64) -> Result<u64> {
292 let (width, height) = decoder.dimensions();
293 let pixels = u64::from(width).saturating_mul(u64::from(height));
294
295 if pixels == 0 || pixels > budget {
296 return Err!(
297 debug_warn!(%width, %height, %budget, "Canvas is outside the pixel budget.")
298 );
299 }
300
301 Ok(pixels)
302}