Skip to main content

cssparser/
color.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5//! General color-parsing utilities, independent on the specific color storage and parsing
6//! implementation.
7//!
8//! For a more complete css-color implementation take a look at cssparser-color crate, or at
9//! Gecko's color module.
10
11// Allow text like <color> in docs.
12#![allow(rustdoc::invalid_html_tags)]
13
14/// The opaque alpha value of 1.0.
15pub const OPAQUE: f32 = 1.0;
16
17use crate::{BasicParseError, Parser, ToCss};
18use std::fmt;
19
20/// Clamp a 0..1 number to a 0..255 range to u8.
21///
22/// Whilst scaling by 256 and flooring would provide
23/// an equal distribution of integers to percentage inputs,
24/// this is not what Gecko does so we instead multiply by 255
25/// and round (adding 0.5 and flooring is equivalent to rounding)
26///
27/// Chrome does something similar for the alpha value, but not
28/// the rgb values.
29///
30/// See <https://bugzilla.mozilla.org/show_bug.cgi?id=1340484>
31///
32/// Clamping to 256 and rounding after would let 1.0 map to 256, and
33/// `256.0_f32 as u8` is undefined behavior:
34///
35/// <https://github.com/rust-lang/rust/issues/10184>
36#[inline]
37pub fn clamp_unit_f32(val: f32) -> u8 {
38    clamp_floor_256_f32(val * 255.)
39}
40
41/// Round and clamp a single number to a u8.
42#[inline]
43pub fn clamp_floor_256_f32(val: f32) -> u8 {
44    val.round().clamp(0., 255.) as u8
45}
46
47/// Serialize the alpha copmonent of a color according to the specification.
48/// <https://drafts.csswg.org/css-color-4/#serializing-alpha-values>
49#[inline]
50pub fn serialize_color_alpha(
51    dest: &mut impl fmt::Write,
52    alpha: Option<f32>,
53    legacy_syntax: bool,
54) -> fmt::Result {
55    let alpha = match alpha {
56        None => return dest.write_str(" / none"),
57        Some(a) => a,
58    };
59
60    // If the alpha component is full opaque, don't emit the alpha value in CSS.
61    if alpha == OPAQUE {
62        return Ok(());
63    }
64
65    dest.write_str(if legacy_syntax { ", " } else { " / " })?;
66
67    // Try first with two decimal places, then with three.
68    let mut rounded_alpha = (alpha * 100.).round() / 100.;
69    if clamp_unit_f32(rounded_alpha) != clamp_unit_f32(alpha) {
70        rounded_alpha = (alpha * 1000.).round() / 1000.;
71    }
72
73    rounded_alpha.to_css(dest)
74}
75
76/// A Predefined color space specified in:
77/// <https://drafts.csswg.org/css-color-4/#predefined>
78#[derive(Clone, Copy, Eq, PartialEq, Debug)]
79#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
80#[cfg_attr(feature = "serde", serde(tag = "type"))]
81pub enum PredefinedColorSpace {
82    /// <https://drafts.csswg.org/css-color-4/#predefined-sRGB>
83    Srgb,
84    /// <https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear>
85    SrgbLinear,
86    /// <https://drafts.csswg.org/css-color-4/#predefined-display-p3>
87    DisplayP3,
88    /// <https://drafts.csswg.org/css-color-4/#predefined-display-p3-linear>
89    DisplayP3Linear,
90    /// <https://drafts.csswg.org/css-color-4/#predefined-a98-rgb>
91    A98Rgb,
92    /// <https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb>
93    ProphotoRgb,
94    /// <https://drafts.csswg.org/css-color-4/#predefined-rec2020>
95    Rec2020,
96    /// <https://drafts.csswg.org/css-color-4/#predefined-xyz>
97    XyzD50,
98    /// <https://drafts.csswg.org/css-color-4/#predefined-xyz>
99    XyzD65,
100}
101
102impl PredefinedColorSpace {
103    /// Parse a PredefinedColorSpace from the given input.
104    pub fn parse(input: &mut Parser) -> Result<Self, BasicParseError> {
105        let ident = input.expect_ident()?;
106        Ok(match_ignore_ascii_case! { ident,
107            "srgb" => Self::Srgb,
108            "srgb-linear" => Self::SrgbLinear,
109            "display-p3" => Self::DisplayP3,
110            "display-p3-linear" => Self::DisplayP3Linear,
111            "a98-rgb" => Self::A98Rgb,
112            "prophoto-rgb" => Self::ProphotoRgb,
113            "rec2020" => Self::Rec2020,
114            "xyz-d50" => Self::XyzD50,
115            "xyz" | "xyz-d65" => Self::XyzD65,
116            _ => return Err(BasicParseError::unexpected_token()),
117        })
118    }
119}
120
121impl ToCss for PredefinedColorSpace {
122    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
123    where
124        W: fmt::Write,
125    {
126        dest.write_str(match self {
127            Self::Srgb => "srgb",
128            Self::SrgbLinear => "srgb-linear",
129            Self::DisplayP3 => "display-p3",
130            Self::DisplayP3Linear => "display-p3-linear",
131            Self::A98Rgb => "a98-rgb",
132            Self::ProphotoRgb => "prophoto-rgb",
133            Self::Rec2020 => "rec2020",
134            Self::XyzD50 => "xyz-d50",
135            Self::XyzD65 => "xyz-d65",
136        })
137    }
138}
139
140/// Parse a color hash, without the leading '#' character.
141#[allow(clippy::result_unit_err)]
142#[inline]
143pub fn parse_hash_color(value: &[u8]) -> Result<(u8, u8, u8, f32), ()> {
144    Ok(match value.len() {
145        8 => (
146            from_hex(value[0])? * 16 + from_hex(value[1])?,
147            from_hex(value[2])? * 16 + from_hex(value[3])?,
148            from_hex(value[4])? * 16 + from_hex(value[5])?,
149            (from_hex(value[6])? * 16 + from_hex(value[7])?) as f32 / 255.0,
150        ),
151        6 => (
152            from_hex(value[0])? * 16 + from_hex(value[1])?,
153            from_hex(value[2])? * 16 + from_hex(value[3])?,
154            from_hex(value[4])? * 16 + from_hex(value[5])?,
155            OPAQUE,
156        ),
157        4 => (
158            from_hex(value[0])? * 17,
159            from_hex(value[1])? * 17,
160            from_hex(value[2])? * 17,
161            (from_hex(value[3])? * 17) as f32 / 255.0,
162        ),
163        3 => (
164            from_hex(value[0])? * 17,
165            from_hex(value[1])? * 17,
166            from_hex(value[2])? * 17,
167            OPAQUE,
168        ),
169        _ => return Err(()),
170    })
171}
172
173ascii_case_insensitive_map! {
174    named_colors -> (u8, u8, u8) = {
175        "black" => (0, 0, 0),
176        "silver" => (192, 192, 192),
177        "gray" => (128, 128, 128),
178        "white" => (255, 255, 255),
179        "maroon" => (128, 0, 0),
180        "red" => (255, 0, 0),
181        "purple" => (128, 0, 128),
182        "fuchsia" => (255, 0, 255),
183        "green" => (0, 128, 0),
184        "lime" => (0, 255, 0),
185        "olive" => (128, 128, 0),
186        "yellow" => (255, 255, 0),
187        "navy" => (0, 0, 128),
188        "blue" => (0, 0, 255),
189        "teal" => (0, 128, 128),
190        "aqua" => (0, 255, 255),
191
192        "aliceblue" => (240, 248, 255),
193        "antiquewhite" => (250, 235, 215),
194        "aquamarine" => (127, 255, 212),
195        "azure" => (240, 255, 255),
196        "beige" => (245, 245, 220),
197        "bisque" => (255, 228, 196),
198        "blanchedalmond" => (255, 235, 205),
199        "blueviolet" => (138, 43, 226),
200        "brown" => (165, 42, 42),
201        "burlywood" => (222, 184, 135),
202        "cadetblue" => (95, 158, 160),
203        "chartreuse" => (127, 255, 0),
204        "chocolate" => (210, 105, 30),
205        "coral" => (255, 127, 80),
206        "cornflowerblue" => (100, 149, 237),
207        "cornsilk" => (255, 248, 220),
208        "crimson" => (220, 20, 60),
209        "cyan" => (0, 255, 255),
210        "darkblue" => (0, 0, 139),
211        "darkcyan" => (0, 139, 139),
212        "darkgoldenrod" => (184, 134, 11),
213        "darkgray" => (169, 169, 169),
214        "darkgreen" => (0, 100, 0),
215        "darkgrey" => (169, 169, 169),
216        "darkkhaki" => (189, 183, 107),
217        "darkmagenta" => (139, 0, 139),
218        "darkolivegreen" => (85, 107, 47),
219        "darkorange" => (255, 140, 0),
220        "darkorchid" => (153, 50, 204),
221        "darkred" => (139, 0, 0),
222        "darksalmon" => (233, 150, 122),
223        "darkseagreen" => (143, 188, 143),
224        "darkslateblue" => (72, 61, 139),
225        "darkslategray" => (47, 79, 79),
226        "darkslategrey" => (47, 79, 79),
227        "darkturquoise" => (0, 206, 209),
228        "darkviolet" => (148, 0, 211),
229        "deeppink" => (255, 20, 147),
230        "deepskyblue" => (0, 191, 255),
231        "dimgray" => (105, 105, 105),
232        "dimgrey" => (105, 105, 105),
233        "dodgerblue" => (30, 144, 255),
234        "firebrick" => (178, 34, 34),
235        "floralwhite" => (255, 250, 240),
236        "forestgreen" => (34, 139, 34),
237        "gainsboro" => (220, 220, 220),
238        "ghostwhite" => (248, 248, 255),
239        "gold" => (255, 215, 0),
240        "goldenrod" => (218, 165, 32),
241        "greenyellow" => (173, 255, 47),
242        "grey" => (128, 128, 128),
243        "honeydew" => (240, 255, 240),
244        "hotpink" => (255, 105, 180),
245        "indianred" => (205, 92, 92),
246        "indigo" => (75, 0, 130),
247        "ivory" => (255, 255, 240),
248        "khaki" => (240, 230, 140),
249        "lavender" => (230, 230, 250),
250        "lavenderblush" => (255, 240, 245),
251        "lawngreen" => (124, 252, 0),
252        "lemonchiffon" => (255, 250, 205),
253        "lightblue" => (173, 216, 230),
254        "lightcoral" => (240, 128, 128),
255        "lightcyan" => (224, 255, 255),
256        "lightgoldenrodyellow" => (250, 250, 210),
257        "lightgray" => (211, 211, 211),
258        "lightgreen" => (144, 238, 144),
259        "lightgrey" => (211, 211, 211),
260        "lightpink" => (255, 182, 193),
261        "lightsalmon" => (255, 160, 122),
262        "lightseagreen" => (32, 178, 170),
263        "lightskyblue" => (135, 206, 250),
264        "lightslategray" => (119, 136, 153),
265        "lightslategrey" => (119, 136, 153),
266        "lightsteelblue" => (176, 196, 222),
267        "lightyellow" => (255, 255, 224),
268        "limegreen" => (50, 205, 50),
269        "linen" => (250, 240, 230),
270        "magenta" => (255, 0, 255),
271        "mediumaquamarine" => (102, 205, 170),
272        "mediumblue" => (0, 0, 205),
273        "mediumorchid" => (186, 85, 211),
274        "mediumpurple" => (147, 112, 219),
275        "mediumseagreen" => (60, 179, 113),
276        "mediumslateblue" => (123, 104, 238),
277        "mediumspringgreen" => (0, 250, 154),
278        "mediumturquoise" => (72, 209, 204),
279        "mediumvioletred" => (199, 21, 133),
280        "midnightblue" => (25, 25, 112),
281        "mintcream" => (245, 255, 250),
282        "mistyrose" => (255, 228, 225),
283        "moccasin" => (255, 228, 181),
284        "navajowhite" => (255, 222, 173),
285        "oldlace" => (253, 245, 230),
286        "olivedrab" => (107, 142, 35),
287        "orange" => (255, 165, 0),
288        "orangered" => (255, 69, 0),
289        "orchid" => (218, 112, 214),
290        "palegoldenrod" => (238, 232, 170),
291        "palegreen" => (152, 251, 152),
292        "paleturquoise" => (175, 238, 238),
293        "palevioletred" => (219, 112, 147),
294        "papayawhip" => (255, 239, 213),
295        "peachpuff" => (255, 218, 185),
296        "peru" => (205, 133, 63),
297        "pink" => (255, 192, 203),
298        "plum" => (221, 160, 221),
299        "powderblue" => (176, 224, 230),
300        "rebeccapurple" => (102, 51, 153),
301        "rosybrown" => (188, 143, 143),
302        "royalblue" => (65, 105, 225),
303        "saddlebrown" => (139, 69, 19),
304        "salmon" => (250, 128, 114),
305        "sandybrown" => (244, 164, 96),
306        "seagreen" => (46, 139, 87),
307        "seashell" => (255, 245, 238),
308        "sienna" => (160, 82, 45),
309        "skyblue" => (135, 206, 235),
310        "slateblue" => (106, 90, 205),
311        "slategray" => (112, 128, 144),
312        "slategrey" => (112, 128, 144),
313        "snow" => (255, 250, 250),
314        "springgreen" => (0, 255, 127),
315        "steelblue" => (70, 130, 180),
316        "tan" => (210, 180, 140),
317        "thistle" => (216, 191, 216),
318        "tomato" => (255, 99, 71),
319        "turquoise" => (64, 224, 208),
320        "violet" => (238, 130, 238),
321        "wheat" => (245, 222, 179),
322        "whitesmoke" => (245, 245, 245),
323        "yellowgreen" => (154, 205, 50),
324    }
325}
326
327/// Returns the named color with the given name.
328/// <https://drafts.csswg.org/css-color-4/#typedef-named-color>
329#[allow(clippy::result_unit_err)]
330#[inline]
331pub fn parse_named_color(ident: &str) -> Result<(u8, u8, u8), ()> {
332    named_colors::get(ident).copied().ok_or(())
333}
334
335/// Returns an iterator over all named CSS colors.
336/// <https://drafts.csswg.org/css-color-4/#typedef-named-color>
337#[inline]
338pub fn all_named_colors() -> impl Iterator<Item = (&'static str, (u8, u8, u8))> {
339    named_colors::entries().map(|(k, v)| (*k, *v))
340}
341
342#[inline]
343fn from_hex(c: u8) -> Result<u8, ()> {
344    match c {
345        b'0'..=b'9' => Ok(c - b'0'),
346        b'a'..=b'f' => Ok(c - b'a' + 10),
347        b'A'..=b'F' => Ok(c - b'A' + 10),
348        _ => Err(()),
349    }
350}