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)]
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[cfg_attr(
19 feature = "schema",
20 schemars(rename = "Rgba", transform = crate::schema::with_hex_form)
21)]
22#[serde(remote = "Self", deny_unknown_fields)]
23pub struct Rgba {
24 pub red: f32,
26 pub green: f32,
28 pub blue: f32,
30 pub alpha: f32,
32}
33
34impl Rgba {
35 #[must_use]
37 pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
38 Self {
39 red,
40 green,
41 blue,
42 alpha,
43 }
44 }
45
46 pub(crate) fn is_valid(self) -> bool {
47 [self.red, self.green, self.blue, self.alpha]
48 .iter()
49 .all(|value| (0.0..=1.0).contains(value))
50 }
51
52 #[must_use]
54 pub fn from_hex(text: &str) -> Option<Self> {
55 let digits = text.strip_prefix('#')?.as_bytes();
56 let nibble = |index: usize| char::from(digits[index]).to_digit(16);
57 let component = |value: u32| u8::try_from(value).ok().map(|byte| f32::from(byte) / 255.0);
58 let channels = match digits.len() {
59 3 | 4 => (0..digits.len())
60 .map(|index| component(nibble(index)? * 17))
61 .collect::<Option<Vec<_>>>()?,
62 6 | 8 => (0..digits.len() / 2)
63 .map(|index| component(nibble(index * 2)? * 16 + nibble(index * 2 + 1)?))
64 .collect::<Option<Vec<_>>>()?,
65 _ => return None,
66 };
67 Some(Self::new(
68 channels[0],
69 channels[1],
70 channels[2],
71 channels.get(3).copied().unwrap_or(1.0),
72 ))
73 }
74}
75
76struct RgbaVisitor;
78
79impl<'de> serde::de::Visitor<'de> for RgbaVisitor {
80 type Value = Rgba;
81
82 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 formatter.write_str("a color: the {red, green, blue, alpha} object or a CSS hex string")
84 }
85
86 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Rgba, E> {
87 Rgba::from_hex(value).ok_or_else(|| {
88 E::custom(format!(
89 "invalid color \"{value}\": write #rgb, #rgba, #rrggbb, or #rrggbbaa"
90 ))
91 })
92 }
93
94 fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Rgba, A::Error> {
95 Rgba::deserialize(serde::de::value::MapAccessDeserializer::new(map))
96 }
97}
98
99impl<'de> Deserialize<'de> for Rgba {
100 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101 deserializer.deserialize_any(RgbaVisitor)
102 }
103}
104
105impl Serialize for Rgba {
106 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
107 Self::serialize(self, serializer)
108 }
109}
110
111#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
118#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
119#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
120pub enum Generator {
121 Fill {
123 paint: Paint,
125 },
126 Shapes {
129 items: Vec<ShapeItem>,
131 },
132 Text {
135 text: crate::TextContent,
138 #[serde(default)]
141 style: TextStyle,
142 },
143 Layout {
146 root: Box<LayoutNode>,
148 },
149}
150
151#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
157#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
158#[cfg_attr(
159 feature = "schema",
160 schemars(rename = "Paint", transform = crate::schema::with_color_form)
161)]
162#[serde(
163 remote = "Self",
164 tag = "type",
165 rename_all = "snake_case",
166 deny_unknown_fields
167)]
168pub enum Paint {
169 Solid {
171 color: Rgba,
173 },
174 LinearGradient {
176 start: Vec2,
178 end: Vec2,
180 stops: Vec<GradientStop>,
182 #[serde(default)]
184 spread: Spread,
185 },
186 RadialGradient {
188 center: Vec2,
190 radius: f32,
193 stops: Vec<GradientStop>,
195 #[serde(default)]
197 spread: Spread,
198 },
199 ConicGradient {
201 center: Vec2,
203 #[serde(default)]
205 angle_degrees: f32,
206 stops: Vec<GradientStop>,
208 },
209}
210
211impl<'de> Deserialize<'de> for Paint {
212 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
213 use serde::de::Error as _;
214 let value = serde_json::Value::deserialize(deserializer)?;
216 if value.get("type").is_some() {
217 Self::deserialize(value).map_err(D::Error::custom)
218 } else {
219 <Rgba as Deserialize>::deserialize(value)
220 .map(|color| Self::Solid { color })
221 .map_err(D::Error::custom)
222 }
223 }
224}
225
226impl Serialize for Paint {
227 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
228 Self::serialize(self, serializer)
229 }
230}
231
232#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
234#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
235#[serde(deny_unknown_fields)]
236pub struct GradientStop {
237 pub offset: f32,
239 pub color: Rgba,
241}
242
243#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
245#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
246#[serde(rename_all = "snake_case")]
247pub enum Spread {
248 #[default]
250 Pad,
251 Repeat,
253 Reflect,
255}
256
257impl Generator {
258 #[must_use]
260 pub fn is_valid(&self) -> bool {
261 self.validate().is_ok()
262 }
263
264 pub fn validate(&self) -> Result<(), GeneratorIssue> {
271 self.validate_canvas(None)
272 }
273
274 pub fn validate_on(&self, width: u32, height: u32) -> Result<(), GeneratorIssue> {
280 self.validate_canvas(Some((width, height)))
281 }
282
283 fn validate_canvas(&self, canvas: Option<(u32, u32)>) -> Result<(), GeneratorIssue> {
284 let valid = match self {
285 Self::Fill { paint } => paint.is_valid(),
286 Self::Shapes { items } => {
287 return crate::shapes::validate_shape_items(items).map_err(GeneratorIssue::Shape);
288 }
289 Self::Layout { root } => return crate::layout::validate_layout(root, canvas),
290 Self::Text { text, style } => {
291 text.is_valid(MAX_TEXT_CONTENT_BYTES)
292 && style.is_valid(MAX_TEXT_GENERATOR_FONT_SIZE)
293 }
294 };
295 valid.then_some(()).ok_or(GeneratorIssue::Invalid)
296 }
297}
298
299impl Paint {
300 pub(crate) fn is_valid(&self) -> bool {
301 match self {
302 Self::Solid { color } => color.is_valid(),
303 Self::LinearGradient {
304 start, end, stops, ..
305 } => finite(*start) && finite(*end) && start != end && valid_stops(stops),
306 Self::RadialGradient {
307 center,
308 radius,
309 stops,
310 ..
311 } => finite(*center) && radius.is_finite() && *radius > 0.0 && valid_stops(stops),
312 Self::ConicGradient {
313 center,
314 angle_degrees,
315 stops,
316 } => finite(*center) && angle_degrees.is_finite() && valid_stops(stops),
317 }
318 }
319}
320
321fn finite(value: Vec2) -> bool {
322 value.x.is_finite() && value.y.is_finite()
323}
324
325fn valid_stops(stops: &[GradientStop]) -> bool {
326 (2..=MAX_GRADIENT_STOPS).contains(&stops.len())
327 && stops
328 .iter()
329 .all(|stop| (0.0..=1.0).contains(&stop.offset) && stop.color.is_valid())
330 && stops
331 .windows(2)
332 .all(|pair| pair[0].offset <= pair[1].offset)
333}