fraiseql_server/files/
processing.rs1use std::{collections::HashMap, io::Cursor};
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use image::{DynamicImage, ImageFormat, imageops::FilterType};
8
9use crate::files::{
10 config::{ProcessingConfig, VariantConfig},
11 error::ProcessingError,
12 traits::ImageProcessor,
13};
14
15pub struct ProcessedImages {
16 pub variants: HashMap<String, Bytes>,
17}
18
19pub struct ImageProcessorImpl {
20 config: ProcessingConfig,
21}
22
23impl ImageProcessorImpl {
24 pub fn new(config: ProcessingConfig) -> Self {
25 Self { config }
26 }
27
28 pub fn process_sync(&self, data: &Bytes) -> Result<ProcessedImages, ProcessingError> {
30 let img = image::load_from_memory(data).map_err(|e| ProcessingError::LoadFailed {
32 message: e.to_string(),
33 })?;
34
35 let mut variants = HashMap::new();
40
41 let original_key = "original".to_string();
43 let original_data = self.encode_image(&img, None)?;
44 variants.insert(original_key, original_data);
45
46 for variant_config in &self.config.variants {
48 let resized = self.resize_image(&img, variant_config)?;
49 let encoded = self.encode_image(&resized, None)?;
50
51 variants.insert(variant_config.name.clone(), encoded);
52 }
53
54 Ok(ProcessedImages { variants })
55 }
56
57 fn resize_image(
58 &self,
59 img: &DynamicImage,
60 config: &VariantConfig,
61 ) -> Result<DynamicImage, ProcessingError> {
62 let resized = match config.mode.as_str() {
63 "fit" => img.resize(config.width, config.height, FilterType::Lanczos3),
64 "fill" | "crop" => {
65 img.resize_to_fill(config.width, config.height, FilterType::Lanczos3)
66 },
67 "exact" => img.resize_exact(config.width, config.height, FilterType::Lanczos3),
68 _ => {
69 return Err(ProcessingError::InvalidConfig {
70 message: format!("Invalid resize mode: {}", config.mode),
71 });
72 },
73 };
74
75 Ok(resized)
76 }
77
78 fn encode_image(
79 &self,
80 img: &DynamicImage,
81 quality: Option<u8>,
82 ) -> Result<Bytes, ProcessingError> {
83 let format = match self.config.output_format.as_deref() {
84 Some("webp") => ImageFormat::WebP,
85 Some("jpeg") | Some("jpg") => ImageFormat::Jpeg,
86 Some("png") => ImageFormat::Png,
87 _ => ImageFormat::Jpeg, };
89
90 let quality = quality.or(self.config.quality).unwrap_or(85);
91
92 let mut buffer = Cursor::new(Vec::new());
93
94 match format {
95 ImageFormat::Jpeg => {
96 let encoder =
97 image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buffer, quality);
98 img.write_with_encoder(encoder).map_err(|e| ProcessingError::EncodeFailed {
99 message: e.to_string(),
100 })?;
101 },
102 ImageFormat::WebP => {
103 img.write_to(&mut buffer, ImageFormat::WebP).map_err(|e| {
105 ProcessingError::EncodeFailed {
106 message: e.to_string(),
107 }
108 })?;
109 },
110 ImageFormat::Png => {
111 img.write_to(&mut buffer, ImageFormat::Png).map_err(|e| {
112 ProcessingError::EncodeFailed {
113 message: e.to_string(),
114 }
115 })?;
116 },
117 _ => {
118 return Err(ProcessingError::InvalidConfig {
119 message: format!("Unsupported format: {:?}", format),
120 });
121 },
122 }
123
124 Ok(Bytes::from(buffer.into_inner()))
125 }
126}
127
128#[async_trait]
129impl ImageProcessor for ImageProcessorImpl {
130 async fn process(
131 &self,
132 data: &Bytes,
133 _config: &ProcessingConfig,
134 ) -> Result<ProcessedImages, ProcessingError> {
135 let data = data.clone();
137 let processor = self.clone();
138 tokio::task::spawn_blocking(move || processor.process_sync(&data))
139 .await
140 .map_err(|e| ProcessingError::LoadFailed {
141 message: format!("Task join error: {}", e),
142 })?
143 }
144}
145
146impl Clone for ImageProcessorImpl {
147 fn clone(&self) -> Self {
148 Self {
149 config: self.config.clone(),
150 }
151 }
152}