rsvg/
color.rs

1//! CSS color values.
2
3use cssparser::{ParseErrorKind, Parser};
4use cssparser_color as cssc;
5use cssparser_color::{hsl_to_rgb, hwb_to_rgb};
6
7use crate::error::*;
8use crate::parsers::Parse;
9use crate::unit_interval::UnitInterval;
10use crate::util;
11
12/// Subset of <https://drafts.csswg.org/css-color-4/#color-type>
13#[derive(Clone, Copy, PartialEq, Debug)]
14pub enum Color {
15    /// The 'currentcolor' keyword.
16    CurrentColor,
17    /// Specify sRGB colors directly by their red/green/blue/alpha chanels.
18    Rgba(RGBA),
19    /// Specifies a color in sRGB using hue, saturation and lightness components.
20    Hsl(Hsl),
21    /// Specifies a color in sRGB using hue, whiteness and blackness components.
22    Hwb(Hwb),
23}
24
25/// A color with red, green, blue, and alpha components, in a byte each.
26#[allow(clippy::upper_case_acronyms)]
27#[derive(Clone, Copy, PartialEq, Debug)]
28pub struct RGBA {
29    /// The red component.
30    pub red: u8,
31    /// The green component.
32    pub green: u8,
33    /// The blue component.
34    pub blue: u8,
35    /// The alpha component.
36    pub alpha: f32,
37}
38
39/// Color specified by hue, saturation and lightness components.
40#[derive(Clone, Copy, PartialEq, Debug)]
41pub struct Hsl {
42    /// The hue component.
43    pub hue: Option<f32>,
44    /// The saturation component.
45    pub saturation: Option<f32>,
46    /// The lightness component.
47    pub lightness: Option<f32>,
48    /// The alpha component.
49    pub alpha: Option<f32>,
50}
51
52/// Color specified by hue, whiteness and blackness components.
53#[derive(Clone, Copy, PartialEq, Debug)]
54pub struct Hwb {
55    /// The hue component.
56    pub hue: Option<f32>,
57    /// The whiteness component.
58    pub whiteness: Option<f32>,
59    /// The blackness component.
60    pub blackness: Option<f32>,
61    /// The alpha component.
62    pub alpha: Option<f32>,
63}
64
65const OPAQUE: f32 = 1.0;
66
67impl RGBA {
68    /// Constructs a new RGBA value from float components. It expects the red,
69    /// green, blue and alpha channels in that order, and all values will be
70    /// clamped to the 0.0 ... 1.0 range.
71    #[inline]
72    fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
73        Self::new(
74            clamp_unit_f32(red),
75            clamp_unit_f32(green),
76            clamp_unit_f32(blue),
77            alpha.clamp(0.0, OPAQUE),
78        )
79    }
80
81    /// Same thing, but with `u8` values instead of floats in the 0 to 1 range.
82    #[inline]
83    pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
84        Self {
85            red,
86            green,
87            blue,
88            alpha,
89        }
90    }
91}
92
93impl From<cssc::RgbaLegacy> for RGBA {
94    fn from(c: cssc::RgbaLegacy) -> RGBA {
95        RGBA {
96            red: c.red,
97            green: c.green,
98            blue: c.blue,
99            alpha: c.alpha,
100        }
101    }
102}
103
104impl From<cssc::Hsl> for Hsl {
105    fn from(c: cssc::Hsl) -> Hsl {
106        Hsl {
107            hue: c.hue,
108            saturation: c.saturation,
109            lightness: c.lightness,
110            alpha: c.alpha,
111        }
112    }
113}
114
115impl From<cssc::Hwb> for Hwb {
116    fn from(c: cssc::Hwb) -> Hwb {
117        Hwb {
118            hue: c.hue,
119            whiteness: c.whiteness,
120            blackness: c.blackness,
121            alpha: c.alpha,
122        }
123    }
124}
125
126fn clamp_unit_f32(val: f32) -> u8 {
127    // Whilst scaling by 256 and flooring would provide
128    // an equal distribution of integers to percentage inputs,
129    // this is not what Gecko does so we instead multiply by 255
130    // and round (adding 0.5 and flooring is equivalent to rounding)
131    //
132    // Chrome does something similar for the alpha value, but not
133    // the rgb values.
134    //
135    // See <https://bugzilla.mozilla.org/show_bug.cgi?id=1340484>
136    //
137    // Clamping to 256 and rounding after would let 1.0 map to 256, and
138    // `256.0_f32 as u8` is undefined behavior:
139    //
140    // <https://github.com/rust-lang/rust/issues/10184>
141    clamp_floor_256_f32(val * 255.)
142}
143
144fn clamp_floor_256_f32(val: f32) -> u8 {
145    val.round().clamp(0., 255.) as u8
146}
147
148/// Turn a short-lived [`cssparser::ParseError`] into a long-lived [`ParseError`].
149///
150/// cssparser's error type has a lifetime equal to the string being parsed.  We want
151/// a long-lived error so we can store it away if needed.  Basically, here we turn
152/// a `&str` into a `String`.
153fn map_color_parse_error(err: cssparser::ParseError<'_, ()>) -> ParseError<'_> {
154    let string_err = match err.kind {
155        ParseErrorKind::Basic(ref e) => format!("{}", e),
156        ParseErrorKind::Custom(()) => {
157            // In cssparser 0.31, the error type for Color::parse is defined like this:
158            //
159            //   pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i, ()>> {
160            //
161            // The ParseError<'i, ()> means that the ParseErrorKind::Custom(T) variant will have
162            // T be the () type.
163            //
164            // So, here we match for () inside the Custom variant.  If cssparser
165            // changes its error API, this match will hopefully catch errors.
166            //
167            // Implementation detail: Color::parse() does not ever return Custom errors, only
168            // Basic ones.  So the match for Basic above handles everything, and this one
169            // for () is a dummy case.
170            "could not parse color".to_string()
171        }
172    };
173
174    ParseError {
175        kind: ParseErrorKind::Custom(ValueErrorKind::Parse(string_err)),
176        location: err.location,
177    }
178}
179
180fn parse_plain_color<'i>(parser: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i>> {
181    let loc = parser.current_source_location();
182
183    let csscolor = cssc::Color::parse(parser).map_err(map_color_parse_error)?;
184
185    // Return only supported color types, and mark the others as errors.
186    match csscolor {
187        cssc::Color::CurrentColor => Ok(Color::CurrentColor),
188
189        cssc::Color::Rgba(rgba) => Ok(Color::Rgba(rgba.into())),
190
191        cssc::Color::Hsl(hsl) => Ok(Color::Hsl(hsl.into())),
192
193        cssc::Color::Hwb(hwb) => Ok(Color::Hwb(hwb.into())),
194
195        _ => Err(ParseError {
196            kind: ParseErrorKind::Custom(ValueErrorKind::parse_error("unsupported color syntax")),
197            location: loc,
198        }),
199    }
200}
201
202/// Parse a custom property name.
203///
204/// <https://drafts.csswg.org/css-variables/#typedef-custom-property-name>
205fn parse_name(s: &str) -> Result<&str, ()> {
206    if s.starts_with("--") && s.len() > 2 {
207        Ok(&s[2..])
208    } else {
209        Err(())
210    }
211}
212
213fn parse_var_with_fallback<'i>(parser: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i>> {
214    let name = parser.expect_ident_cloned()?;
215
216    // ignore the name for now; we'll use it later when we actually
217    // process the names of custom variables
218    let _name = parse_name(&name).map_err(|()| {
219        parser.new_custom_error(ValueErrorKind::parse_error(&format!(
220            "unexpected identifier {}",
221            name
222        )))
223    })?;
224
225    parser.expect_comma()?;
226
227    // FIXME: when fixing #459 (full support for var()), note that
228    // https://drafts.csswg.org/css-variables/#using-variables indicates that var(--a,) is
229    // a valid function, which means that the fallback value is an empty set of tokens.
230    //
231    // Also, see Servo's extra code to handle semicolons and stuff in toplevel rules.
232    //
233    // Also, tweak the tests tagged with "FIXME: var()" below.
234
235    parse_plain_color(parser)
236}
237
238impl Parse for Color {
239    fn parse<'i>(parser: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i>> {
240        if let Ok(c) = parser.try_parse(|p| {
241            p.expect_function_matching("var")?;
242            p.parse_nested_block(parse_var_with_fallback)
243        }) {
244            Ok(c)
245        } else {
246            parse_plain_color(parser)
247        }
248    }
249}
250
251/// Normalizes `h` (a hue value in degrees) to be in the interval `[0.0, 1.0]`.
252///
253/// Rust-cssparser (the cssparser-color crate) provides
254/// [`hsl_to_rgb()`], but it assumes that the hue is between 0 and 1.
255/// `normalize_hue()` takes a value with respect to a scale of 0 to
256/// 360 degrees and converts it to that different scale.
257fn normalize_hue(h: f32) -> f32 {
258    h.rem_euclid(360.0) / 360.0
259}
260
261pub fn color_to_rgba(color: &Color) -> RGBA {
262    match color {
263        Color::Rgba(rgba) => *rgba,
264
265        Color::Hsl(hsl) => {
266            let hue = normalize_hue(hsl.hue.unwrap_or(0.0));
267            let (red, green, blue) = hsl_to_rgb(
268                hue,
269                hsl.saturation.unwrap_or(0.0),
270                hsl.lightness.unwrap_or(0.0),
271            );
272
273            RGBA::from_floats(red, green, blue, hsl.alpha.unwrap_or(OPAQUE))
274        }
275
276        Color::Hwb(hwb) => {
277            let hue = normalize_hue(hwb.hue.unwrap_or(0.0));
278            let (red, green, blue) = hwb_to_rgb(
279                hue,
280                hwb.whiteness.unwrap_or(0.0),
281                hwb.blackness.unwrap_or(0.0),
282            );
283
284            RGBA::from_floats(red, green, blue, hwb.alpha.unwrap_or(OPAQUE))
285        }
286
287        _ => unimplemented!(),
288    }
289}
290
291/// Takes the `opacity` property and an alpha value from a CSS `<color>` and returns a resulting
292/// alpha for a computed value.
293///
294/// `alpha` is `Option<f32>` because that is what cssparser uses everywhere.
295fn resolve_alpha(opacity: UnitInterval, alpha: Option<f32>) -> f32 {
296    let UnitInterval(o) = opacity;
297
298    let alpha = f64::from(alpha.unwrap_or(0.0)) * o;
299    let alpha = util::clamp(alpha, 0.0, 1.0);
300    cast::f32(alpha).unwrap()
301}
302
303fn black() -> Color {
304    Color::Rgba(RGBA::new(0, 0, 0, 1.0))
305}
306
307/// Resolves a CSS color from itself, an `opacity` property, and a `color` property (to resolve `currentColor`).
308///
309/// A CSS color can be `currentColor`, in which case the computed value comes from
310/// the `color` property.  You should pass the `color` property's value for `current_color`.
311///
312/// Note that `currrent_color` can itself have a value of `currentColor`.  In that case, we
313/// consider it to be opaque black.
314pub fn resolve_color(color: &Color, opacity: UnitInterval, current_color: &Color) -> Color {
315    let without_opacity_applied = match color {
316        Color::CurrentColor => {
317            if let Color::CurrentColor = current_color {
318                black()
319            } else {
320                *current_color
321            }
322        }
323
324        _ => *color,
325    };
326
327    match without_opacity_applied {
328        Color::CurrentColor => unreachable!(),
329
330        Color::Rgba(rgba) => Color::Rgba(RGBA {
331            alpha: resolve_alpha(opacity, Some(rgba.alpha)),
332            ..rgba
333        }),
334
335        Color::Hsl(hsl) => Color::Hsl(Hsl {
336            alpha: Some(resolve_alpha(opacity, hsl.alpha)),
337            ..hsl
338        }),
339
340        Color::Hwb(hwb) => Color::Hwb(Hwb {
341            alpha: Some(resolve_alpha(opacity, hwb.alpha)),
342            ..hwb
343        }),
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn parses_plain_color() {
353        assert_eq!(
354            Color::parse_str("#112233").unwrap(),
355            Color::Rgba(RGBA::new(0x11, 0x22, 0x33, 1.0))
356        );
357    }
358
359    #[test]
360    fn var_with_fallback_parses_as_color() {
361        assert_eq!(
362            Color::parse_str("var(--foo, #112233)").unwrap(),
363            Color::Rgba(RGBA::new(0x11, 0x22, 0x33, 1.0))
364        );
365
366        assert_eq!(
367            Color::parse_str("var(--foo, rgb(100% 50% 25%)").unwrap(),
368            Color::Rgba(RGBA::new(0xff, 0x80, 0x40, 1.0))
369        );
370    }
371
372    // FIXME: var() - when fixing #459, see the note in the code above.  All the syntaxes
373    // in this test function will become valid once we have full support for var().
374    #[test]
375    fn var_without_fallback_yields_error() {
376        assert!(Color::parse_str("var(--foo)").is_err());
377        assert!(Color::parse_str("var(--foo,)").is_err());
378        assert!(Color::parse_str("var(--foo, )").is_err());
379        assert!(Color::parse_str("var(--foo, this is not a color)").is_err());
380        assert!(Color::parse_str("var(--foo, #112233, blah)").is_err());
381    }
382
383    #[test]
384    fn normalizes_hue() {
385        assert_eq!(normalize_hue(0.0), 0.0);
386        assert_eq!(normalize_hue(360.0), 0.0);
387        assert_eq!(normalize_hue(90.0), 0.25);
388        assert_eq!(normalize_hue(-90.0), 0.75);
389        assert_eq!(normalize_hue(450.0), 0.25); // 360 + 90 degrees
390        assert_eq!(normalize_hue(-450.0), 0.75);
391    }
392
393    // Bug #1117
394    #[test]
395    fn large_hue_value() {
396        let _ = color_to_rgba(&Color::parse_str("hsla(70000000000000,4%,10%,.2)").unwrap());
397    }
398}