1use base64::prelude::*;
4#[cfg(feature = "native_crypto")]
5use simploxide_api_types::CryptoFile;
6use tokio::io::{AsyncReadExt as _, AsyncSeekExt as _};
7
8use crate::util;
9
10use std::{
11 io::SeekFrom,
12 path::{Path, PathBuf},
13};
14
15const DEFAULT_PREVIEW: &str = "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/\
162wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/\
172wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCABVAIADASIAAhEBAxEB/\
188QAFgABAQEAAAAAAAAAAAAAAAAAAAEE/8QAFBABAAAAAAAAAAAAAAAAAAAAAP/EABgBAQEBAQEAAAAAAAAAAAAAAAMCAQUE/8QAFhEBAQEAAAAAAAAAAAAAAAAAAAER/\
199oADAMBAAIRAxEAPwDaKF17qgo3UVBRWjqCjdFUFFaOoKN0VQUVo6go3R0FHi13ago3R1BRWjoijdHUFFSiqCjZR1BRUo6go3RVQHh13aAK1FAVuiqCipR1BRUo6go2UV\
20QUVKOoKK0dAHg13aCjdHUFFaOoKKlHUFFSiqCipR1BRsoqgoqUdBR4Nd2oKNlHUUFSjoAqUdAFSjoAqUVAFSjoAqUdAHPd2gCoOqAqDoA2DoAuCoAqDoAqCoAqDr//2Q==";
21
22const MAX_PREVIEW_BYTES: usize = 9350;
23#[cfg(feature = "multimedia")]
24const MAX_FILE_SIZE: usize = 20 * 1024 * 1024;
25
26#[derive(Clone)]
32pub struct ImagePreview {
33 source: PreviewSource,
34 #[cfg(feature = "multimedia")]
35 transcoder: Transcoder,
36}
37
38impl Default for ImagePreview {
39 fn default() -> Self {
40 Self {
41 source: PreviewSource::Default,
42 #[cfg(feature = "multimedia")]
43 transcoder: Transcoder::thumbnail(),
44 }
45 }
46}
47
48impl std::fmt::Debug for ImagePreview {
49 #[cfg(not(feature = "multimedia"))]
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 f.debug_struct("ImagePreview")
52 .field("source", &self.kind())
53 .finish()
54 }
55
56 #[cfg(feature = "multimedia")]
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.debug_struct("ImagePreview")
59 .field("source", &self.kind())
60 .field("transcoder", &self.transcoder)
61 .finish()
62 }
63}
64
65impl ImagePreview {
66 pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Self {
69 Self {
70 source: PreviewSource::Bytes(bytes.into()),
71 #[cfg(feature = "multimedia")]
72 transcoder: Transcoder::thumbnail(),
73 }
74 }
75
76 pub fn raw(uri: impl Into<String>) -> Self {
78 Self {
79 source: PreviewSource::DataUri(uri.into()),
80 #[cfg(feature = "multimedia")]
81 transcoder: Transcoder::thumbnail(),
82 }
83 }
84
85 pub fn from_file(path: impl AsRef<Path>) -> Self {
87 Self {
88 source: PreviewSource::File(path.as_ref().to_path_buf()),
89 #[cfg(feature = "multimedia")]
90 transcoder: Transcoder::thumbnail(),
91 }
92 }
93
94 pub fn kind(&self) -> PreviewKind {
95 match self.source {
96 PreviewSource::Default => PreviewKind::Default,
97 PreviewSource::Bytes(_) => PreviewKind::Bytes,
98 PreviewSource::DataUri(_) => PreviewKind::Raw,
99 PreviewSource::File(_) => PreviewKind::File,
100 #[cfg(feature = "native_crypto")]
101 PreviewSource::CryptoFile(_) => PreviewKind::CryptoFile,
102 }
103 }
104
105 #[cfg(feature = "native_crypto")]
106 pub fn from_crypto_file(file: CryptoFile) -> Self {
108 Self {
109 source: PreviewSource::CryptoFile(file),
110 #[cfg(feature = "multimedia")]
111 transcoder: Transcoder::thumbnail(),
112 }
113 }
114
115 #[cfg(feature = "multimedia")]
116 pub fn with_transcoder(mut self, transcoder: Transcoder) -> Self {
121 self.set_transcoder(transcoder);
122 self
123 }
124
125 #[cfg(feature = "multimedia")]
126 pub fn set_transcoder(&mut self, transcoder: Transcoder) {
127 self.transcoder = transcoder;
128 }
129
130 pub async fn resolve(self) -> String {
132 match self.try_resolve().await {
133 Ok(s) => s,
134 Err(e) => {
135 log::warn!("Falling back to default preview due to an error: {e}");
136 default()
137 }
138 }
139 }
140
141 #[cfg(not(feature = "multimedia"))]
142 pub async fn try_resolve(self) -> Result<String, PreviewError> {
147 match self.source {
148 PreviewSource::Default => Ok(default()),
149 PreviewSource::Bytes(b) => try_encode_jpg_to_uri(&b),
150 PreviewSource::DataUri(s) => validate_uri_preview(s),
151 PreviewSource::File(path) => {
152 let bytes = read_plain_file(&path, MAX_PREVIEW_BYTES).await?;
153 try_encode_jpg_to_uri(&bytes)
154 }
155 #[cfg(feature = "native_crypto")]
156 PreviewSource::CryptoFile(file) => {
157 let bytes = read_crypto_file(file, MAX_PREVIEW_BYTES).await?;
158 try_encode_jpg_to_uri(&bytes)
159 }
160 }
161 }
162
163 #[cfg(feature = "multimedia")]
164 pub async fn try_resolve(self) -> Result<String, PreviewError> {
169 let bytes = match self.source {
170 PreviewSource::Default => return Ok(default()),
171 PreviewSource::Bytes(b) => b,
172 PreviewSource::DataUri(s) => return validate_uri_preview(s),
173 PreviewSource::File(path) => read_plain_file(&path, MAX_FILE_SIZE).await?,
174 #[cfg(feature = "native_crypto")]
175 PreviewSource::CryptoFile(file) => read_crypto_file(file, MAX_FILE_SIZE).await?,
176 };
177
178 if self.transcoder.is_enabled() {
179 let jpg_bytes =
180 tokio::task::spawn_blocking(move || self.transcoder.transcode_to_jpg(bytes))
181 .await??;
182 Ok(encode_to_uri(&jpg_bytes))
183 } else {
184 try_encode_jpg_to_uri(&bytes)
185 }
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
190pub enum PreviewKind {
191 Default,
192 Bytes,
193 Raw,
194 File,
195 #[cfg(feature = "native_crypto")]
196 CryptoFile,
197}
198
199#[cfg(feature = "multimedia")]
200pub mod transcoder {
201 use image::{
202 ImageFormat, ImageReader,
203 codecs::jpeg::JpegEncoder,
204 imageops::{self, FilterType},
205 };
206 use std::io::Cursor;
207
208 use super::PreviewError;
209
210 const THUMBNAIL_MAX_BYTES: usize = 10450;
211 const AVATAR_MAX_BYTES: usize = 9350;
212
213 const THUMBNAIL_SIZES: [u16; 4] = [256, 192, 128, 96];
214 const AVATAR_SIZES: [u16; 4] = [160, 128, 96, 64];
215 const PREVIEW_QUALITIES: [u8; 4] = [85, 75, 60, 45];
216
217 #[derive(Debug, Clone, Copy)]
218 pub struct Transcoder {
219 sizes: [u16; 4],
220 max_bytes: usize,
221 }
222
223 impl Default for Transcoder {
224 fn default() -> Self {
225 Self::thumbnail()
226 }
227 }
228
229 impl Transcoder {
230 pub const fn disabled() -> Self {
231 Self {
232 sizes: [0; 4],
233 max_bytes: 0,
234 }
235 }
236
237 pub const fn thumbnail() -> Self {
238 Self {
239 sizes: THUMBNAIL_SIZES,
240 max_bytes: THUMBNAIL_MAX_BYTES,
241 }
242 }
243
244 pub const fn avatar() -> Self {
245 Self {
246 sizes: AVATAR_SIZES,
247 max_bytes: AVATAR_MAX_BYTES,
248 }
249 }
250
251 pub const fn is_enabled(&self) -> bool {
252 !self.is_disabled()
253 }
254
255 pub const fn is_disabled(&self) -> bool {
256 self.max_bytes == 0 || self.sizes[0] == 0
257 }
258
259 pub const fn with_sizes(mut self, sizes: [u16; 4]) -> Self {
260 self.sizes = sizes;
261 self
262 }
263
264 pub const fn with_max_bytes(mut self, max_bytes: usize) -> Self {
265 self.max_bytes = max_bytes;
266 self
267 }
268
269 pub fn transcode_to_jpg(self, mut bytes: Vec<u8>) -> Result<Vec<u8>, PreviewError> {
270 if !self.is_enabled() {
271 return Ok(bytes);
272 }
273
274 let reader = ImageReader::new(Cursor::new(&bytes)).with_guessed_format()?;
275
276 if reader.format() == Some(ImageFormat::Jpeg) && bytes.len() <= self.max_bytes {
277 return Ok(bytes);
278 }
279
280 let img = reader.decode()?;
281
282 let (orig_w, orig_h) = (img.width(), img.height());
283 let max_orig = orig_w.max(orig_h);
284
285 let mut last_effective = u32::MAX;
286 for &size in &self.sizes {
287 let effective = (size as u32).min(max_orig);
289 if effective == last_effective {
290 continue;
291 }
292 last_effective = effective;
293
294 let rgb = if effective < max_orig {
295 let resized = img.resize(effective, effective, FilterType::Lanczos3);
296 imageops::unsharpen(&resized.to_rgb8(), 0.5, 0)
297 } else {
298 img.to_rgb8()
299 };
300
301 for &quality in &PREVIEW_QUALITIES {
302 bytes.clear();
303 JpegEncoder::new_with_quality(&mut bytes, quality).encode_image(&rgb)?;
304
305 if bytes.len() <= self.max_bytes {
306 return Ok(bytes);
307 }
308 }
309 }
310
311 Err(PreviewError::TooLarge)
312 }
313 }
314}
315
316#[cfg(feature = "multimedia")]
317pub use transcoder::Transcoder;
318
319const URI_HEADER: &str = "data:image/jpg;base64,";
320
321pub fn default() -> String {
322 DEFAULT_PREVIEW.to_owned()
323}
324
325pub fn encode_jpg_to_uri(bytes: &[u8]) -> String {
327 match try_encode_jpg_to_uri(bytes) {
328 Ok(s) => s,
329 Err(e) => {
330 log::warn!("{e}");
331 default()
332 }
333 }
334}
335
336pub fn try_encode_jpg_to_uri(bytes: &[u8]) -> Result<String, PreviewError> {
337 if bytes.len() > MAX_PREVIEW_BYTES {
338 return Err(PreviewError::TooLarge);
339 }
340 Ok(encode_to_uri(bytes))
341}
342
343fn encode_to_uri(bytes: &[u8]) -> String {
344 let mut encoded = String::with_capacity(bytes.len() * 4 / 3 + URI_HEADER.len() + 4);
345 encoded.push_str(URI_HEADER);
346 BASE64_STANDARD.encode_string(bytes, &mut encoded);
347 encoded
348}
349
350pub fn try_decode_jpg_from_uri(uri_str: &str) -> Result<Vec<u8>, UriDecodeError> {
351 let Some(s) = uri_str.strip_prefix(URI_HEADER) else {
352 return Err(UriDecodeError::NotAUri);
353 };
354
355 BASE64_STANDARD.decode(s).map_err(UriDecodeError::Base64)
356}
357
358#[derive(Debug)]
359pub enum PreviewError {
360 TooLarge,
361 BadUri(UriDecodeError),
362 Io(std::io::Error),
363 #[cfg(feature = "multimedia")]
364 Transcoding(image::ImageError),
365 #[cfg(feature = "multimedia")]
366 Tokio(tokio::task::JoinError),
367}
368
369impl From<std::io::Error> for PreviewError {
370 fn from(err: std::io::Error) -> Self {
371 Self::Io(err)
372 }
373}
374
375#[cfg(feature = "multimedia")]
376impl From<image::ImageError> for PreviewError {
377 fn from(err: image::ImageError) -> Self {
378 Self::Transcoding(err)
379 }
380}
381
382#[cfg(feature = "multimedia")]
383impl From<tokio::task::JoinError> for PreviewError {
384 fn from(err: tokio::task::JoinError) -> Self {
385 Self::Tokio(err)
386 }
387}
388
389impl std::fmt::Display for PreviewError {
390 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391 match self {
392 Self::TooLarge => {
393 write!(
394 f,
395 "preview size exceeds the max possible size({MAX_PREVIEW_BYTES} bytes)"
396 )
397 }
398 Self::BadUri(e) => write!(f, "{e}"),
399 Self::Io(error) => write!(f, "Cannot process preview file: {error}"),
400 #[cfg(feature = "multimedia")]
401 Self::Transcoding(error) => write!(f, "Cannot transcode preview: {error}"),
402 #[cfg(feature = "multimedia")]
403 Self::Tokio(error) => write!(f, "Failed to join the transcoding task: {error}"),
404 }
405 }
406}
407
408impl std::error::Error for PreviewError {
409 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
410 match self {
411 Self::TooLarge => None,
412 Self::BadUri(error) => Some(error),
413 Self::Io(error) => Some(error),
414 #[cfg(feature = "multimedia")]
415 Self::Transcoding(error) => Some(error),
416 #[cfg(feature = "multimedia")]
417 Self::Tokio(error) => Some(error),
418 }
419 }
420}
421
422#[derive(Debug)]
423pub enum UriDecodeError {
424 NotAUri,
425 Base64(base64::DecodeError),
426}
427
428impl std::fmt::Display for UriDecodeError {
429 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430 match self {
431 Self::NotAUri => write!(f, "not a URI string"),
432 Self::Base64(e) => write!(f, "{e}"),
433 }
434 }
435}
436
437impl std::error::Error for UriDecodeError {
438 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
439 if let Self::Base64(e) = self {
440 Some(e)
441 } else {
442 None
443 }
444 }
445}
446
447#[derive(Clone)]
448enum PreviewSource {
449 Default,
450 Bytes(Vec<u8>),
451 DataUri(String),
452 File(PathBuf),
453 #[cfg(feature = "native_crypto")]
454 CryptoFile(CryptoFile),
455}
456
457async fn read_plain_file(path: &PathBuf, size_limit: usize) -> std::io::Result<Vec<u8>> {
458 let mut f = tokio::fs::File::open(&path).await?;
459 let size_hint = f.seek(SeekFrom::End(0)).await?;
460 f.seek(SeekFrom::Start(0)).await?;
461 let size_hint: usize = util::cast_file_size(size_hint)?;
462
463 if size_hint > size_limit {
464 return Err(util::file_is_too_large(format!(
465 "Size exceeds {size_limit} bytes"
466 )));
467 }
468
469 let mut buf = Vec::with_capacity(size_hint);
470 f.read_to_end(&mut buf).await?;
471
472 Ok(buf)
473}
474
475#[cfg(feature = "native_crypto")]
476async fn read_crypto_file(file: CryptoFile, size_limit: usize) -> std::io::Result<Vec<u8>> {
477 let mut f = crate::crypto::fs::TokioMaybeCryptoFile::from_crypto_file(file).await?;
478 let size_hint = f.size_hint().await?;
479
480 if size_hint > size_limit {
481 return Err(util::file_is_too_large(format!(
482 "Size exceeds {size_limit} bytes"
483 )));
484 }
485
486 let mut buf = Vec::with_capacity(size_hint);
487 f.read_to_end(&mut buf).await?;
488
489 Ok(buf)
490}
491
492fn validate_uri_preview(uri: String) -> Result<String, PreviewError> {
493 let Some(s) = uri.strip_prefix(URI_HEADER) else {
494 return Err(PreviewError::BadUri(UriDecodeError::NotAUri));
495 };
496
497 if s.len() > MAX_PREVIEW_BYTES * 4 / 3 {
498 return Err(PreviewError::TooLarge);
499 }
500
501 Ok(uri)
502}