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    pub(crate) const fn default_grid_step() -> u32 {
104        30
105    }
106
107    pub(crate) fn default_color() -> SolidColor {
108        SolidColor::new(128, 128, 128, 255)
109    }
110
111    const fn default_grid() -> bool {
112        true
113    }
114}
115
116impl<'de> Deserialize<'de> for Debug {
117    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
118        struct DebugVisitor;
119
120        impl<'de> Visitor<'de> for DebugVisitor {
121            type Value = Debug;
122
123            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
124                formatter.write_str("a boolean or a debug config object")
125            }
126
127            fn visit_bool<E: de::Error>(self, v: bool) -> Result<Debug, E> {
128                Ok(if v {
129                    Debug {
130                        enable: true,
131                        ..Debug::default()
132                    }
133                } else {
134                    Debug::default()
135                })
136            }
137
138            fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Debug, A::Error> {
139                let mut enable = None;
140                let mut grid = None;
141                let mut grid_step = None;
142                let mut color = None;
143
144                while let Some(key) = map.next_key::<std::borrow::Cow<str>>()? {
145                    match key.as_ref() {
146                        "enable" => enable = Some(map.next_value()?),
147                        "grid" => grid = Some(map.next_value()?),
148                        "grid_step" => grid_step = Some(map.next_value()?),
149                        "color" => color = Some(map.next_value()?),
150                        unknown => {
151                            return Err(de::Error::unknown_field(
152                                unknown,
153                                &["enable", "grid", "grid_step", "color"],
154                            ));
155                        }
156                    }
157                }
158
159                Ok(Debug {
160                    enable: enable.unwrap_or(false),
161                    grid: grid.unwrap_or_else(Debug::default_grid),
162                    grid_step: grid_step.unwrap_or_else(Debug::default_grid_step),
163                    color: color.unwrap_or_else(Debug::default_color),
164                })
165            }
166        }
167
168        deserializer.deserialize_any(DebugVisitor)
169    }
170}
171
172impl Default for Debug {
173    fn default() -> Self {
174        Self {
175            enable: false,
176            grid: Self::default_grid(),
177            grid_step: Self::default_grid_step(),
178            color: Self::default_color(),
179        }
180    }
181}
182
183/// A data structure used to represent a generated image's layout.
184#[cfg_attr(
185    feature = "pyo3",
186    pyclass(module = "img_gen", set_all, get_all, from_py_object)
187)]
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct Layout {
190    /// The layout's `Size`
191    #[serde(default)]
192    pub size: Size,
193
194    /// A list of the layout's `Layer` objects.
195    #[serde(default)]
196    pub layers: Vec<Layer>,
197
198    /// An optional `Debug` attribute can be used to show the constraints of the `Layout`'s `layers`.
199    pub debug: Option<Debug>,
200}
201
202#[cfg(test)]
203mod tests {
204    #![allow(clippy::unwrap_used)]
205
206    use super::Debug;
207
208    fn assert_all_defaults(d: &Debug) {
209        assert_eq!(d.grid, Debug::default_grid());
210        assert_eq!(d.grid_step, Debug::default_grid_step());
211        assert_eq!(d.color.to_tuple(), Debug::default_color().to_tuple());
212    }
213
214    #[test]
215    fn debug_bool_true() {
216        let d: Debug = serde_saphyr::from_str("true").unwrap();
217        assert!(d.enable);
218        assert_all_defaults(&d);
219    }
220
221    #[test]
222    fn debug_bool_false() {
223        let d: Debug = serde_saphyr::from_str("false").unwrap();
224        assert!(!d.enable);
225        assert_all_defaults(&d);
226    }
227
228    #[test]
229    fn debug_map_full() {
230        let yaml = "enable: true\ngrid: false\ngrid_step: 50\ncolor: \"blue\"\n";
231        let d: Debug = serde_saphyr::from_str(yaml).unwrap();
232        assert!(d.enable);
233        assert!(!d.grid);
234        assert_eq!(d.grid_step, 50);
235        assert_eq!(d.color.to_tuple(), (0, 0, 255, 255));
236    }
237
238    #[test]
239    fn debug_map_defaults() {
240        let d: Debug = serde_saphyr::from_str("{}").unwrap();
241        assert!(!d.enable);
242        assert_all_defaults(&d);
243    }
244
245    #[test]
246    fn debug_map_unknown_field() {
247        let result: Result<Debug, _> = serde_saphyr::from_str("unknown_key: true\n");
248        assert!(result.is_err());
249    }
250
251    #[test]
252    fn debug_invalid_type() {
253        // Passing an integer triggers the unimplemented visitor path, which calls expecting()
254        let result: Result<Debug, _> = serde_saphyr::from_str("42");
255        assert!(result.is_err());
256    }
257
258    #[test]
259    fn debug_foreground_dark_color() {
260        // Black (0,0,0): all channels <= 0.03928 threshold -> component / 12.92 path (L111)
261        // Luminance = 0 <= 0.451 -> white foreground
262        let d: Debug = serde_saphyr::from_str("color: \"black\"\n").unwrap();
263        assert_eq!(
264            d.color.get_foreground_color().to_tuple(),
265            (255, 255, 255, 255)
266        );
267    }
268
269    #[test]
270    fn debug_foreground_bright_color() {
271        // White (255,255,255): luminance ~= 1.0 > 0.451 -> black foreground (L130)
272        let d: Debug = serde_saphyr::from_str("color: \"white\"\n").unwrap();
273        assert_eq!(d.color.get_foreground_color().to_tuple(), (0, 0, 0, 255));
274    }
275}