Skip to main content

nexus_common/media/processors/
image.rs

1use async_trait::async_trait;
2use tokio::process::Command;
3
4use crate::{media::FileVariant, models::file::FileDetails, types::DynError};
5
6use super::{BaseProcessingOptions, VariantProcessor};
7
8const SMALL_IMAGE_WIDTH: &str = "320";
9const FEED_IMAGE_WIDTH: &str = "720";
10const IMAGE_FORMAT: &str = "webp";
11
12pub struct ImageOptions {
13    width: String,
14    format: String,
15    content_type: String,
16}
17
18impl BaseProcessingOptions for ImageOptions {
19    fn content_type(&self) -> String {
20        self.content_type.clone()
21    }
22}
23
24pub struct ImageProcessor;
25
26#[async_trait]
27impl VariantProcessor for ImageProcessor {
28    type ProcessingOptions = ImageOptions;
29
30    fn get_valid_variants_for_content_type(_content_type: &str) -> Vec<FileVariant> {
31        vec![FileVariant::Main, FileVariant::Small, FileVariant::Feed]
32    }
33
34    fn get_content_type_for_variant(file: &FileDetails, variant: &FileVariant) -> String {
35        if variant.eq(&FileVariant::Main) {
36            return file.content_type.clone();
37        }
38        String::from("image/webp")
39    }
40
41    fn get_options_for_variant(
42        file: &FileDetails,
43        variant: &FileVariant,
44    ) -> Result<ImageOptions, DynError> {
45        let width = match variant {
46            FileVariant::Small => String::from(SMALL_IMAGE_WIDTH),
47            FileVariant::Feed => String::from(FEED_IMAGE_WIDTH),
48            _ => return Err("Unsupported image variant".into()),
49        };
50        let content_type = Self::get_content_type_for_variant(file, variant);
51        Ok(ImageOptions {
52            format: IMAGE_FORMAT.to_string(),
53            width,
54            content_type,
55        })
56    }
57
58    async fn process(
59        origin_file_path: &str,
60        output_file_path: &str,
61        options: &ImageOptions,
62    ) -> Result<String, DynError> {
63        let origin_file_format = ImageProcessor::get_format(origin_file_path)
64            .await?
65            .to_lowercase();
66
67        let output = match origin_file_format == options.format {
68            true => output_file_path.to_string(),
69            false => format!("{}:{}", options.format, output_file_path),
70        };
71
72        let child_output = Command::new("convert")
73            .arg(origin_file_path)
74            .arg("-resize")
75            .arg(format!("{}x", options.width))
76            .arg("-auto-orient") // https://github.com/ImageMagick/ImageMagick/issues/6396
77            .arg(output)
78            .output() // Automatically pipes stdout and stderr
79            .await?;
80
81        if child_output.status.success() {
82            Ok(String::from_utf8_lossy(&child_output.stdout).to_string())
83        } else {
84            Err(format!(
85                "ImageMagick command failed: {}",
86                String::from_utf8_lossy(&child_output.stdout)
87            )
88            .into())
89        }
90    }
91}
92
93impl ImageProcessor {
94    // function to get image format
95    async fn get_format(file_path: &str) -> Result<String, DynError> {
96        let child_output = match Command::new("identify")
97            .arg("-format")
98            .arg("%m")
99            .arg(file_path)
100            .output() // Automatically pipes stdout and stderr
101            .await
102        {
103            Ok(output) => output,
104            Err(err) => return Err(err.into()),
105        };
106
107        if child_output.status.success() {
108            Ok(String::from_utf8_lossy(&child_output.stdout).to_string())
109        } else {
110            Err(format!(
111                "ImageMagick format extraction failed: {}",
112                String::from_utf8_lossy(&child_output.stderr)
113            )
114            .into())
115        }
116    }
117}