1use serde::de;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43pub struct Rgba {
44 pub r: u8,
46 pub g: u8,
48 pub b: u8,
50 pub a: u8,
52}
53
54impl Rgba {
62 #[must_use]
64 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
65 Self { r, g, b, a: 255 }
66 }
67
68 #[must_use]
70 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
71 Self { r, g, b, a }
72 }
73
74 pub const TRANSPARENT: Self = Self {
84 r: 0,
85 g: 0,
86 b: 0,
87 a: 0,
88 };
89
90 pub const BLACK: Self = Self {
92 r: 0,
93 g: 0,
94 b: 0,
95 a: 255,
96 };
97
98 pub const WHITE: Self = Self {
100 r: 255,
101 g: 255,
102 b: 255,
103 a: 255,
104 };
105
106 #[must_use]
114 pub fn from_f32(r: f32, g: f32, b: f32, a: f32) -> Self {
115 Self {
116 r: (r.clamp(0.0, 1.0) * 255.0).round() as u8,
117 g: (g.clamp(0.0, 1.0) * 255.0).round() as u8,
118 b: (b.clamp(0.0, 1.0) * 255.0).round() as u8,
119 a: (a.clamp(0.0, 1.0) * 255.0).round() as u8,
120 }
121 }
122
123 #[must_use]
128 pub fn to_f32_array(&self) -> [f32; 4] {
129 [
130 self.r as f32 / 255.0,
131 self.g as f32 / 255.0,
132 self.b as f32 / 255.0,
133 self.a as f32 / 255.0,
134 ]
135 }
136}
137
138impl fmt::Display for Rgba {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 if self.a == 255 {
141 write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
142 } else {
143 write!(
144 f,
145 "#{:02x}{:02x}{:02x}{:02x}",
146 self.r, self.g, self.b, self.a
147 )
148 }
149 }
150}
151
152#[derive(Debug, Clone)]
158pub struct ParseColorError(String);
159
160impl fmt::Display for ParseColorError {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 f.write_str(&self.0)
163 }
164}
165
166impl std::error::Error for ParseColorError {}
167
168fn hex_nibble(b: u8, label: &str) -> Result<u8, ParseColorError> {
170 char::from(b)
171 .to_digit(16)
172 .and_then(|v| u8::try_from(v).ok())
173 .ok_or_else(|| ParseColorError(format!("invalid {label} hex digit {:?}", char::from(b))))
174}
175
176fn hex_byte(hi: u8, lo: u8) -> u8 {
178 hi.wrapping_shl(4) | (lo & 0x0f)
180}
181
182fn double_nibble(n: u8) -> u8 {
185 n.wrapping_shl(4) | (n & 0x0f)
186}
187
188impl FromStr for Rgba {
189 type Err = ParseColorError;
190
191 fn from_str(s: &str) -> Result<Self, Self::Err> {
192 let hex = s.strip_prefix('#').unwrap_or(s);
193
194 if hex.is_empty() {
195 return Err(ParseColorError("empty hex color string".into()));
196 }
197
198 match hex.as_bytes() {
203 [r, g, b] => {
205 let r = hex_nibble(*r, "red")?;
206 let g = hex_nibble(*g, "green")?;
207 let b = hex_nibble(*b, "blue")?;
208 Ok(Rgba::rgb(
209 double_nibble(r),
210 double_nibble(g),
211 double_nibble(b),
212 ))
213 }
214 [r, g, b, a] => {
216 let r = hex_nibble(*r, "red")?;
217 let g = hex_nibble(*g, "green")?;
218 let b = hex_nibble(*b, "blue")?;
219 let a = hex_nibble(*a, "alpha")?;
220 Ok(Rgba::new(
221 double_nibble(r),
222 double_nibble(g),
223 double_nibble(b),
224 double_nibble(a),
225 ))
226 }
227 [r1, r2, g1, g2, b1, b2] => {
229 let r = hex_byte(hex_nibble(*r1, "red")?, hex_nibble(*r2, "red")?);
230 let g = hex_byte(hex_nibble(*g1, "green")?, hex_nibble(*g2, "green")?);
231 let b = hex_byte(hex_nibble(*b1, "blue")?, hex_nibble(*b2, "blue")?);
232 Ok(Rgba::rgb(r, g, b))
233 }
234 [r1, r2, g1, g2, b1, b2, a1, a2] => {
236 let r = hex_byte(hex_nibble(*r1, "red")?, hex_nibble(*r2, "red")?);
237 let g = hex_byte(hex_nibble(*g1, "green")?, hex_nibble(*g2, "green")?);
238 let b = hex_byte(hex_nibble(*b1, "blue")?, hex_nibble(*b2, "blue")?);
239 let a = hex_byte(hex_nibble(*a1, "alpha")?, hex_nibble(*a2, "alpha")?);
240 Ok(Rgba::new(r, g, b, a))
241 }
242 other => Err(ParseColorError(format!(
243 "invalid hex color length {}: expected 3, 4, 6, or 8 hex digits",
244 other.len()
245 ))),
246 }
247 }
248}
249
250impl Serialize for Rgba {
251 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
252 serializer.serialize_str(&self.to_string())
253 }
254}
255
256impl<'de> Deserialize<'de> for Rgba {
257 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
258 let s = String::deserialize(deserializer)?;
259 Rgba::from_str(&s).map_err(de::Error::custom)
260 }
261}
262
263#[allow(dead_code)]
273pub(crate) fn unpremultiply_alpha(buffer: &mut [u8]) {
274 for pixel in buffer.as_chunks_mut::<4>().0 {
279 let [r, g, b, a] = pixel;
280 let a_val = u16::from(*a);
281 if (1..255).contains(&a_val) {
285 #[allow(clippy::integer_division, clippy::arithmetic_side_effects)]
286 {
287 *r = (u16::from(*r).saturating_mul(255) / a_val).min(255) as u8;
288 *g = (u16::from(*g).saturating_mul(255) / a_val).min(255) as u8;
289 *b = (u16::from(*b).saturating_mul(255) / a_val).min(255) as u8;
290 }
291 }
292 }
293}
294
295#[allow(dead_code)]
303pub(crate) fn tint_monochrome(buffer: &mut [u8], rgb: Option<[u8; 3]>) {
304 let Some([r, g, b]) = rgb else {
305 return;
306 };
307 for pixel in buffer.as_chunks_mut::<4>().0 {
308 let [_, _, _, a] = *pixel;
309 *pixel = [r, g, b, a];
310 }
311}
312
313#[cfg(test)]
314#[allow(clippy::unwrap_used, clippy::expect_used)]
315mod tests {
316 use super::*;
317
318 #[test]
321 fn rgb_constructor_sets_alpha_255() {
322 let c = Rgba::rgb(61, 174, 233);
323 assert_eq!(
324 c,
325 Rgba {
326 r: 61,
327 g: 174,
328 b: 233,
329 a: 255
330 }
331 );
332 }
333
334 #[test]
335 fn rgba_constructor_sets_all_fields() {
336 let c = Rgba::new(61, 174, 233, 128);
337 assert_eq!(
338 c,
339 Rgba {
340 r: 61,
341 g: 174,
342 b: 233,
343 a: 128
344 }
345 );
346 }
347
348 #[test]
351 fn parse_6_digit_hex_with_hash() {
352 let c: Rgba = "#3daee9".parse().unwrap();
353 assert_eq!(c, Rgba::rgb(61, 174, 233));
354 }
355
356 #[test]
357 fn parse_8_digit_hex_with_hash() {
358 let c: Rgba = "#3daee980".parse().unwrap();
359 assert_eq!(c, Rgba::new(61, 174, 233, 128));
360 }
361
362 #[test]
363 fn parse_6_digit_hex_without_hash() {
364 let c: Rgba = "3daee9".parse().unwrap();
365 assert_eq!(c, Rgba::rgb(61, 174, 233));
366 }
367
368 #[test]
369 fn parse_3_digit_shorthand() {
370 let c: Rgba = "#abc".parse().unwrap();
371 assert_eq!(c, Rgba::rgb(0xaa, 0xbb, 0xcc));
372 }
373
374 #[test]
375 fn parse_4_digit_shorthand() {
376 let c: Rgba = "#abcd".parse().unwrap();
377 assert_eq!(c, Rgba::new(0xaa, 0xbb, 0xcc, 0xdd));
378 }
379
380 #[test]
381 fn parse_uppercase_hex() {
382 let c: Rgba = "#AABBCC".parse().unwrap();
383 assert_eq!(c, Rgba::rgb(0xaa, 0xbb, 0xcc));
384 }
385
386 #[test]
387 fn parse_empty_string_is_error() {
388 assert!("".parse::<Rgba>().is_err());
389 }
390
391 #[test]
392 fn parse_invalid_hex_chars_is_error() {
393 assert!("#gggggg".parse::<Rgba>().is_err());
394 }
395
396 #[test]
397 fn parse_invalid_length_5_chars_is_error() {
398 assert!("#12345".parse::<Rgba>().is_err());
399 }
400
401 #[test]
404 fn display_omits_alpha_when_255() {
405 assert_eq!(Rgba::rgb(61, 174, 233).to_string(), "#3daee9");
406 }
407
408 #[test]
409 fn display_includes_alpha_when_not_255() {
410 assert_eq!(Rgba::new(61, 174, 233, 128).to_string(), "#3daee980");
411 }
412
413 #[test]
416 fn serde_json_round_trip() {
417 let c = Rgba::rgb(61, 174, 233);
418 let json = serde_json::to_string(&c).unwrap();
419 assert_eq!(json, "\"#3daee9\"");
420 let deserialized: Rgba = serde_json::from_str(&json).unwrap();
421 assert_eq!(deserialized, c);
422 }
423
424 #[test]
425 fn serde_toml_round_trip() {
426 #[derive(Debug, PartialEq, Serialize, Deserialize)]
427 struct Wrapper {
428 color: Rgba,
429 }
430 let w = Wrapper {
431 color: Rgba::new(61, 174, 233, 128),
432 };
433 let toml_str = toml::to_string(&w).unwrap();
434 let deserialized: Wrapper = toml::from_str(&toml_str).unwrap();
435 assert_eq!(deserialized, w);
436 }
437
438 #[test]
441 fn to_f32_array_black() {
442 let arr = Rgba::rgb(0, 0, 0).to_f32_array();
443 assert_eq!(arr, [0.0, 0.0, 0.0, 1.0]);
444 }
445
446 #[test]
447 fn to_f32_array_white_transparent() {
448 let arr = Rgba::new(255, 255, 255, 0).to_f32_array();
449 assert_eq!(arr, [1.0, 1.0, 1.0, 0.0]);
450 }
451
452 #[test]
455 fn rgba_is_copy() {
456 let a = Rgba::rgb(1, 2, 3);
457 let b = a; assert_eq!(a, b); }
460
461 #[test]
462 fn rgba_is_hash() {
463 use std::collections::HashSet;
464 let mut set = HashSet::new();
465 set.insert(Rgba::rgb(1, 2, 3));
466 assert!(set.contains(&Rgba::rgb(1, 2, 3)));
467 }
468
469 #[test]
472 fn from_f32_basic() {
473 let c = Rgba::from_f32(1.0, 0.5, 0.0, 1.0);
474 assert_eq!(c.r, 255);
475 assert_eq!(c.g, 128); assert_eq!(c.b, 0);
477 assert_eq!(c.a, 255);
478 }
479
480 #[test]
481 fn from_f32_clamps_out_of_range() {
482 let c = Rgba::from_f32(-0.5, 1.5, 0.0, 0.0);
483 assert_eq!(c.r, 0);
484 assert_eq!(c.g, 255);
485 }
486
487 #[test]
490 fn tint_monochrome_colours_a_white_glyph_and_keeps_alpha() {
491 let mut buf = [255u8, 255, 255, 255, 255, 255, 255, 127];
492 tint_monochrome(&mut buf, Some([10, 20, 30]));
493 assert_eq!(buf, [10, 20, 30, 255, 10, 20, 30, 127]);
494 }
495
496 #[test]
497 fn tint_monochrome_keeps_a_transparent_pixel_transparent() {
498 let mut buf = [0u8, 0, 0, 0];
499 tint_monochrome(&mut buf, Some([10, 20, 30]));
500 assert_eq!(buf[3], 0);
501 }
502
503 #[test]
504 fn tint_monochrome_without_a_colour_leaves_the_bytes() {
505 let original = [255u8, 255, 255, 127, 0, 0, 0, 255, 1, 2, 3, 0];
506 let mut buf = original;
507 tint_monochrome(&mut buf, None);
508 assert_eq!(buf, original);
509 }
510}