Skip to main content

img_gen_spec/validators/
layout.rs

1#[cfg(feature = "pyo3")]
2use pyo3::prelude::*;
3
4use serde::{
5    Deserialize, Deserializer, Serialize,
6    de::{self, MapAccess, Visitor},
7};
8
9use super::{
10    SolidColor,
11    layers::{Background, Ellipse, Icon, LayerOffset, Polygon, Rectangle, Size, Typography},
12};
13
14/// An attribute to describe a [`Layer`]'s [`Mask`].
15#[cfg_attr(
16    feature = "pyo3",
17    pyclass(module = "img_gen", get_all, set_all, from_py_object)
18)]
19#[derive(Debug, Clone, Default, Serialize, Deserialize)]
20pub struct Mask {
21    /// The mask's [`Size`].
22    pub size: Option<Size>,
23
24    /// The mask's [`LayerOffset`].
25    #[serde(default)]
26    pub offset: LayerOffset,
27
28    /// A flag to control the behavior of the mask.
29    ///
30    /// False means only visible pixels are used in the mask.
31    /// True means only invisible pixels are used in the mask.
32    #[serde(default)]
33    pub invert: bool,
34
35    /// A background attribute for the mask.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub background: Option<Background>,
38    /// A rectangle attribute for the mask.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub rectangle: Option<Rectangle>,
41    /// An ellipse attribute for the mask.
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub ellipse: Option<Ellipse>,
44    /// An polygon attribute for the mask.
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub polygon: Option<Polygon>,
47    /// An icon attribute for the mask.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub icon: Option<Icon>,
50    /// A typography attribute for the mask.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub typography: Option<Typography>,
53}
54
55/// A data structure to represent a single [`Layer`] in a [`Layout`].
56#[cfg_attr(
57    feature = "pyo3",
58    pyclass(module = "img_gen", get_all, set_all, from_py_object)
59)]
60#[derive(Debug, Clone, Default, Serialize, Deserialize)]
61pub struct Layer {
62    /// The layer's [`Size`]
63    pub size: Option<Size>,
64
65    /// The layer's [`LayerOffset`].
66    #[serde(default)]
67    pub offset: LayerOffset,
68
69    /// A background attribute for the layer.
70    pub background: Option<Background>,
71    /// A rectangle attribute for the layer.
72    pub rectangle: Option<Rectangle>,
73    /// An ellipse attribute for the layer.
74    pub ellipse: Option<Ellipse>,
75    /// An polygon attribute for the layer.
76    pub polygon: Option<Polygon>,
77    /// An icon attribute for the layer.
78    pub icon: Option<Icon>,
79    /// A typography attribute for the layer.
80    pub typography: Option<Typography>,
81    /// A mask attribute for the layer.
82    pub mask: Option<Mask>,
83}
84
85/// A struct to describe a [`Layout`]'s visual debug output.
86#[cfg_attr(
87    feature = "pyo3",
88    pyclass(module = "img_gen", set_all, get_all, from_py_object)
89)]
90#[derive(Debug, Clone, Serialize)]
91pub struct Debug {
92    /// A flag to enable or disable the debug output.
93    pub enable: bool,
94    /// A flag to control if the debug output shall show a grid of points over the layout.
95    pub grid: bool,
96    /// The space between points on the debug output's grid.
97    pub grid_step: u32,
98    /// The color used to outline debug output.
99    pub color: SolidColor,
100}
101
102impl Debug {
103    /// Calculate a black or white foreground color using [`Debug::color`] as a background.
104    pub fn get_foreground_color(&self) -> SolidColor {
105        let luminance = {
106            let mut result = 0.0f32;
107            for (index, c) in vec![
108                &self.color.get_r(),
109                &self.color.get_g(),
110                &self.color.get_b(),
111            ]
112            .into_iter()
113            .take(3)
114            .enumerate()
115            {
116                let component = *c as f32 / 255.0;
117                let new_component = if component <= 0.03928 {
118                    component / 12.92
119                } else {
120                    ((component + 0.055) / 1.055).powf(2.4)
121                };
122                match index {
123                    0 => {
124                        result += 0.2126 * new_component;
125                    }
126                    1 => {
127                        result += 0.7152 * new_component;
128                    }
129                    _ => {
130                        result += 0.0722 * new_component;
131                    }
132                }
133            }
134            result
135        };
136        if luminance > 0.451 {
137            SolidColor::new(0, 0, 0, 255)
138        } else {
139            SolidColor::new(255, 255, 255, 255)
140        }
141    }
142
143    pub(crate) const fn default_grid_step() -> u32 {
144        30
145    }
146
147    pub(crate) fn default_color() -> SolidColor {
148        SolidColor::new(128, 128, 128, 255)
149    }
150
151    const fn default_grid() -> bool {
152        true
153    }
154}
155
156impl<'de> Deserialize<'de> for Debug {
157    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
158        struct DebugVisitor;
159
160        impl<'de> Visitor<'de> for DebugVisitor {
161            type Value = Debug;
162
163            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
164                formatter.write_str("a boolean or a debug config object")
165            }
166
167            fn visit_bool<E: de::Error>(self, v: bool) -> Result<Debug, E> {
168                Ok(if v {
169                    Debug {
170                        enable: true,
171                        ..Debug::default()
172                    }
173                } else {
174                    Debug::default()
175                })
176            }
177
178            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Debug, A::Error> {
179                let mut enable = None;
180                let mut grid = None;
181                let mut grid_step = None;
182                let mut color = None;
183
184                while let Some(key) = map.next_key::<std::borrow::Cow<str>>()? {
185                    match key.as_ref() {
186                        "enable" => enable = Some(map.next_value()?),
187                        "grid" => grid = Some(map.next_value()?),
188                        "grid_step" => grid_step = Some(map.next_value()?),
189                        "color" => color = Some(map.next_value()?),
190                        unknown => {
191                            return Err(de::Error::unknown_field(
192                                unknown,
193                                &["enable", "grid", "grid_step", "color"],
194                            ));
195                        }
196                    }
197                }
198
199                Ok(Debug {
200                    enable: enable.unwrap_or(false),
201                    grid: grid.unwrap_or_else(Debug::default_grid),
202                    grid_step: grid_step.unwrap_or_else(Debug::default_grid_step),
203                    color: color.unwrap_or_else(Debug::default_color),
204                })
205            }
206        }
207
208        deserializer.deserialize_any(DebugVisitor)
209    }
210}
211
212impl Default for Debug {
213    fn default() -> Self {
214        Self {
215            enable: false,
216            grid: Self::default_grid(),
217            grid_step: Self::default_grid_step(),
218            color: Self::default_color(),
219        }
220    }
221}
222
223/// A data structure used to represent a generated image's layout.
224#[cfg_attr(
225    feature = "pyo3",
226    pyclass(module = "img_gen", set_all, get_all, from_py_object)
227)]
228#[derive(Debug, Clone, Default, Serialize, Deserialize)]
229pub struct Layout {
230    /// The layout's `Size`
231    #[serde(default)]
232    pub size: Size,
233
234    /// A list of the layout's `Layer` objects.
235    #[serde(default)]
236    pub layers: Vec<Layer>,
237
238    /// An optional `Debug` attribute can be used to show the constraints of the `Layout`'s `layers`.
239    pub debug: Option<Debug>,
240}
241
242#[cfg(test)]
243mod tests {
244    #![allow(clippy::unwrap_used)]
245
246    use super::Debug;
247
248    fn assert_all_defaults(d: &Debug) {
249        assert_eq!(d.grid, Debug::default_grid());
250        assert_eq!(d.grid_step, Debug::default_grid_step());
251        assert_eq!(d.color.to_tuple(), Debug::default_color().to_tuple());
252    }
253
254    #[test]
255    fn debug_bool_true() {
256        let d: Debug = serde_saphyr::from_str("true").unwrap();
257        assert!(d.enable);
258        assert_all_defaults(&d);
259    }
260
261    #[test]
262    fn debug_bool_false() {
263        let d: Debug = serde_saphyr::from_str("false").unwrap();
264        assert!(!d.enable);
265        assert_all_defaults(&d);
266    }
267
268    #[test]
269    fn debug_map_full() {
270        let yaml = "enable: true\ngrid: false\ngrid_step: 50\ncolor: \"blue\"\n";
271        let d: Debug = serde_saphyr::from_str(yaml).unwrap();
272        assert!(d.enable);
273        assert!(!d.grid);
274        assert_eq!(d.grid_step, 50);
275        assert_eq!(d.color.to_tuple(), (0, 0, 255, 255));
276    }
277
278    #[test]
279    fn debug_map_defaults() {
280        let d: Debug = serde_saphyr::from_str("{}").unwrap();
281        assert!(!d.enable);
282        assert_all_defaults(&d);
283    }
284
285    #[test]
286    fn debug_map_unknown_field() {
287        let result: Result<Debug, _> = serde_saphyr::from_str("unknown_key: true\n");
288        assert!(result.is_err());
289    }
290
291    #[test]
292    fn debug_invalid_type() {
293        // Passing an integer triggers the unimplemented visitor path, which calls expecting()
294        let result: Result<Debug, _> = serde_saphyr::from_str("42");
295        assert!(result.is_err());
296    }
297
298    #[test]
299    fn debug_foreground_dark_color() {
300        // Black (0,0,0): all channels <= 0.03928 threshold -> component / 12.92 path (L111)
301        // Luminance = 0 <= 0.451 -> white foreground
302        let d: Debug = serde_saphyr::from_str("color: \"black\"\n").unwrap();
303        assert_eq!(d.get_foreground_color().to_tuple(), (255, 255, 255, 255));
304    }
305
306    #[test]
307    fn debug_foreground_bright_color() {
308        // White (255,255,255): luminance ~= 1.0 > 0.451 -> black foreground (L130)
309        let d: Debug = serde_saphyr::from_str("color: \"white\"\n").unwrap();
310        assert_eq!(d.get_foreground_color().to_tuple(), (0, 0, 0, 255));
311    }
312}