1use 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 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 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 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 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 ("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 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 (
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}