1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
use std::str::FromStr;

use serde::de::Deserializer;
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};

#[cfg(feature = "druid")]
use druid::Data;

pub static PUBLIC_OBJECT_LIBS_KEY: &str = "public.objectLibs";

/// A Plist dictionary.
pub type Plist = plist::Dictionary;

/// A color in RGBA (Red-Green-Blue-Alpha) format.
///
/// See <https://unifiedfontobject.org/versions/ufo3/conventions/#colors>.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "druid", derive(Data))]
pub struct Color {
    /// Red channel value. Must be in the range 0 to 1, inclusive.
    red: f64,
    /// Green channel value. Must be in the range 0 to 1, inclusive.
    green: f64,
    /// Blue channel value. Must be in the range 0 to 1, inclusive.
    blue: f64,
    /// Alpha (transparency) channel value. Must be in the range 0 to 1, inclusive.
    alpha: f64,
}

impl Color {
    /// Create a color with RGBA values in the range `0..=1.0`.
    ///
    /// Returns an error if any of the provided values are not in the allowed range.
    pub fn new(red: f64, green: f64, blue: f64, alpha: f64) -> Result<Self, ColorError> {
        if [red, green, blue, alpha].iter().all(|v| (0.0..=1.0).contains(v)) {
            Ok(Self { red, green, blue, alpha })
        } else {
            Err(ColorError::Value)
        }
    }

    /// Returns the RGBA channel values.
    pub fn channels(&self) -> (f64, f64, f64, f64) {
        (self.red, self.green, self.blue, self.alpha)
    }
}

/// An error representing an invalid [`Color`] string.
///
/// [`Color`]: crate::Color
#[derive(Debug, thiserror::Error)]
pub enum ColorError {
    /// The color string was malformed.
    #[error("failed to parse color string '{0}'")]
    Parse(String),
    /// A channel value was not between 0 and 1, inclusive.
    #[error("color channel values must be between 0 and 1, inclusive")]
    Value,
}

impl FromStr for Color {
    type Err = ColorError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let mut iter =
            s.split(',').map(|v| v.parse::<f64>().map_err(|_| ColorError::Parse(s.to_owned())));
        let red = iter.next().unwrap_or_else(|| Err(ColorError::Parse(s.to_owned())))?;
        let green = iter.next().unwrap_or_else(|| Err(ColorError::Parse(s.to_owned())))?;
        let blue = iter.next().unwrap_or_else(|| Err(ColorError::Parse(s.to_owned())))?;
        let alpha = iter.next().unwrap_or_else(|| Err(ColorError::Parse(s.to_owned())))?;
        if iter.next().is_some() {
            Err(ColorError::Parse(s.to_owned()))
        } else {
            Color::new(red, green, blue, alpha)
        }
    }
}

impl Serialize for Color {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let color_string = self.to_rgba_string();
        serializer.serialize_str(&color_string)
    }
}

impl<'de> Deserialize<'de> for Color {
    fn deserialize<D>(deserializer: D) -> Result<Color, D::Error>
    where
        D: Deserializer<'de>,
    {
        let string = String::deserialize(deserializer)?;
        Color::from_str(&string).map_err(serde::de::Error::custom)
    }
}

#[cfg(test)]
mod tests {
    use serde_test::{assert_de_tokens, assert_ser_tokens, assert_tokens, Token};

    use super::*;

    #[test]
    fn color_parsing() {
        let c1 = Color { red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0 };
        assert_tokens(&c1, &[Token::Str("1,0,0,1")]);

        let c2 = Color { red: 0.0, green: 0.5, blue: 0.0, alpha: 0.5 };
        assert_tokens(&c2, &[Token::Str("0,0.5,0,0.5")]);

        let c3 = Color { red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0 };
        assert_tokens(&c3, &[Token::Str("0,0,0,0")]);

        let c4 = Color { red: 0.123, green: 0.456, blue: 0.789, alpha: 0.159 };
        assert_tokens(&c4, &[Token::Str("0.123,0.456,0.789,0.159")]);

        #[allow(clippy::excessive_precision)]
        let c5 = Color { red: 0.123456789, green: 0.456789123, blue: 0.789123456, alpha: 0.1 };
        assert_ser_tokens(&c5, &[Token::Str("0.123,0.457,0.789,0.1")]);

        #[allow(clippy::excessive_precision)]
        let c6 = Color { red: 0.123456789, green: 0.456789123, blue: 0.789123456, alpha: 0.1 };
        assert_de_tokens(&c6, &[Token::Str("0.123456789,0.456789123,0.789123456,0.1")]);
    }
}