color/
serialize.rs

1// Copyright 2024 the Color Authors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! CSS-compatible string serializations of colors.
5
6use core::fmt::{Formatter, Result};
7
8use crate::{ColorSpaceTag, DynamicColor, Rgba8};
9
10fn write_scaled_component(
11    color: &DynamicColor,
12    ix: usize,
13    f: &mut Formatter<'_>,
14    scale: f32,
15) -> Result {
16    if color.flags.missing().contains(ix) {
17        // According to the serialization rules (ยง15.2), missing should be converted to 0.
18        // However, it seems useful to preserve these. Perhaps we want to talk about whether
19        // we want string formatting to strictly follow the serialization spec.
20
21        write!(f, "none")
22    } else {
23        write!(f, "{}", color.components[ix] * scale)
24    }
25}
26
27fn write_modern_function(color: &DynamicColor, name: &str, f: &mut Formatter<'_>) -> Result {
28    write!(f, "{name}(")?;
29    write_scaled_component(color, 0, f, 1.0)?;
30    write!(f, " ")?;
31    write_scaled_component(color, 1, f, 1.0)?;
32    write!(f, " ")?;
33    write_scaled_component(color, 2, f, 1.0)?;
34    if color.components[3] < 1.0 {
35        write!(f, " / ")?;
36        // TODO: clamp negative values
37        write_scaled_component(color, 3, f, 1.0)?;
38    }
39    write!(f, ")")
40}
41
42fn write_color_function(color: &DynamicColor, name: &str, f: &mut Formatter<'_>) -> Result {
43    write!(f, "color({name} ")?;
44    write_scaled_component(color, 0, f, 1.0)?;
45    write!(f, " ")?;
46    write_scaled_component(color, 1, f, 1.0)?;
47    write!(f, " ")?;
48    write_scaled_component(color, 2, f, 1.0)?;
49    if color.components[3] < 1.0 {
50        write!(f, " / ")?;
51        // TODO: clamp negative values
52        write_scaled_component(color, 3, f, 1.0)?;
53    }
54    write!(f, ")")
55}
56
57fn write_legacy_function(
58    color: &DynamicColor,
59    name: &str,
60    scale: f32,
61    f: &mut Formatter<'_>,
62) -> Result {
63    let opt_a = if color.components[3] < 1.0 { "a" } else { "" };
64    write!(f, "{name}{opt_a}(")?;
65    write_scaled_component(color, 0, f, scale)?;
66    write!(f, ", ")?;
67    write_scaled_component(color, 1, f, scale)?;
68    write!(f, ", ")?;
69    write_scaled_component(color, 2, f, scale)?;
70    if color.components[3] < 1.0 {
71        write!(f, ", ")?;
72        // TODO: clamp negative values
73        write_scaled_component(color, 3, f, 1.0)?;
74    }
75    write!(f, ")")
76}
77
78impl core::fmt::Display for DynamicColor {
79    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
80        if let Some(color_name) = self.flags.color_name() {
81            return write!(f, "{color_name}");
82        }
83
84        match self.cs {
85            ColorSpaceTag::Srgb if self.flags.named() => {
86                write_legacy_function(self, "rgb", 255.0, f)
87            }
88            ColorSpaceTag::Hsl | ColorSpaceTag::Hwb if self.flags.named() => {
89                let srgb = self.convert(ColorSpaceTag::Srgb);
90                write_legacy_function(&srgb, "rgb", 255.0, f)
91            }
92            ColorSpaceTag::Srgb => write_color_function(self, "srgb", f),
93            ColorSpaceTag::LinearSrgb => write_color_function(self, "srgb-linear", f),
94            ColorSpaceTag::DisplayP3 => write_color_function(self, "display-p3", f),
95            ColorSpaceTag::A98Rgb => write_color_function(self, "a98-rgb", f),
96            ColorSpaceTag::ProphotoRgb => write_color_function(self, "prophoto-rgb", f),
97            ColorSpaceTag::Rec2020 => write_color_function(self, "rec2020", f),
98            ColorSpaceTag::Aces2065_1 => write_color_function(self, "--aces2065-1", f),
99            ColorSpaceTag::AcesCg => write_color_function(self, "--acescg", f),
100            ColorSpaceTag::Hsl => write_legacy_function(self, "hsl", 1.0, f),
101            ColorSpaceTag::Hwb => write_modern_function(self, "hwb", f),
102            ColorSpaceTag::XyzD50 => write_color_function(self, "xyz-d50", f),
103            ColorSpaceTag::XyzD65 => write_color_function(self, "xyz-d65", f),
104            ColorSpaceTag::Lab => write_modern_function(self, "lab", f),
105            ColorSpaceTag::Lch => write_modern_function(self, "lch", f),
106            ColorSpaceTag::Oklab => write_modern_function(self, "oklab", f),
107            ColorSpaceTag::Oklch => write_modern_function(self, "oklch", f),
108        }
109    }
110}
111
112impl core::fmt::Display for Rgba8 {
113    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
114        if self.a == 255 {
115            write!(f, "rgb({}, {}, {})", self.r, self.g, self.b)
116        } else {
117            let a = self.a as f32 * (1.0 / 255.0);
118            write!(f, "rgba({}, {}, {}, {a})", self.r, self.g, self.b)
119        }
120    }
121}
122
123impl core::fmt::LowerHex for Rgba8 {
124    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
125        if self.a == 255 {
126            write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
127        } else {
128            write!(
129                f,
130                "#{:02x}{:02x}{:02x}{:02x}",
131                self.r, self.g, self.b, self.a
132            )
133        }
134    }
135}
136
137impl core::fmt::UpperHex for Rgba8 {
138    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
139        if self.a == 255 {
140            write!(f, "#{:02X}{:02X}{:02X}", self.r, self.g, self.b)
141        } else {
142            write!(
143                f,
144                "#{:02X}{:02X}{:02X}{:02X}",
145                self.r, self.g, self.b, self.a
146            )
147        }
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    extern crate alloc;
154
155    use crate::{parse_color, AlphaColor, DynamicColor, Hsl, Oklab, Srgb, XyzD65};
156    use alloc::format;
157
158    #[test]
159    fn rgb8() {
160        let c = parse_color("#abcdef").unwrap().to_alpha_color::<Srgb>();
161        assert_eq!(format!("{:x}", c.to_rgba8()), "#abcdef");
162        assert_eq!(format!("{:X}", c.to_rgba8()), "#ABCDEF");
163        let c_alpha = c.with_alpha(1. / 3.);
164        assert_eq!(format!("{:x}", c_alpha.to_rgba8()), "#abcdef55");
165        assert_eq!(format!("{:X}", c_alpha.to_rgba8()), "#ABCDEF55");
166    }
167
168    #[test]
169    fn specified_to_serialized() {
170        for (specified, expected) in [
171            ("#ff0000", "rgb(255, 0, 0)"),
172            ("rgb(255,0,0)", "rgb(255, 0, 0)"),
173            ("rgba(255,0,0,50%)", "rgba(255, 0, 0, 0.5)"),
174            ("rgb(255 0 0 / 95%)", "rgba(255, 0, 0, 0.95)"),
175            // TODO: output rounding? Otherwise the tests should check for approximate equality
176            // (and not string equality) for these conversion cases
177            // (
178            //     "hwb(740deg 20% 30% / 50%)",
179            //     "rgba(178.5, 93.50008, 50.999996, 0.5)",
180            // ),
181            ("ReD", "red"),
182            ("RgB(1,1,1)", "rgb(1, 1, 1)"),
183            ("rgb(257,-2,50)", "rgb(255, 0, 50)"),
184            ("color(srgb 1.0 1.0 1.0)", "color(srgb 1 1 1)"),
185            ("oklab(0.4 0.2 -0.2)", "oklab(0.4 0.2 -0.2)"),
186            ("lab(20% 0 60)", "lab(20 0 60)"),
187        ] {
188            let result = format!("{}", parse_color(specified).unwrap());
189            assert_eq!(
190                result,
191                expected,
192                "Failed serializing specified color `{specified}`. Expected: `{expected}`. Got: `{result}`."
193            );
194        }
195
196        // TODO: this can be removed when the "output rounding" TODO above is resolved. Here we
197        // just check the prefix is as expected.
198        for (specified, expected_prefix) in [
199            ("hwb(740deg 20% 30%)", "rgb("),
200            ("hwb(740deg 20% 30% / 50%)", "rgba("),
201            ("hsl(120deg 50% 25%)", "rgb("),
202            ("hsla(0.4turn 50% 25% / 50%)", "rgba("),
203        ] {
204            let result = format!("{}", parse_color(specified).unwrap());
205            assert!(
206                result.starts_with(expected_prefix),
207                "Failed serializing specified color `{specified}`. Expected the serialization to start with: `{expected_prefix}`. Got: `{result}`."
208            );
209        }
210    }
211
212    #[test]
213    fn generated_to_serialized() {
214        for (color, expected) in [
215            (
216                DynamicColor::from_alpha_color(AlphaColor::<Srgb>::new([0.5, 0.2, 1.1, 0.5])),
217                "color(srgb 0.5 0.2 1.1 / 0.5)",
218            ),
219            (
220                DynamicColor::from_alpha_color(AlphaColor::<Oklab>::new([0.4, 0.2, -0.2, 1.])),
221                "oklab(0.4 0.2 -0.2)",
222            ),
223            (
224                DynamicColor::from_alpha_color(AlphaColor::<XyzD65>::new([
225                    0.472, 0.372, 0.131, 1.,
226                ])),
227                "color(xyz-d65 0.472 0.372 0.131)",
228            ),
229            // Perhaps this should actually serialize to `rgb(...)`.
230            (
231                DynamicColor::from_alpha_color(AlphaColor::<Hsl>::new([120., 50., 25., 1.])),
232                "hsl(120, 50, 25)",
233            ),
234        ] {
235            let result = format!("{color}");
236            assert_eq!(
237                result,
238                expected,
239                "Failed serializing specified color `{color}`. Expected: `{expected}`. Got: `{result}`."
240            );
241        }
242    }
243
244    #[test]
245    fn roundtrip_named_colors() {
246        for name in crate::x11_colors::NAMES {
247            let result = format!("{}", parse_color(name).unwrap());
248            assert_eq!(
249                result,
250                name,
251                "Failed serializing specified named color `{name}`. Expected it to roundtrip. Got: `{result}`."
252            );
253        }
254    }
255}