use crate::{
GeneratorIssue, LayoutNode, MAX_TEXT_CONTENT_BYTES, MAX_TEXT_GENERATOR_FONT_SIZE, ShapeItem,
TextStyle, Vec2,
};
use serde::{Deserialize, Serialize};
pub const MAX_GRADIENT_STOPS: usize = 8;
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schema",
schemars(rename = "Rgba", transform = crate::schema::with_hex_form)
)]
#[serde(remote = "Self", deny_unknown_fields)]
pub struct Rgba {
pub red: f32,
pub green: f32,
pub blue: f32,
pub alpha: f32,
}
impl Rgba {
#[must_use]
pub const fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
pub(crate) fn is_valid(self) -> bool {
[self.red, self.green, self.blue, self.alpha]
.iter()
.all(|value| (0.0..=1.0).contains(value))
}
#[must_use]
pub fn from_hex(text: &str) -> Option<Self> {
let digits = text.strip_prefix('#')?.as_bytes();
let nibble = |index: usize| char::from(digits[index]).to_digit(16);
let component = |value: u32| u8::try_from(value).ok().map(|byte| f32::from(byte) / 255.0);
let channels = match digits.len() {
3 | 4 => (0..digits.len())
.map(|index| component(nibble(index)? * 17))
.collect::<Option<Vec<_>>>()?,
6 | 8 => (0..digits.len() / 2)
.map(|index| component(nibble(index * 2)? * 16 + nibble(index * 2 + 1)?))
.collect::<Option<Vec<_>>>()?,
_ => return None,
};
Some(Self::new(
channels[0],
channels[1],
channels[2],
channels.get(3).copied().unwrap_or(1.0),
))
}
}
struct RgbaVisitor;
impl<'de> serde::de::Visitor<'de> for RgbaVisitor {
type Value = Rgba;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a color: the {red, green, blue, alpha} object or a CSS hex string")
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Rgba, E> {
Rgba::from_hex(value).ok_or_else(|| {
E::custom(format!(
"invalid color \"{value}\": write #rgb, #rgba, #rrggbb, or #rrggbbaa"
))
})
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, map: A) -> Result<Rgba, A::Error> {
Rgba::deserialize(serde::de::value::MapAccessDeserializer::new(map))
}
}
impl<'de> Deserialize<'de> for Rgba {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(RgbaVisitor)
}
}
impl Serialize for Rgba {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Self::serialize(self, serializer)
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case", deny_unknown_fields)]
pub enum Generator {
Fill {
paint: Paint,
},
Shapes {
items: Vec<ShapeItem>,
},
Text {
text: String,
#[serde(default)]
style: TextStyle,
},
Layout {
root: Box<LayoutNode>,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "schema",
schemars(rename = "Paint", transform = crate::schema::with_color_form)
)]
#[serde(
remote = "Self",
tag = "type",
rename_all = "snake_case",
deny_unknown_fields
)]
pub enum Paint {
Solid {
color: Rgba,
},
LinearGradient {
start: Vec2,
end: Vec2,
stops: Vec<GradientStop>,
#[serde(default)]
spread: Spread,
},
RadialGradient {
center: Vec2,
radius: f32,
stops: Vec<GradientStop>,
#[serde(default)]
spread: Spread,
},
ConicGradient {
center: Vec2,
#[serde(default)]
angle_degrees: f32,
stops: Vec<GradientStop>,
},
}
impl<'de> Deserialize<'de> for Paint {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error as _;
let value = serde_json::Value::deserialize(deserializer)?;
if value.get("type").is_some() {
Self::deserialize(value).map_err(D::Error::custom)
} else {
<Rgba as Deserialize>::deserialize(value)
.map(|color| Self::Solid { color })
.map_err(D::Error::custom)
}
}
}
impl Serialize for Paint {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Self::serialize(self, serializer)
}
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GradientStop {
pub offset: f32,
pub color: Rgba,
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum Spread {
#[default]
Pad,
Repeat,
Reflect,
}
impl Generator {
#[must_use]
pub fn is_valid(&self) -> bool {
self.validate().is_ok()
}
pub fn validate(&self) -> Result<(), GeneratorIssue> {
self.validate_canvas(None)
}
pub fn validate_on(&self, width: u32, height: u32) -> Result<(), GeneratorIssue> {
self.validate_canvas(Some((width, height)))
}
fn validate_canvas(&self, canvas: Option<(u32, u32)>) -> Result<(), GeneratorIssue> {
let valid = match self {
Self::Fill { paint } => paint.is_valid(),
Self::Shapes { items } => {
return crate::shapes::validate_shape_items(items).map_err(GeneratorIssue::Shape);
}
Self::Layout { root } => return crate::layout::validate_layout(root, canvas),
Self::Text { text, style } => {
!text.is_empty()
&& text.len() <= MAX_TEXT_CONTENT_BYTES
&& style.is_valid(MAX_TEXT_GENERATOR_FONT_SIZE)
}
};
valid.then_some(()).ok_or(GeneratorIssue::Invalid)
}
}
impl Paint {
pub(crate) fn is_valid(&self) -> bool {
match self {
Self::Solid { color } => color.is_valid(),
Self::LinearGradient {
start, end, stops, ..
} => finite(*start) && finite(*end) && start != end && valid_stops(stops),
Self::RadialGradient {
center,
radius,
stops,
..
} => finite(*center) && radius.is_finite() && *radius > 0.0 && valid_stops(stops),
Self::ConicGradient {
center,
angle_degrees,
stops,
} => finite(*center) && angle_degrees.is_finite() && valid_stops(stops),
}
}
}
fn finite(value: Vec2) -> bool {
value.x.is_finite() && value.y.is_finite()
}
fn valid_stops(stops: &[GradientStop]) -> bool {
(2..=MAX_GRADIENT_STOPS).contains(&stops.len())
&& stops
.iter()
.all(|stop| (0.0..=1.0).contains(&stop.offset) && stop.color.is_valid())
&& stops
.windows(2)
.all(|pair| pair[0].offset <= pair[1].offset)
}