Skip to main content

rustrails_storage/
transformations.rs

1//! Typed image transformation configuration helpers.
2
3use std::collections::BTreeMap;
4
5use serde_json::{Value, json};
6
7/// Resize dimensions for an image variant.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct ResizeTransform {
10    /// Target width in pixels.
11    pub width: u32,
12    /// Target height in pixels.
13    pub height: u32,
14}
15
16impl ResizeTransform {
17    /// Creates a resize transform.
18    #[must_use]
19    pub const fn new(width: u32, height: u32) -> Self {
20        Self { width, height }
21    }
22}
23
24/// Crop rectangle for an image variant.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct CropTransform {
27    /// Left offset in pixels.
28    pub x: u32,
29    /// Top offset in pixels.
30    pub y: u32,
31    /// Crop width in pixels.
32    pub width: u32,
33    /// Crop height in pixels.
34    pub height: u32,
35}
36
37impl CropTransform {
38    /// Creates a crop transform.
39    #[must_use]
40    pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
41        Self {
42            x,
43            y,
44            width,
45            height,
46        }
47    }
48}
49
50/// Typed image transformation configuration that converts into variant options.
51#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct ImageTransformations {
53    /// Resize instruction applied before upload.
54    pub resize: Option<ResizeTransform>,
55    /// Crop instruction applied before upload.
56    pub crop: Option<CropTransform>,
57    /// Optional output image format such as `png`.
58    pub format: Option<String>,
59}
60
61impl ImageTransformations {
62    /// Creates an empty transformation set.
63    #[must_use]
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Adds a resize transform.
69    #[must_use]
70    pub fn resize(mut self, width: u32, height: u32) -> Self {
71        self.resize = Some(ResizeTransform::new(width, height));
72        self
73    }
74
75    /// Adds a crop transform.
76    #[must_use]
77    pub fn crop(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
78        self.crop = Some(CropTransform::new(x, y, width, height));
79        self
80    }
81
82    /// Sets the output image format.
83    #[must_use]
84    pub fn format(mut self, format: impl Into<String>) -> Self {
85        self.format = Some(format.into());
86        self
87    }
88
89    /// Converts the transformation set into variant options.
90    #[must_use]
91    pub fn to_map(&self) -> BTreeMap<String, Value> {
92        let mut transformations = BTreeMap::new();
93        if let Some(resize) = self.resize {
94            transformations.insert(
95                "resize".to_owned(),
96                json!({"width": resize.width, "height": resize.height}),
97            );
98        }
99        if let Some(crop) = self.crop {
100            transformations.insert(
101                "crop".to_owned(),
102                json!({
103                    "x": crop.x,
104                    "y": crop.y,
105                    "width": crop.width,
106                    "height": crop.height,
107                }),
108            );
109        }
110        if let Some(format) = &self.format {
111            transformations.insert("format".to_owned(), Value::String(format.clone()));
112        }
113        transformations
114    }
115}
116
117impl From<ImageTransformations> for BTreeMap<String, Value> {
118    fn from(value: ImageTransformations) -> Self {
119        value.to_map()
120    }
121}
122
123impl From<&ImageTransformations> for BTreeMap<String, Value> {
124    fn from(value: &ImageTransformations) -> Self {
125        value.to_map()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn resize_and_crop_transformations_convert_to_variant_map() {
135        let transformations = ImageTransformations::new()
136            .resize(100, 200)
137            .crop(10, 20, 80, 160)
138            .format("png")
139            .to_map();
140        assert_eq!(
141            transformations.get("resize"),
142            Some(&json!({"width": 100, "height": 200})),
143        );
144        assert_eq!(
145            transformations.get("crop"),
146            Some(&json!({"x": 10, "y": 20, "width": 80, "height": 160})),
147        );
148        assert_eq!(
149            transformations.get("format"),
150            Some(&Value::String("png".to_owned()))
151        );
152    }
153
154    #[test]
155    fn empty_transformations_produce_empty_map() {
156        assert!(ImageTransformations::new().to_map().is_empty());
157    }
158}