docbox_processing/
image.rs

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