docbox_core/processing/
image.rs

1use super::{ProcessingError, ProcessingOutput};
2use crate::files::generated::QueuedUpload;
3use bytes::Bytes;
4use docbox_database::models::generated_file::GeneratedFileType;
5use image::{DynamicImage, ImageDecoder, ImageFormat, ImageReader, ImageResult};
6use std::io::Cursor;
7
8/// Encodes the provided [DynamicImage] into a byte array
9/// in the requested image `format`
10pub fn create_img_bytes(image: &DynamicImage, format: ImageFormat) -> ImageResult<Vec<u8>> {
11    let mut buffer = Cursor::new(Vec::new());
12    image.write_to(&mut buffer, format)?;
13    Ok(buffer.into_inner())
14}
15
16/// Image processing is CPU intensive, this async variant moves the image processing
17/// to a separate thread where blocking is acceptable to prevent blocking other
18/// asynchronous tasks
19pub async fn process_image_async(
20    file_bytes: Bytes,
21    format: ImageFormat,
22) -> Result<ProcessingOutput, ProcessingError> {
23    tokio::task::spawn_blocking(move || process_image(file_bytes, format)).await?
24}
25
26/// Processes a compatible image file
27///
28/// Creates multiple small to medium sized thumbnail preview images for faster
29/// previewing within the browser
30fn process_image(
31    file_bytes: Bytes,
32    format: ImageFormat,
33) -> Result<ProcessingOutput, ProcessingError> {
34    let mut decoder = ImageReader::with_format(Cursor::new(&file_bytes), format)
35        .into_decoder()
36        .map_err(ProcessingError::DecodeImage)?;
37
38    // Extract the image orientation
39    let orientation = decoder
40        .orientation()
41        .map_err(ProcessingError::DecodeImage)?;
42
43    let mut img = DynamicImage::from_decoder(decoder).map_err(ProcessingError::DecodeImage)?;
44
45    // Apply image exif orientation
46    img.apply_orientation(orientation);
47
48    tracing::debug!("generated image thumbnails");
49
50    let generated =
51        generate_image_preview(img, format).map_err(ProcessingError::GenerateThumbnail)?;
52
53    let upload_queue = vec![
54        QueuedUpload::new(
55            mime::IMAGE_JPEG,
56            GeneratedFileType::LargeThumbnail,
57            generated.large_thumbnail_jpeg.into(),
58        ),
59        QueuedUpload::new(
60            mime::IMAGE_JPEG,
61            GeneratedFileType::SmallThumbnail,
62            generated.thumbnail_jpeg.into(),
63        ),
64    ];
65
66    Ok(ProcessingOutput {
67        upload_queue,
68        ..Default::default()
69    })
70}
71
72/// Generated preview images for a file
73struct GeneratedPreviewImages {
74    /// Small 64x64 file thumbnail
75    thumbnail_jpeg: Vec<u8>,
76    /// Smaller 385x385 version of first page
77    /// (Not actually 385x385 fits whatever the image aspect ratio inside those dimensions)
78    large_thumbnail_jpeg: Vec<u8>,
79}
80
81fn generate_image_preview(
82    image: DynamicImage,
83    format: ImageFormat,
84) -> anyhow::Result<GeneratedPreviewImages> {
85    tracing::debug!("rendering image preview variants");
86
87    let thumbnail_jpeg = create_thumbnail(&image, format)?;
88    let large_thumbnail_jpeg = create_thumbnail_large(&image, format)?;
89
90    Ok(GeneratedPreviewImages {
91        thumbnail_jpeg,
92        large_thumbnail_jpeg,
93    })
94}
95
96fn create_thumbnail(image: &DynamicImage, format: ImageFormat) -> ImageResult<Vec<u8>> {
97    let thumbnail = image.thumbnail(64, 64);
98    create_img_bytes(&thumbnail, format)
99}
100
101fn create_thumbnail_large(image: &DynamicImage, format: ImageFormat) -> ImageResult<Vec<u8>> {
102    let (width, height) = match format {
103        // .ico format has specific max size requirements
104        ImageFormat::Ico => (256, 256),
105        _ => (512, 512),
106    };
107
108    let cover_page_preview = image.resize(width, height, image::imageops::FilterType::Triangle);
109    create_img_bytes(&cover_page_preview, format)
110}