1use crate::{
4 GeneratorIssue, LayoutNode, MAX_TEXT_CONTENT_BYTES, MAX_TEXT_GENERATOR_FONT_SIZE, ShapeItem,
5 TextStyle, Vec2,
6};
7use serde::{Deserialize, Serialize};
8
9pub const MAX_GRADIENT_STOPS: usize = 8;
11
12#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[cfg_attr(
21 feature = "schema",
22 schemars(rename = "Rgba", transform = crate::schema::with_hex_form)
23)]
24#[serde(remote = "Self", deny_unknown_fields)]
25pub struct Rgba {
26 pub red: f32,
28 pub green: f32,
30 pub blue: f32,
32 pub alpha: f32,
34}
35
36impl Rgba {
37 #[must_use]
39 pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
40 Self {
41 red,
42 green,
43 blue,
44 alpha,
45 }
46 }
47
48 pub(crate) fn is_valid(self) -> bool {
49 [self.red, self.green, self.blue, self.alpha]
50 .iter()
51 .all(|value| (0.0..=1.0).contains(value))
52 }
53
54 #[must_use]
56 pub fn from_hex(text: &str) -> Option<Self> {
57 let digits = text.strip_prefix('#')?.as_bytes();
58 let nibble = |index: usize| char::from(digits[index]).to_digit(16);
59 let component = |value: u32| u8::try_from(value).ok().map(|byte| f32::from(byte) / 255.0);
60 let channels = match digits.len() {
61 3 | 4 => (0..digits.len())
62 .map(|index| component(nibble(index)? * 17))
63 .collect::<Option<Vec<_>>>()?,
64 6 | 8 => (0..digits.len() / 2)
65 .map(|index| component(nibble(index * 2)? * 16 + nibble(index * 2 + 1)?))
66 .collect::<Option<Vec<_>>>()?,
67 _ => return None,
68 };
69 Some(Self::new(
70 channels[0],
71 channels[1],
72 channels[2],
73 channels.get(3).copied().unwrap_or(1.0),
74 ))
75 }
76}
77
78struct RgbaVisitor;
80
81impl<'de> serde::de::Visitor<'de> for RgbaVisitor {
82 type Value = Rgba;
83
84 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 formatter.write_str("a color: the {red, green, blue, alpha} object or a CSS hex string")
86 }
87
88 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Rgba, E> {
89 Rgba::from_hex(value).ok_or_else(|| {
90 E::custom(format!(
91 "invalid color \"{value}\": write #rgb, #rgba, #rrggbb, or #rrggbbaa"
92 ))
93 })
94 }
95
96 fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Rgba, A::Error> {
97 Rgba::deserialize(serde::de::value::MapAccessDeserializer::new(map))
98 }
99}
100
101impl<'de> Deserialize<'de> for Rgba {
102 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
103 deserializer.deserialize_any(RgbaVisitor)
104 }
105}
106
107impl Serialize for Rgba {
108 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
109 Self::serialize(self, serializer)
110 }
111}
112
113#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
124pub enum Generator {
125 Fill {
127 paint: Paint,
129 },
130 Shapes {
135 items: Vec<ShapeItem>,
137 },
138 Text {
143 text: String,
146 #[serde(default)]
149 style: TextStyle,
150 },
151 Layout {
156 root: Box<LayoutNode>,
158 },
159}
160
161#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
171#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
172#[cfg_attr(
173 feature = "schema",
174 schemars(rename = "Paint", transform = crate::schema::with_color_form)
175)]
176#[serde(
177 remote = "Self",
178 tag = "type",
179 rename_all = "snake_case",
180 deny_unknown_fields
181)]
182pub enum Paint {
183 Solid {
185 color: Rgba,
187 },
188 LinearGradient {
190 start: Vec2,
192 end: Vec2,
194 stops: Vec<GradientStop>,
196 #[serde(default)]
198 spread: Spread,
199 },
200 RadialGradient {
202 center: Vec2,
204 radius: f32,
207 stops: Vec<GradientStop>,
209 #[serde(default)]
211 spread: Spread,
212 },
213 ConicGradient {
215 center: Vec2,
217 #[serde(default)]
219 angle_degrees: f32,
220 stops: Vec<GradientStop>,
222 },
223}
224
225impl<'de> Deserialize<'de> for Paint {
226 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
227 use serde::de::Error as _;
228 let value = serde_json::Value::deserialize(deserializer)?;
230 if value.get("type").is_some() {
231 Self::deserialize(value).map_err(D::Error::custom)
232 } else {
233 <Rgba as Deserialize>::deserialize(value)
234 .map(|color| Self::Solid { color })
235 .map_err(D::Error::custom)
236 }
237 }
238}
239
240impl Serialize for Paint {
241 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
242 Self::serialize(self, serializer)
243 }
244}
245
246#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[serde(deny_unknown_fields)]
250pub struct GradientStop {
251 pub offset: f32,
253 pub color: Rgba,
255}
256
257#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
259#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
260#[serde(rename_all = "snake_case")]
261pub enum Spread {
262 #[default]
264 Pad,
265 Repeat,
267 Reflect,
269}
270
271impl Generator {
272 #[must_use]
274 pub fn is_valid(&self) -> bool {
275 self.validate().is_ok()
276 }
277
278 pub fn validate(&self) -> Result<(), GeneratorIssue> {
286 self.validate_canvas(None)
287 }
288
289 pub fn validate_on(&self, width: u32, height: u32) -> Result<(), GeneratorIssue> {
295 self.validate_canvas(Some((width, height)))
296 }
297
298 fn validate_canvas(&self, canvas: Option<(u32, u32)>) -> Result<(), GeneratorIssue> {
299 let valid = match self {
300 Self::Fill { paint } => paint.is_valid(),
301 Self::Shapes { items } => {
302 return crate::shapes::validate_shape_items(items).map_err(GeneratorIssue::Shape);
303 }
304 Self::Layout { root } => return crate::layout::validate_layout(root, canvas),
305 Self::Text { text, style } => {
306 !text.is_empty()
307 && text.len() <= MAX_TEXT_CONTENT_BYTES
308 && style.is_valid(MAX_TEXT_GENERATOR_FONT_SIZE)
309 }
310 };
311 valid.then_some(()).ok_or(GeneratorIssue::Invalid)
312 }
313}
314
315impl Paint {
316 pub(crate) fn is_valid(&self) -> bool {
317 match self {
318 Self::Solid { color } => color.is_valid(),
319 Self::LinearGradient {
320 start, end, stops, ..
321 } => finite(*start) && finite(*end) && start != end && valid_stops(stops),
322 Self::RadialGradient {
323 center,
324 radius,
325 stops,
326 ..
327 } => finite(*center) && radius.is_finite() && *radius > 0.0 && valid_stops(stops),
328 Self::ConicGradient {
329 center,
330 angle_degrees,
331 stops,
332 } => finite(*center) && angle_degrees.is_finite() && valid_stops(stops),
333 }
334 }
335}
336
337fn finite(value: Vec2) -> bool {
338 value.x.is_finite() && value.y.is_finite()
339}
340
341fn valid_stops(stops: &[GradientStop]) -> bool {
342 (2..=MAX_GRADIENT_STOPS).contains(&stops.len())
343 && stops
344 .iter()
345 .all(|stop| (0.0..=1.0).contains(&stop.offset) && stop.color.is_valid())
346 && stops
347 .windows(2)
348 .all(|pair| pair[0].offset <= pair[1].offset)
349}