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::default(),
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::default(),
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::default(),
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::default(),
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::default(),
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 {
222 enabled: true,
223 size: (128, 128),
224 quality: 60,
225 blur: 0.0,
226 }
227 }
228 }
229
230 impl Transcoder {
231 pub fn disabled() -> Self {
233 Self {
234 enabled: false,
235 ..Default::default()
236 }
237 }
238
239 pub fn is_enabled(&self) -> bool {
240 self.enabled
241 }
242
243 pub fn with_size(mut self, x: u8, y: u8) -> Self {
245 let x = std::cmp::max(32, x);
246 let y = std::cmp::max(32, y);
247
248 self.size = (x, y);
249
250 self
251 }
252
253 pub fn with_quality(mut self, quality: u8) -> Self {
255 if quality == 0 {
256 self.quality = 1;
257 } else if quality > 100 {
258 self.quality = 100;
259 } else {
260 self.quality = quality;
261 }
262
263 self
264 }
265
266 pub fn with_blur(mut self, sigma: f32) -> Self {
268 if sigma < 1.0 {
269 self.blur = 0.0;
270 } else if sigma > 100.0 {
271 self.blur = 100.0
272 } else {
273 self.blur = sigma
274 };
275
276 self
277 }
278
279 pub fn transcode_to_jpg(self, mut bytes: Vec<u8>) -> Result<Vec<u8>, PreviewError> {
283 if !self.enabled {
284 return Ok(bytes);
285 }
286
287 let img = ImageReader::new(Cursor::new(&bytes))
288 .with_guessed_format()?
289 .decode()?;
290
291 let img = img.thumbnail(self.size.0.into(), self.size.1.into());
292
293 let img = if self.blur >= 1.0 {
294 img.fast_blur(self.blur)
295 } else {
296 img
297 };
298
299 bytes.clear();
300 let encoder = JpegEncoder::new_with_quality(&mut bytes, self.quality);
301 img.write_with_encoder(encoder)?;
302
303 Ok(bytes)
304 }
305 }
306}
307
308#[cfg(feature = "multimedia")]
309pub use transcoder::Transcoder;
310
311const URI_HEADER: &str = "data:image/jpg;base64,";
312
313pub fn default() -> String {
314 DEFAULT_PREVIEW.to_owned()
315}
316
317pub fn encode_jpg_to_uri(bytes: &[u8]) -> String {
319 match try_encode_jpg_to_uri(bytes) {
320 Ok(s) => s,
321 Err(e) => {
322 log::warn!("{e}");
323 default()
324 }
325 }
326}
327
328pub fn try_encode_jpg_to_uri(bytes: &[u8]) -> Result<String, PreviewError> {
329 if bytes.len() > MAX_PREVIEW_BYTES {
330 return Err(PreviewError::TooLarge);
331 }
332
333 let mut encoded = String::with_capacity(bytes.len() * 4 / 3 + URI_HEADER.len() + 3);
334 encoded.push_str(URI_HEADER);
335 BASE64_STANDARD.encode_string(bytes, &mut encoded);
336
337 Ok(encoded)
338}
339
340pub fn try_decode_jpg_from_uri(uri_str: &str) -> Result<Vec<u8>, UriDecodeError> {
341 let Some(s) = uri_str.strip_prefix(URI_HEADER) else {
342 return Err(UriDecodeError::NotAUri);
343 };
344
345 BASE64_STANDARD.decode(s).map_err(UriDecodeError::Base64)
346}
347
348#[derive(Debug)]
349pub enum PreviewError {
350 TooLarge,
351 BadUri(UriDecodeError),
352 Io(std::io::Error),
353 #[cfg(feature = "multimedia")]
354 Transcoding(image::ImageError),
355 #[cfg(feature = "multimedia")]
356 Tokio(tokio::task::JoinError),
357}
358
359impl From<std::io::Error> for PreviewError {
360 fn from(err: std::io::Error) -> Self {
361 Self::Io(err)
362 }
363}
364
365#[cfg(feature = "multimedia")]
366impl From<image::ImageError> for PreviewError {
367 fn from(err: image::ImageError) -> Self {
368 Self::Transcoding(err)
369 }
370}
371
372#[cfg(feature = "multimedia")]
373impl From<tokio::task::JoinError> for PreviewError {
374 fn from(err: tokio::task::JoinError) -> Self {
375 Self::Tokio(err)
376 }
377}
378
379impl std::fmt::Display for PreviewError {
380 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381 match self {
382 Self::TooLarge => {
383 write!(
384 f,
385 "preview size exceeds the max possible size({MAX_PREVIEW_BYTES} bytes)"
386 )
387 }
388 Self::BadUri(e) => write!(f, "{e}"),
389 Self::Io(error) => write!(f, "Cannot process preview file: {error}"),
390 #[cfg(feature = "multimedia")]
391 Self::Transcoding(error) => write!(f, "Cannot transcode preview: {error}"),
392 #[cfg(feature = "multimedia")]
393 Self::Tokio(error) => write!(f, "Failed to join the transcoding task: {error}"),
394 }
395 }
396}
397
398impl std::error::Error for PreviewError {
399 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
400 match self {
401 Self::TooLarge => None,
402 Self::BadUri(error) => Some(error),
403 Self::Io(error) => Some(error),
404 #[cfg(feature = "multimedia")]
405 Self::Transcoding(error) => Some(error),
406 #[cfg(feature = "multimedia")]
407 Self::Tokio(error) => Some(error),
408 }
409 }
410}
411
412#[derive(Debug)]
413pub enum UriDecodeError {
414 NotAUri,
415 Base64(base64::DecodeError),
416}
417
418impl std::fmt::Display for UriDecodeError {
419 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
420 match self {
421 Self::NotAUri => write!(f, "not a URI string"),
422 Self::Base64(e) => write!(f, "{e}"),
423 }
424 }
425}
426
427impl std::error::Error for UriDecodeError {
428 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
429 if let Self::Base64(e) = self {
430 Some(e)
431 } else {
432 None
433 }
434 }
435}
436
437#[derive(Clone)]
438enum PreviewSource {
439 Default,
440 Bytes(Vec<u8>),
441 DataUri(String),
442 File(PathBuf),
443 #[cfg(feature = "native_crypto")]
444 CryptoFile(CryptoFile),
445}
446
447async fn read_plain_file(path: &PathBuf, size_limit: usize) -> std::io::Result<Vec<u8>> {
448 let mut f = tokio::fs::File::open(&path).await?;
449 let size_hint = f.seek(SeekFrom::End(0)).await?;
450 f.seek(SeekFrom::Start(0)).await?;
451 let size_hint: usize = util::cast_file_size(size_hint)?;
452
453 if size_hint > size_limit {
454 return Err(util::file_is_too_large(format!(
455 "Size exceeds {size_limit} bytes"
456 )));
457 }
458
459 let mut buf = Vec::with_capacity(size_hint);
460 f.read_to_end(&mut buf).await?;
461
462 Ok(buf)
463}
464
465#[cfg(feature = "native_crypto")]
466async fn read_crypto_file(file: CryptoFile, size_limit: usize) -> std::io::Result<Vec<u8>> {
467 let mut f = crate::crypto::fs::TokioMaybeCryptoFile::from_crypto_file(file).await?;
468 let size_hint = f.size_hint().await?;
469
470 if size_hint > size_limit {
471 return Err(util::file_is_too_large(format!(
472 "Size exceeds {size_limit} bytes"
473 )));
474 }
475
476 let mut buf = Vec::with_capacity(size_hint);
477 f.read_to_end(&mut buf).await?;
478
479 Ok(buf)
480}
481
482fn validate_uri_preview(uri: String) -> Result<String, PreviewError> {
483 let Some(s) = uri.strip_prefix(URI_HEADER) else {
484 return Err(PreviewError::BadUri(UriDecodeError::NotAUri));
485 };
486
487 if s.len() > MAX_PREVIEW_BYTES * 4 / 3 {
488 return Err(PreviewError::TooLarge);
489 }
490
491 Ok(uri)
492}