piet/
color.rs

1// Copyright 2019 the Piet Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! A simple representation of color
5
6use std::fmt::{Debug, Formatter};
7
8/// A datatype representing color.
9///
10/// Currently this is only a 32 bit RGBA value, but it will likely
11/// extend to some form of wide-gamut colorspace, and in the meantime
12/// is useful for giving programs proper type.
13#[derive(Clone, Copy, PartialEq, Eq, Hash)]
14#[non_exhaustive]
15pub enum Color {
16    #[doc(hidden)]
17    Rgba32(u32),
18}
19
20/// Errors that can occur when parsing a hex color.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum ColorParseError {
23    /// The input string has an incorrect length
24    WrongSize(usize),
25    /// A byte in the input string is not in one of the ranges `0..=9`,
26    /// `a..=f`, or `A..=F`.
27    #[allow(missing_docs)]
28    NotHex { idx: usize, byte: u8 },
29}
30
31impl Color {
32    /// Create a color from 8 bit per sample RGB values.
33    pub const fn rgb8(r: u8, g: u8, b: u8) -> Color {
34        Color::from_rgba32_u32(((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | 0xff)
35    }
36
37    /// Create a color from 8 bit per sample RGBA values.
38    pub const fn rgba8(r: u8, g: u8, b: u8, a: u8) -> Color {
39        Color::from_rgba32_u32(
40            ((r as u32) << 24) | ((g as u32) << 16) | ((b as u32) << 8) | (a as u32),
41        )
42    }
43
44    /// Create a color from a 32-bit rgba value (alpha as least significant byte).
45    pub const fn from_rgba32_u32(rgba: u32) -> Color {
46        Color::Rgba32(rgba)
47    }
48
49    /// Attempt to create a color from a CSS-style hex string.
50    ///
51    /// This will accept strings in the following formats, *with or without*
52    /// the leading `#`:
53    ///
54    /// - `rrggbb`
55    /// - `rrggbbaa`
56    /// - `rbg`
57    /// - `rbga`
58    ///
59    /// This method returns a [`ColorParseError`] if the color cannot be parsed.
60    pub const fn from_hex_str(hex: &str) -> Result<Color, ColorParseError> {
61        // can't use `map()` in a const function
62        match get_4bit_hex_channels(hex) {
63            Ok(channels) => Ok(color_from_4bit_hex(channels)),
64            Err(e) => Err(e),
65        }
66    }
67
68    /// Create a color from a grey value.
69    ///
70    /// ```
71    /// use piet::Color;
72    ///
73    /// let grey_val = 0x55;
74    ///
75    /// let one = Color::grey8(grey_val);
76    /// // is shorthand for
77    /// let two = Color::rgb8(grey_val, grey_val, grey_val);
78    ///
79    /// assert_eq!(one.as_rgba_u32(), two.as_rgba_u32());
80    /// ```
81    pub const fn grey8(grey: u8) -> Color {
82        Color::rgb8(grey, grey, grey)
83    }
84
85    /// Create a color with a grey value in the range 0.0..=1.0.
86    pub fn grey(grey: f64) -> Color {
87        Color::rgb(grey, grey, grey)
88    }
89
90    /// Create a color from four floating point values, each in the range 0.0 to 1.0.
91    ///
92    /// The interpretation is the same as rgba32, and no greater precision is
93    /// (currently) assumed.
94    pub fn rgba(r: f64, g: f64, b: f64, a: f64) -> Color {
95        let r = (r.clamp(0.0, 1.0) * 255.0).round() as u32;
96        let g = (g.clamp(0.0, 1.0) * 255.0).round() as u32;
97        let b = (b.clamp(0.0, 1.0) * 255.0).round() as u32;
98        let a = (a.clamp(0.0, 1.0) * 255.0).round() as u32;
99        Color::from_rgba32_u32((r << 24) | (g << 16) | (b << 8) | a)
100    }
101
102    /// Create a color from three floating point values, each in the range 0.0 to 1.0.
103    ///
104    /// The interpretation is the same as rgb8, and no greater precision is
105    /// (currently) assumed.
106    pub fn rgb(r: f64, g: f64, b: f64) -> Color {
107        let r = (r.clamp(0.0, 1.0) * 255.0).round() as u32;
108        let g = (g.clamp(0.0, 1.0) * 255.0).round() as u32;
109        let b = (b.clamp(0.0, 1.0) * 255.0).round() as u32;
110        Color::from_rgba32_u32((r << 24) | (g << 16) | (b << 8) | 0xff)
111    }
112
113    /// Create a color from a CIEL\*a\*b\* polar (also known as CIE HCL)
114    /// specification.
115    ///
116    /// The `h` parameter is an angle in degrees, with 0 roughly magenta, 90
117    /// roughly yellow, 180 roughly cyan, and 270 roughly blue. The `l`
118    /// parameter is perceptual luminance, with 0 black and 100 white.
119    /// The `c` parameter is a chrominance concentration, with 0 grayscale
120    /// and a nominal maximum of 127 (in the future, higher values might
121    /// be useful, for high gamut contexts).
122    ///
123    /// Currently this is just converted into sRGB, but in the future as we
124    /// support high-gamut colorspaces, it can be used to specify more colors
125    /// or existing colors with a higher accuracy.
126    ///
127    /// Currently out-of-gamut values are clipped to the nearest sRGB color,
128    /// which is perhaps not ideal (the clipping might change the hue). See
129    /// <https://github.com/d3/d3-color/issues/33> for discussion.
130    #[allow(non_snake_case)]
131    #[allow(clippy::many_single_char_names)]
132    #[allow(clippy::unreadable_literal)]
133    pub fn hlc(h: f64, L: f64, c: f64) -> Color {
134        // The reverse transformation from Lab to XYZ, see
135        // https://en.wikipedia.org/wiki/CIELAB_color_space
136        fn f_inv(t: f64) -> f64 {
137            let d = 6. / 29.;
138            if t > d {
139                t.powi(3)
140            } else {
141                3. * d * d * (t - 4. / 29.)
142            }
143        }
144        let th = h * (std::f64::consts::PI / 180.);
145        let a = c * th.cos();
146        let b = c * th.sin();
147        let ll = (L + 16.) * (1. / 116.);
148        // Produce raw XYZ values
149        let X = f_inv(ll + a * (1. / 500.));
150        let Y = f_inv(ll);
151        let Z = f_inv(ll - b * (1. / 200.));
152        // This matrix is the concatenation of three sources.
153        // First, the white point is taken to be ICC standard D50, so
154        // the diagonal matrix of [0.9642, 1, 0.8249]. Note that there
155        // is some controversy around this value. However, it matches
156        // the other matrices, thus minimizing chroma error.
157        //
158        // Second, an adaption matrix from D50 to D65. This is the
159        // inverse of the recommended D50 to D65 adaptation matrix
160        // from the W3C sRGB spec:
161        // https://www.w3.org/Graphics/Color/srgb
162        //
163        // Finally, the conversion from XYZ to linear sRGB values,
164        // also taken from the W3C sRGB spec.
165        let r_lin = 3.02172918 * X - 1.61692294 * Y - 0.40480625 * Z;
166        let g_lin = -0.94339358 * X + 1.91584267 * Y + 0.02755094 * Z;
167        let b_lin = 0.06945666 * X - 0.22903204 * Y + 1.15957526 * Z;
168        fn gamma(u: f64) -> f64 {
169            if u <= 0.0031308 {
170                12.92 * u
171            } else {
172                1.055 * u.powf(1. / 2.4) - 0.055
173            }
174        }
175        Color::rgb(gamma(r_lin), gamma(g_lin), gamma(b_lin))
176    }
177
178    /// Create a color from a CIEL\*a\*b\* polar specification and alpha.
179    ///
180    /// The `a` value represents alpha in the range 0.0 to 1.0.
181    pub fn hlca(h: f64, l: f64, c: f64, a: f64) -> Color {
182        Color::hlc(h, c, l).with_alpha(a)
183    }
184
185    /// Change just the alpha value of a color.
186    ///
187    /// The `a` value represents alpha in the range 0.0 to 1.0.
188    pub fn with_alpha(self, a: f64) -> Color {
189        let a = (a.clamp(0.0, 1.0) * 255.0).round() as u32;
190        Color::from_rgba32_u32((self.as_rgba_u32() & !0xff) | a)
191    }
192
193    /// Change just the red value of a color.
194    ///
195    /// The `r` value represents red as a `u8` from 0 to 255.
196    pub const fn with_r8(self, r: u8) -> Color {
197        Color::from_rgba32_u32((self.as_rgba_u32() & !0xff000000) | (r as u32) << 24)
198    }
199
200    /// Change just the green value of a color.
201    ///
202    /// The `g` value represents green as a `u8` from 0 to 255.
203    pub const fn with_g8(self, g: u8) -> Color {
204        Color::from_rgba32_u32((self.as_rgba_u32() & !0xff0000) | (g as u32) << 16)
205    }
206
207    /// Change just the blue value of a color.
208    ///
209    /// The `b` value represents blue as a `u8` from 0 to 255.
210    pub const fn with_b8(self, b: u8) -> Color {
211        Color::from_rgba32_u32((self.as_rgba_u32() & !0xff00) | (b as u32) << 8)
212    }
213
214    /// Change just the alpha value of a color.
215    ///
216    /// The `a` value represents alpha as a `u8` from 0 to 255.
217    pub const fn with_a8(self, a: u8) -> Color {
218        Color::from_rgba32_u32((self.as_rgba_u32() & !0xff) | a as u32)
219    }
220
221    /// Convert a color value to a 32-bit rgba value.
222    pub const fn as_rgba_u32(self) -> u32 {
223        match self {
224            Color::Rgba32(rgba) => rgba,
225        }
226    }
227
228    /// Convert a color value to four 8-bit rgba values.
229    pub fn as_rgba8(self) -> (u8, u8, u8, u8) {
230        let rgba = self.as_rgba_u32();
231        (
232            (rgba >> 24 & 255) as u8,
233            ((rgba >> 16) & 255) as u8,
234            ((rgba >> 8) & 255) as u8,
235            (rgba & 255) as u8,
236        )
237    }
238
239    /// Convert a color value to four f64 values, each in the range 0.0 to 1.0.
240    pub fn as_rgba(self) -> (f64, f64, f64, f64) {
241        let rgba = self.as_rgba_u32();
242        (
243            (rgba >> 24) as f64 / 255.0,
244            ((rgba >> 16) & 255) as f64 / 255.0,
245            ((rgba >> 8) & 255) as f64 / 255.0,
246            (rgba & 255) as f64 / 255.0,
247        )
248    }
249
250    // basic css3 colors (not including shades for now)
251
252    /// Opaque aqua (or cyan).
253    pub const AQUA: Color = Color::rgb8(0, 255, 255);
254
255    /// Opaque black.
256    pub const BLACK: Color = Color::rgb8(0, 0, 0);
257
258    /// Opaque blue.
259    pub const BLUE: Color = Color::rgb8(0, 0, 255);
260
261    /// Opaque fuchsia (or magenta).
262    pub const FUCHSIA: Color = Color::rgb8(255, 0, 255);
263
264    /// Opaque gray.
265    pub const GRAY: Color = Color::grey8(128);
266
267    /// Opaque green.
268    pub const GREEN: Color = Color::rgb8(0, 128, 0);
269
270    /// Opaque lime.
271    pub const LIME: Color = Color::rgb8(0, 255, 0);
272
273    /// Opaque maroon.
274    pub const MAROON: Color = Color::rgb8(128, 0, 0);
275
276    /// Opaque navy.
277    pub const NAVY: Color = Color::rgb8(0, 0, 128);
278
279    /// Opaque olive.
280    pub const OLIVE: Color = Color::rgb8(128, 128, 0);
281
282    /// Opaque purple.
283    pub const PURPLE: Color = Color::rgb8(128, 0, 128);
284
285    /// Opaque red.
286    pub const RED: Color = Color::rgb8(255, 0, 0);
287
288    /// Opaque silver.
289    pub const SILVER: Color = Color::grey8(192);
290
291    /// Opaque teal.
292    pub const TEAL: Color = Color::rgb8(0, 128, 128);
293
294    /// Fully transparent
295    pub const TRANSPARENT: Color = Color::rgba8(0, 0, 0, 0);
296
297    /// Opaque white.
298    pub const WHITE: Color = Color::grey8(255);
299
300    /// Opaque yellow.
301    pub const YELLOW: Color = Color::rgb8(255, 255, 0);
302}
303
304const fn get_4bit_hex_channels(hex_str: &str) -> Result<[u8; 8], ColorParseError> {
305    let mut four_bit_channels = match hex_str.as_bytes() {
306        &[b'#', r, g, b] | &[r, g, b] => [r, r, g, g, b, b, b'f', b'f'],
307        &[b'#', r, g, b, a] | &[r, g, b, a] => [r, r, g, g, b, b, a, a],
308        &[b'#', r0, r1, g0, g1, b0, b1] | &[r0, r1, g0, g1, b0, b1] => {
309            [r0, r1, g0, g1, b0, b1, b'f', b'f']
310        }
311        &[b'#', r0, r1, g0, g1, b0, b1, a0, a1] | &[r0, r1, g0, g1, b0, b1, a0, a1] => {
312            [r0, r1, g0, g1, b0, b1, a0, a1]
313        }
314        other => return Err(ColorParseError::WrongSize(other.len())),
315    };
316
317    // convert to hex in-place
318    // this is written without a for loop to satisfy `const`
319    let mut i = 0;
320    while i < four_bit_channels.len() {
321        let ascii = four_bit_channels[i];
322        let as_hex = match hex_from_ascii_byte(ascii) {
323            Ok(hex) => hex,
324            Err(byte) => return Err(ColorParseError::NotHex { idx: i, byte }),
325        };
326        four_bit_channels[i] = as_hex;
327        i += 1;
328    }
329    Ok(four_bit_channels)
330}
331
332const fn color_from_4bit_hex(components: [u8; 8]) -> Color {
333    let [r0, r1, g0, g1, b0, b1, a0, a1] = components;
334    Color::rgba8(r0 << 4 | r1, g0 << 4 | g1, b0 << 4 | b1, a0 << 4 | a1)
335}
336
337const fn hex_from_ascii_byte(b: u8) -> Result<u8, u8> {
338    match b {
339        b'0'..=b'9' => Ok(b - b'0'),
340        b'A'..=b'F' => Ok(b - b'A' + 10),
341        b'a'..=b'f' => Ok(b - b'a' + 10),
342        _ => Err(b),
343    }
344}
345
346impl Debug for Color {
347    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
348        write!(f, "#{:08x}", self.as_rgba_u32())
349    }
350}
351
352impl std::fmt::Display for ColorParseError {
353    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
354        match self {
355            ColorParseError::WrongSize(n) => write!(f, "Input string has invalid length {n}"),
356            ColorParseError::NotHex { idx, byte } => {
357                write!(f, "byte {byte:X} at index {idx} is not valid hex digit")
358            }
359        }
360    }
361}
362
363impl std::error::Error for ColorParseError {}
364#[cfg(test)]
365mod tests {
366    use super::*;
367    #[test]
368    fn color_from_hex() {
369        assert_eq!(Color::from_hex_str("#BAD"), Color::from_hex_str("BBAADD"));
370        assert_eq!(
371            Color::from_hex_str("#BAD"),
372            Ok(Color::from_rgba32_u32(0xBBAADDFF))
373        );
374        assert_eq!(Color::from_hex_str("BAD"), Color::from_hex_str("BBAADD"));
375        assert_eq!(Color::from_hex_str("#BADF"), Color::from_hex_str("BAD"));
376        assert_eq!(Color::from_hex_str("#BBAADDFF"), Color::from_hex_str("BAD"));
377        assert_eq!(Color::from_hex_str("BBAADDFF"), Color::from_hex_str("BAD"));
378        assert_eq!(Color::from_hex_str("bBAadDfF"), Color::from_hex_str("BAD"));
379        assert_eq!(Color::from_hex_str("#0f6"), Ok(Color::rgb8(0, 0xff, 0x66)));
380        assert_eq!(
381            Color::from_hex_str("#0f6a"),
382            Ok(Color::rgba8(0, 0xff, 0x66, 0xaa))
383        );
384        assert!(Color::from_hex_str("#0f6aa").is_err());
385        assert!(Color::from_hex_str("#0f").is_err());
386        assert!(Color::from_hex_str("x0f").is_err());
387        assert!(Color::from_hex_str("#0afa1").is_err());
388    }
389
390    #[test]
391    fn change_subcolor_values() {
392        let color = Color::from_rgba32_u32(0x11aa22bb);
393
394        assert_eq!(color.with_r8(0xff), Color::from_rgba32_u32(0xffaa22bb));
395        assert_eq!(color.with_g8(0xff), Color::from_rgba32_u32(0x11ff22bb));
396        assert_eq!(color.with_b8(0xff), Color::from_rgba32_u32(0x11aaffbb));
397        assert_eq!(color.with_a8(0xff), Color::from_rgba32_u32(0x11aa22ff));
398    }
399}