Skip to main content

fraiseql_server/files/
processing.rs

1//! Image processing pipeline
2
3use 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    /// Process an image and generate variants
29    pub fn process_sync(&self, data: &Bytes) -> Result<ProcessedImages, ProcessingError> {
30        // Load image
31        let img = image::load_from_memory(data).map_err(|e| ProcessingError::LoadFailed {
32            message: e.to_string(),
33        })?;
34
35        // Strip EXIF if configured
36        // Note: image crate doesn't preserve EXIF, so it's stripped by default
37        // For explicit control, we'd need a different approach
38
39        let mut variants = HashMap::new();
40
41        // Generate original (possibly in different format)
42        let original_key = "original".to_string();
43        let original_data = self.encode_image(&img, None)?;
44        variants.insert(original_key, original_data);
45
46        // Generate configured variants
47        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, // Default
88        };
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                // image crate's WebP encoder
104                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        // Run synchronous processing in blocking task
136        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}