1use core::fmt;
16use std::error;
17use std::marker::PhantomData;
18use std::str::from_utf8;
19
20#[derive(Debug, Clone, Eq, PartialEq)]
22#[allow(clippy::exhaustive_structs)]
23pub struct Color {
24 pub red: u16,
26 pub green: u16,
28 pub blue: u16,
30 pub alpha: u16,
35}
36
37impl Color {
38 pub const fn rgb(red: u16, green: u16, blue: u16) -> Self {
40 Self {
41 red,
42 green,
43 blue,
44 alpha: u16::MAX,
45 }
46 }
47
48 #[doc(alias = "XParseColor")]
60 pub fn parse(input: &[u8]) -> Result<Color, ColorParseError> {
61 xparsecolor(input).ok_or(ColorParseError(PhantomData))
62 }
63}
64
65#[derive(Debug, Clone)]
67pub struct ColorParseError(PhantomData<()>);
68
69impl error::Error for ColorParseError {}
70
71impl fmt::Display for ColorParseError {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 f.write_str("invalid color spec")
74 }
75}
76
77fn xparsecolor(input: &[u8]) -> Option<Color> {
78 if let Some(stripped) = input.strip_prefix(b"#") {
79 parse_sharp(from_utf8(stripped).ok()?)
80 } else if let Some(stripped) = input.strip_prefix(b"rgb:") {
81 parse_rgb(from_utf8(stripped).ok()?)
82 } else if let Some(stripped) = input.strip_prefix(b"rgba:") {
83 parse_rgba(from_utf8(stripped).ok()?)
84 } else {
85 None
86 }
87}
88
89fn parse_sharp(input: &str) -> Option<Color> {
99 const NUM_COMPONENTS: usize = 3;
100 let len = input.len();
101 if len % NUM_COMPONENTS == 0 && len <= NUM_COMPONENTS * 4 {
102 let chunk_size = input.len() / NUM_COMPONENTS;
103 let red = parse_channel_shifted(&input[0..chunk_size])?;
104 let green = parse_channel_shifted(&input[chunk_size..chunk_size * 2])?;
105 let blue = parse_channel_shifted(&input[chunk_size * 2..])?;
106 Some(Color::rgb(red, green, blue))
107 } else {
108 None
109 }
110}
111
112fn parse_channel_shifted(input: &str) -> Option<u16> {
113 let value = u16::from_str_radix(input, 16).ok()?;
114 Some(value << ((4 - input.len()) * 4))
115}
116
117fn parse_rgb(input: &str) -> Option<Color> {
129 let mut parts = input.split('/');
130 let red = parse_channel_scaled(parts.next()?)?;
131 let green = parse_channel_scaled(parts.next()?)?;
132 let blue = parse_channel_scaled(parts.next()?)?;
133 if parts.next().is_none() {
134 Some(Color::rgb(red, green, blue))
135 } else {
136 None
137 }
138}
139
140fn parse_rgba(input: &str) -> Option<Color> {
149 let mut parts = input.split('/');
150 let red = parse_channel_scaled(parts.next()?)?;
151 let green = parse_channel_scaled(parts.next()?)?;
152 let blue = parse_channel_scaled(parts.next()?)?;
153 let alpha = parse_channel_scaled(parts.next()?)?;
154 if parts.next().is_none() {
155 Some(Color {
156 red,
157 green,
158 blue,
159 alpha,
160 })
161 } else {
162 None
163 }
164}
165
166fn parse_channel_scaled(input: &str) -> Option<u16> {
167 let len = input.len();
168 if (1..=4).contains(&len) {
169 let max = u32::pow(16, len as u32) - 1;
170 let value = u32::from_str_radix(input, 16).ok()?;
171 Some((u16::MAX as u32 * value / max) as u16)
172 } else {
173 None
174 }
175}
176
177impl Color {
180 pub fn perceived_lightness(&self) -> f32 {
185 luminance_to_perceived_lightness(self.luminance()) / 100.
186 }
187
188 fn luminance(&self) -> f32 {
190 let r = gamma_function(f32::from(self.red) / f32::from(u16::MAX));
191 let g = gamma_function(f32::from(self.green) / f32::from(u16::MAX));
192 let b = gamma_function(f32::from(self.blue) / f32::from(u16::MAX));
193 0.2126 * r + 0.7152 * g + 0.0722 * b
194 }
195}
196
197fn gamma_function(value: f32) -> f32 {
200 if value <= 0.0 {
201 return value;
202 }
203 if value <= 0.04045 {
204 value / 12.92 } else {
206 ((value + 0.055) / 1.055).powf(2.4) }
208}
209
210fn luminance_to_perceived_lightness(luminance: f32) -> f32 {
212 if luminance <= 216. / 24389. {
213 luminance * (24389. / 27.)
214 } else {
215 luminance.cbrt() * 116. - 16.
216 }
217}
218
219#[cfg(doctest)]
220#[doc = include_str!("../readme.md")]
221pub mod readme_doctests {}
222
223#[cfg(test)]
224#[allow(clippy::unwrap_used)]
225mod tests {
226 use super::*;
227
228 #[test]
231 fn parses_valid_rgb_color() {
232 assert_eq!(
233 Color::parse(b"rgb:f/e/d").unwrap(),
234 Color {
235 red: 0xffff,
236 green: 0xeeee,
237 blue: 0xdddd,
238 alpha: u16::MAX,
239 }
240 );
241 assert_eq!(
242 Color::parse(b"rgb:11/aa/ff").unwrap(),
243 Color {
244 red: 0x1111,
245 green: 0xaaaa,
246 blue: 0xffff,
247 alpha: u16::MAX,
248 }
249 );
250 assert_eq!(
251 Color::parse(b"rgb:f/ed1/cb23").unwrap(),
252 Color {
253 red: 0xffff,
254 green: 0xed1d,
255 blue: 0xcb23,
256 alpha: u16::MAX,
257 }
258 );
259 assert_eq!(
260 Color::parse(b"rgb:ffff/0/0").unwrap(),
261 Color {
262 red: 0xffff,
263 green: 0x0,
264 blue: 0x0,
265 alpha: u16::MAX,
266 }
267 );
268 }
269
270 #[test]
271 fn parses_valid_rgba_color() {
272 assert_eq!(
273 Color::parse(b"rgba:0000/0000/4443/cccc").unwrap(),
274 Color {
275 red: 0x0000,
276 green: 0x0000,
277 blue: 0x4443,
278 alpha: 0xcccc,
279 }
280 );
281 }
282
283 #[test]
284 fn fails_for_invalid_rgb_color() {
285 assert!(Color::parse(b"rgb:").is_err()); assert!(Color::parse(b"rgb:f/f").is_err()); assert!(Color::parse(b"rgb:f/f/f/f").is_err()); assert!(Color::parse(b"rgb:f//f").is_err()); assert!(Color::parse(b"rgb:ffff/ffff/fffff").is_err()); }
291
292 #[test]
295 fn parses_valid_sharp_color() {
296 assert_eq!(
297 Color::parse(b"#1af").unwrap(),
298 Color {
299 red: 0x1000,
300 green: 0xa000,
301 blue: 0xf000,
302 alpha: u16::MAX,
303 }
304 );
305 assert_eq!(
306 Color::parse(b"#1AF").unwrap(),
307 Color {
308 red: 0x1000,
309 green: 0xa000,
310 blue: 0xf000,
311 alpha: u16::MAX,
312 }
313 );
314 assert_eq!(
315 Color::parse(b"#11aaff").unwrap(),
316 Color {
317 red: 0x1100,
318 green: 0xaa00,
319 blue: 0xff00,
320 alpha: u16::MAX,
321 }
322 );
323 assert_eq!(
324 Color::parse(b"#110aa0ff0").unwrap(),
325 Color {
326 red: 0x1100,
327 green: 0xaa00,
328 blue: 0xff00,
329 alpha: u16::MAX,
330 }
331 );
332 assert_eq!(
333 Color::parse(b"#1100aa00ff00").unwrap(),
334 Color {
335 red: 0x1100,
336 green: 0xaa00,
337 blue: 0xff00,
338 alpha: u16::MAX,
339 }
340 );
341 assert_eq!(
342 Color::parse(b"#123456789ABC").unwrap(),
343 Color {
344 red: 0x1234,
345 green: 0x5678,
346 blue: 0x9ABC,
347 alpha: u16::MAX,
348 }
349 );
350 }
351
352 #[test]
353 fn fails_for_invalid_sharp_color() {
354 assert!(Color::parse(b"#").is_err()); assert!(Color::parse(b"#1234").is_err()); assert!(Color::parse(b"#123456789ABCDEF").is_err()); }
358
359 #[test]
360 fn black_has_perceived_lightness_zero() {
361 let black = Color::rgb(0, 0, 0);
362 assert_eq!(0.0, black.perceived_lightness())
363 }
364
365 #[test]
366 fn white_has_perceived_lightness_one() {
367 let white = Color::rgb(u16::MAX, u16::MAX, u16::MAX);
368 assert_eq!(1.0, white.perceived_lightness())
369 }
370}