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 = 10_000;
23#[cfg(feature = "multimedia")]
24const MAX_FILE_SIZE: usize = 64 * 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 {
68 Self {
69 source: PreviewSource::Bytes(bytes.into()),
70 #[cfg(feature = "multimedia")]
71 transcoder: Transcoder::thumbnail(),
72 }
73 }
74
75 pub fn raw(uri: impl Into<String>) -> Self {
77 Self {
78 source: PreviewSource::DataUri(uri.into()),
79 #[cfg(feature = "multimedia")]
80 transcoder: Transcoder::thumbnail(),
81 }
82 }
83
84 pub fn from_file(path: impl AsRef<Path>) -> Self {
86 Self {
87 source: PreviewSource::File(path.as_ref().to_path_buf()),
88 #[cfg(feature = "multimedia")]
89 transcoder: Transcoder::thumbnail(),
90 }
91 }
92
93 pub fn kind(&self) -> PreviewKind {
94 match self.source {
95 PreviewSource::Default => PreviewKind::Default,
96 PreviewSource::Bytes(_) => PreviewKind::Bytes,
97 PreviewSource::DataUri(_) => PreviewKind::Raw,
98 PreviewSource::File(_) => PreviewKind::File,
99 #[cfg(feature = "native_crypto")]
100 PreviewSource::CryptoFile(_) => PreviewKind::CryptoFile,
101 }
102 }
103
104 #[cfg(feature = "native_crypto")]
105 pub fn from_crypto_file(file: CryptoFile) -> Self {
107 Self {
108 source: PreviewSource::CryptoFile(file),
109 #[cfg(feature = "multimedia")]
110 transcoder: Transcoder::thumbnail(),
111 }
112 }
113
114 #[cfg(feature = "multimedia")]
115 pub fn with_transcoder(mut self, transcoder: Transcoder) -> Self {
120 self.set_transcoder(transcoder);
121 self
122 }
123
124 #[cfg(feature = "multimedia")]
125 pub fn set_transcoder(&mut self, transcoder: Transcoder) {
126 self.transcoder = transcoder;
127 }
128
129 pub async fn resolve(self) -> String {
131 match self.try_resolve().await {
132 Ok(s) => s,
133 Err(e) => {
134 log::warn!("Falling back to default preview due to an error: {e}");
135 default()
136 }
137 }
138 }
139
140 #[cfg(not(feature = "multimedia"))]
141 pub async fn try_resolve(self) -> Result<String, PreviewError> {
146 match self.source {
147 PreviewSource::Default => Ok(default()),
148 PreviewSource::Bytes(b) => try_encode_jpg_to_uri(&b),
149 PreviewSource::DataUri(s) => validate_uri_preview(s),
150 PreviewSource::File(path) => {
151 let bytes = read_plain_file(&path, MAX_PREVIEW_BYTES).await?;
152 try_encode_jpg_to_uri(&bytes)
153 }
154 #[cfg(feature = "native_crypto")]
155 PreviewSource::CryptoFile(file) => {
156 let bytes = read_crypto_file(file, MAX_PREVIEW_BYTES).await?;
157 try_encode_jpg_to_uri(&bytes)
158 }
159 }
160 }
161
162 #[cfg(feature = "multimedia")]
163 pub async fn try_resolve(self) -> Result<String, PreviewError> {
168 let bytes = match self.source {
169 PreviewSource::Default => return Ok(default()),
170 PreviewSource::Bytes(b) => b,
171 PreviewSource::DataUri(s) => {
172 return validate_uri_preview(s);
173 }
174 PreviewSource::File(path) => read_plain_file(&path, MAX_FILE_SIZE).await?,
175 #[cfg(feature = "native_crypto")]
176 PreviewSource::CryptoFile(file) => read_crypto_file(file, MAX_FILE_SIZE).await?,
177 };
178
179 let jpg_bytes = if self.transcoder.is_enabled() {
180 tokio::task::spawn_blocking(move || -> Result<Vec<u8>, PreviewError> {
181 self.transcoder.transcode_to_jpg(bytes)
182 })
183 .await??
184 } else {
185 bytes
186 };
187
188 try_encode_jpg_to_uri(&jpg_bytes)
189 }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum PreviewKind {
194 Default,
195 Bytes,
196 Raw,
197 File,
198 #[cfg(feature = "native_crypto")]
199 CryptoFile,
200}
201
202#[cfg(feature = "multimedia")]
203pub mod transcoder {
204 use image::{ImageReader, codecs::jpeg::JpegEncoder};
205 use std::io::Cursor;
206
207 use super::PreviewError;
208
209 #[derive(Debug, Clone, Copy)]
212 pub struct Transcoder {
213 enabled: bool,
214 size: (u8, u8),
215 quality: u8,
216 blur: f32,
217 }
218
219 impl Default for Transcoder {
220 fn default() -> Self {
221 Self::jpeg()
222 }
223 }
224
225 impl Transcoder {
226 pub const fn disabled() -> Self {
228 Self {
229 enabled: false,
230 size: (0, 0),
231 quality: 100,
232 blur: 0.0,
233 }
234 }
235
236 pub const fn jpeg() -> Self {
239 Self {
240 enabled: true,
241 size: (0, 0),
242 quality: 100,
243 blur: 0.0,
244 }
245 }
246
247 pub const fn thumbnail() -> Self {
249 Self {
250 enabled: true,
251 size: (128, 128),
252 quality: 60,
253 blur: 0.0,
254 }
255 }
256
257 pub const fn is_enabled(&self) -> bool {
258 self.enabled
259 }
260
261 pub const fn with_size(mut self, x: u8, y: u8) -> Self {
263 let x: u8 = if x < 32 { 32 } else { x };
264 let y: u8 = if y < 32 { 32 } else { y };
265
266 self.size = (x, y);
267 self
268 }
269
270 pub const fn with_quality(mut self, quality: u8) -> Self {
272 if quality == 0 {
273 self.quality = 1;
274 } else if quality > 100 {
275 self.quality = 100;
276 } else {
277 self.quality = quality;
278 }
279
280 self
281 }
282
283 pub const fn with_blur(mut self, sigma: f32) -> Self {
285 if sigma < 1.0 {
286 self.blur = 0.0;
287 } else if sigma > 100.0 {
288 self.blur = 100.0
289 } else {
290 self.blur = sigma
291 };
292
293 self
294 }
295
296 pub fn transcode_to_jpg(self, mut bytes: Vec<u8>) -> Result<Vec<u8>, PreviewError> {
300 if !self.enabled {
301 return Ok(bytes);
302 }
303
304 let img = ImageReader::new(Cursor::new(&bytes))
305 .with_guessed_format()?
306 .decode()?;
307
308 let img = if self.size != (0, 0) {
309 img.thumbnail(self.size.0.into(), self.size.1.into())
310 } else {
311 img
312 };
313
314 let img = if self.blur >= 1.0 {
315 img.fast_blur(self.blur)
316 } else {
317 img
318 };
319
320 bytes.clear();
321 let encoder = JpegEncoder::new_with_quality(&mut bytes, self.quality);
322 img.write_with_encoder(encoder)?;
323
324 Ok(bytes)
325 }
326 }
327}
328
329#[cfg(feature = "multimedia")]
330pub use transcoder::Transcoder;
331
332const URI_HEADER: &str = "data:image/jpg;base64,";
333
334pub fn default() -> String {
335 DEFAULT_PREVIEW.to_owned()
336}
337
338pub fn encode_jpg_to_uri(bytes: &[u8]) -> String {
340 match try_encode_jpg_to_uri(bytes) {
341 Ok(s) => s,
342 Err(e) => {
343 log::warn!("{e}");
344 default()
345 }
346 }
347}
348
349pub fn try_encode_jpg_to_uri(bytes: &[u8]) -> Result<String, PreviewError> {
350 if bytes.len() > MAX_PREVIEW_BYTES {
351 return Err(PreviewError::TooLarge);
352 }
353
354 let mut encoded = String::with_capacity(bytes.len() * 4 / 3 + URI_HEADER.len() + 3);
355 encoded.push_str(URI_HEADER);
356 BASE64_STANDARD.encode_string(bytes, &mut encoded);
357
358 Ok(encoded)
359}
360
361pub fn try_decode_jpg_from_uri(uri_str: &str) -> Result<Vec<u8>, UriDecodeError> {
362 let Some(s) = uri_str.strip_prefix(URI_HEADER) else {
363 return Err(UriDecodeError::NotAUri);
364 };
365
366 BASE64_STANDARD.decode(s).map_err(UriDecodeError::Base64)
367}
368
369#[derive(Debug)]
370pub enum PreviewError {
371 TooLarge,
372 BadUri(UriDecodeError),
373 Io(std::io::Error),
374 #[cfg(feature = "multimedia")]
375 Transcoding(image::ImageError),
376 #[cfg(feature = "multimedia")]
377 Tokio(tokio::task::JoinError),
378}
379
380impl From<std::io::Error> for PreviewError {
381 fn from(err: std::io::Error) -> Self {
382 Self::Io(err)
383 }
384}
385
386#[cfg(feature = "multimedia")]
387impl From<image::ImageError> for PreviewError {
388 fn from(err: image::ImageError) -> Self {
389 Self::Transcoding(err)
390 }
391}
392
393#[cfg(feature = "multimedia")]
394impl From<tokio::task::JoinError> for PreviewError {
395 fn from(err: tokio::task::JoinError) -> Self {
396 Self::Tokio(err)
397 }
398}
399
400impl std::fmt::Display for PreviewError {
401 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402 match self {
403 Self::TooLarge => {
404 write!(
405 f,
406 "preview size exceeds the max possible size({MAX_PREVIEW_BYTES} bytes)"
407 )
408 }
409 Self::BadUri(e) => write!(f, "{e}"),
410 Self::Io(error) => write!(f, "Cannot process preview file: {error}"),
411 #[cfg(feature = "multimedia")]
412 Self::Transcoding(error) => write!(f, "Cannot transcode preview: {error}"),
413 #[cfg(feature = "multimedia")]
414 Self::Tokio(error) => write!(f, "Failed to join the transcoding task: {error}"),
415 }
416 }
417}
418
419impl std::error::Error for PreviewError {
420 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
421 match self {
422 Self::TooLarge => None,
423 Self::BadUri(error) => Some(error),
424 Self::Io(error) => Some(error),
425 #[cfg(feature = "multimedia")]
426 Self::Transcoding(error) => Some(error),
427 #[cfg(feature = "multimedia")]
428 Self::Tokio(error) => Some(error),
429 }
430 }
431}
432
433#[derive(Debug)]
434pub enum UriDecodeError {
435 NotAUri,
436 Base64(base64::DecodeError),
437}
438
439impl std::fmt::Display for UriDecodeError {
440 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 match self {
442 Self::NotAUri => write!(f, "not a URI string"),
443 Self::Base64(e) => write!(f, "{e}"),
444 }
445 }
446}
447
448impl std::error::Error for UriDecodeError {
449 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
450 if let Self::Base64(e) = self {
451 Some(e)
452 } else {
453 None
454 }
455 }
456}
457
458#[derive(Clone)]
459enum PreviewSource {
460 Default,
461 Bytes(Vec<u8>),
462 DataUri(String),
463 File(PathBuf),
464 #[cfg(feature = "native_crypto")]
465 CryptoFile(CryptoFile),
466}
467
468async fn read_plain_file(path: &PathBuf, size_limit: usize) -> std::io::Result<Vec<u8>> {
469 let mut f = tokio::fs::File::open(&path).await?;
470 let size_hint = f.seek(SeekFrom::End(0)).await?;
471 f.seek(SeekFrom::Start(0)).await?;
472 let size_hint: usize = util::cast_file_size(size_hint)?;
473
474 if size_hint > size_limit {
475 return Err(util::file_is_too_large(format!(
476 "Size exceeds {size_limit} bytes"
477 )));
478 }
479
480 let mut buf = Vec::with_capacity(size_hint);
481 f.read_to_end(&mut buf).await?;
482
483 Ok(buf)
484}
485
486#[cfg(feature = "native_crypto")]
487async fn read_crypto_file(file: CryptoFile, size_limit: usize) -> std::io::Result<Vec<u8>> {
488 let mut f = crate::crypto::fs::TokioMaybeCryptoFile::from_crypto_file(file).await?;
489 let size_hint = f.size_hint().await?;
490
491 if size_hint > size_limit {
492 return Err(util::file_is_too_large(format!(
493 "Size exceeds {size_limit} bytes"
494 )));
495 }
496
497 let mut buf = Vec::with_capacity(size_hint);
498 f.read_to_end(&mut buf).await?;
499
500 Ok(buf)
501}
502
503fn validate_uri_preview(uri: String) -> Result<String, PreviewError> {
504 let Some(s) = uri.strip_prefix(URI_HEADER) else {
505 return Err(PreviewError::BadUri(UriDecodeError::NotAUri));
506 };
507
508 if s.len() > MAX_PREVIEW_BYTES * 4 / 3 {
509 return Err(PreviewError::TooLarge);
510 }
511
512 Ok(uri)
513}