nexus_common/media/processors/
image.rs1use 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") .arg(output)
78 .output() .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 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() .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}