map_engine/
colour.rs

1//! Types and deserializer for colours.
2use palette::{
3    encoding::{Linear, Srgb},
4    rgb::Rgb,
5    Alpha,
6};
7use serde::{
8    de::{self, SeqAccess, Unexpected, Visitor},
9    Deserialize, Deserializer, Serialize,
10};
11use std::{convert::TryFrom, fmt, num::ParseIntError};
12
13pub(crate) type RgbaComponents = (f64, f64, f64, f64);
14type HexString = String;
15
16/// Colour representations.
17///
18/// We support different ways of creating colours:
19/// ```
20/// use map_engine::colour::Colour;
21/// use std::convert::TryFrom;
22/// # use map_engine::errors::MapEngineError;
23///
24/// # fn main() -> Result<(), MapEngineError> {
25/// // Using components in the range 0.0..=1.0
26/// Colour::Seq((1.0, 1.0, 1.0, 1.0));
27/// // Using components in the range 0..=255
28/// Colour::from((255, 255, 255, 255));
29/// // Using a hex string (we support multiple formats)
30/// Colour::try_from("FFFFFF")?; // We assume 100% opacity
31/// Colour::try_from("FFFFFFFF")?;
32/// Colour::try_from("#FFFFFFFF")?;
33/// Colour::try_from("#ffffffff")?;
34/// # Ok(())
35/// # }
36/// ```
37#[derive(Debug, Clone, PartialEq, Serialize)]
38#[serde(untagged)]
39pub enum Colour {
40    Seq(RgbaComponents),
41    /// Don't use this one directly. Prefer any of the `.from()` methods described above.
42    Hex(HexString),
43}
44
45impl<'de> Deserialize<'de> for Colour {
46    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
47    where
48        D: Deserializer<'de>,
49    {
50        struct ColourVisitor;
51        impl<'de> Visitor<'de> for ColourVisitor {
52            type Value = Colour;
53
54            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
55                write!(
56                    formatter,
57                    "4 values (r, g, b, a) in the range 0.0-1.0 or 0-255, or a hex colour"
58                )
59            }
60
61            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
62            where
63                E: de::Error,
64            {
65                match decode_hex(s) {
66                    Ok(c) => Ok(Colour::Seq(c)),
67                    Err(_) => Err(de::Error::invalid_value(Unexpected::Str(s), &self)),
68                }
69            }
70
71            fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error>
72            where
73                V: SeqAccess<'de>,
74            {
75                let r: f64 = seq
76                    .next_element()?
77                    .ok_or_else(|| de::Error::invalid_length(0, &self))?;
78                let g: f64 = seq
79                    .next_element()?
80                    .ok_or_else(|| de::Error::invalid_length(1, &self))?;
81                let b: f64 = seq
82                    .next_element()?
83                    .ok_or_else(|| de::Error::invalid_length(2, &self))?;
84                let a: f64 = seq
85                    .next_element()?
86                    .ok_or_else(|| de::Error::invalid_length(3, &self))?;
87
88                if (0f64..=1.0).contains(&r)
89                    && (0f64..=1.0).contains(&g)
90                    && (0f64..=1.0).contains(&b)
91                    && (0f64..=1.0).contains(&a)
92                {
93                    Ok(Colour::Seq((r, g, b, a)))
94                } else if (0f64..=255.0).contains(&r)
95                    && (0f64..=255.0).contains(&g)
96                    && (0f64..=255.0).contains(&b)
97                    && (0f64..=255.0).contains(&a)
98                {
99                    Ok(Colour::Seq((
100                        (r / 255.0),
101                        (g / 255.0),
102                        (b / 255.0),
103                        (a / 255.0),
104                    )))
105                } else {
106                    Err(de::Error::invalid_value(Unexpected::Seq, &self))
107                }
108            }
109        }
110        deserializer.deserialize_any(ColourVisitor)
111    }
112}
113
114fn decode_hex(s: &str) -> Result<RgbaComponents, ParseIntError> {
115    let s = s.trim_start_matches('#');
116    let mut v: Vec<u8> = (0..s.len())
117        .step_by(2)
118        .map(|i| u8::from_str_radix(&s[i..i + 2], 16))
119        .collect::<Result<_, _>>()?;
120    if v.len() == 3 {
121        v.extend_from_slice(&[255])
122    }
123    Ok((
124        v[0] as f64 / 255.0,
125        v[1] as f64 / 255.0,
126        v[2] as f64 / 255.0,
127        v[3] as f64 / 255.0,
128    ))
129}
130
131impl From<RgbaComponents> for Colour {
132    fn from(comp: RgbaComponents) -> Self {
133        Self::Seq(comp)
134    }
135}
136
137impl From<(u8, u8, u8, u8)> for Colour {
138    fn from(vals: (u8, u8, u8, u8)) -> Self {
139        Self::Seq((
140            vals.0 as f64 / 255.0,
141            vals.1 as f64 / 255.0,
142            vals.2 as f64 / 255.0,
143            vals.3 as f64 / 255.0,
144        ))
145    }
146}
147
148impl TryFrom<&str> for Colour {
149    type Error = ParseIntError;
150    fn try_from(s: &str) -> Result<Self, Self::Error> {
151        Ok(Self::Seq(decode_hex(s)?))
152    }
153}
154
155impl From<Colour> for RgbaComponents {
156    fn from(col: Colour) -> Self {
157        match col {
158            Colour::Seq(comp) => comp,
159            // Colour::Hex(s) => decode_hex(&s).expect("Cannot convert hex colour"),
160            // Is there any way of check this at compile time?
161            Colour::Hex(str) => panic!("Prefer Colour::try_from({:?})", str),
162        }
163    }
164}
165
166impl From<Alpha<Rgb<Linear<Srgb>, f64>, f64>> for Colour {
167    fn from(comp: Alpha<Rgb<Linear<Srgb>, f64>, f64>) -> Self {
168        Self::Seq(comp.into_components())
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use std::convert::TryInto;
176
177    #[test]
178    fn test_decode_hex() {
179        let expected_comp: RgbaComponents = (1., 0., 0., 1.);
180        assert_eq!(decode_hex("ff0000ff").unwrap(), expected_comp);
181        assert_eq!(decode_hex("FF0000FF").unwrap(), expected_comp);
182        assert_eq!(decode_hex("#ff0000ff").unwrap(), expected_comp);
183        // Assumes full opacity
184        assert_eq!(decode_hex("#ff0000").unwrap(), expected_comp);
185    }
186
187    #[test]
188    fn test_colour_from() {
189        assert_eq!(
190            Colour::try_from("ff0000ff").unwrap(),
191            "ff0000ff".try_into().unwrap()
192        );
193        assert_eq!(
194            Colour::try_from("ff0000ff").unwrap(),
195            (255, 0, 0, 255).into()
196        );
197        assert!(Colour::try_from("ff0000gg").is_err());
198    }
199
200    #[test]
201    fn test_colour_is_deserialized() {
202        let expected_col = Colour::Seq((1., 0., 0., 1.));
203        let expected_comp: RgbaComponents = (1., 0., 0., 1.);
204
205        let s = "\"ff0000ff\"";
206        let col: Colour = serde_json::from_str(s).unwrap();
207        assert_eq!(col, expected_col);
208        let comp: RgbaComponents = col.into();
209        assert_eq!(comp, expected_comp);
210
211        let s = r#"
212        [255, 0, 0, 255]
213        "#;
214        let col: Colour = serde_json::from_str(s).unwrap();
215        assert_eq!(col, expected_col);
216        let comp: RgbaComponents = col.into();
217        assert_eq!(comp, expected_comp);
218
219        let s = r#"
220        [1.0, 0.0, 0.0, 1.0]
221        "#;
222        let col: Colour = serde_json::from_str(s).unwrap();
223        assert_eq!(col, expected_col);
224        let comp: RgbaComponents = col.into();
225        assert_eq!(comp, expected_comp);
226    }
227
228    #[test]
229    #[should_panic]
230    fn test_hex_colour_panics() {
231        let _: RgbaComponents = Colour::Hex("ff0000ff".to_string()).into();
232    }
233
234    #[test]
235    fn test_colour_deserialized_fails() {
236        let s = "\"ff0000gg\"";
237        let col: Result<Colour, _> = serde_json::from_str(s);
238        let expected_msg =
239            "invalid value: string \"ff0000gg\", expected 4 values (r, g, b, a) in the range 0.0-1.0 or 0-255, or a hex colour at line 1 column 10";
240        if let Err(err) = col {
241            assert_eq!(format!("{}", err), expected_msg.to_string())
242        };
243    }
244}