1use serde::de;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5use std::fmt;
6use std::str::FromStr;
7
8#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
47pub struct Rgba {
48 pub r: u8,
50 pub g: u8,
52 pub b: u8,
54 pub a: u8,
56}
57
58impl Rgba {
59 #[must_use]
61 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
62 Self { r, g, b, a: 255 }
63 }
64
65 #[must_use]
67 #[allow(clippy::self_named_constructors)]
68 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
69 Self { r, g, b, a }
70 }
71
72 #[must_use]
80 pub fn from_f32(r: f32, g: f32, b: f32, a: f32) -> Self {
81 Self {
82 r: (r.clamp(0.0, 1.0) * 255.0).round() as u8,
83 g: (g.clamp(0.0, 1.0) * 255.0).round() as u8,
84 b: (b.clamp(0.0, 1.0) * 255.0).round() as u8,
85 a: (a.clamp(0.0, 1.0) * 255.0).round() as u8,
86 }
87 }
88
89 #[must_use]
94 pub fn to_f32_array(&self) -> [f32; 4] {
95 [
96 self.r as f32 / 255.0,
97 self.g as f32 / 255.0,
98 self.b as f32 / 255.0,
99 self.a as f32 / 255.0,
100 ]
101 }
102
103 #[must_use]
105 pub fn to_f32_tuple(&self) -> (f32, f32, f32, f32) {
106 (
107 self.r as f32 / 255.0,
108 self.g as f32 / 255.0,
109 self.b as f32 / 255.0,
110 self.a as f32 / 255.0,
111 )
112 }
113}
114
115impl fmt::Display for Rgba {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 if self.a == 255 {
118 write!(f, "#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
119 } else {
120 write!(
121 f,
122 "#{:02x}{:02x}{:02x}{:02x}",
123 self.r, self.g, self.b, self.a
124 )
125 }
126 }
127}
128
129#[derive(Debug, Clone)]
135pub struct ParseColorError(String);
136
137impl fmt::Display for ParseColorError {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 f.write_str(&self.0)
140 }
141}
142
143impl std::error::Error for ParseColorError {}
144
145impl FromStr for Rgba {
146 type Err = ParseColorError;
147
148 fn from_str(s: &str) -> Result<Self, Self::Err> {
149 let hex = s.strip_prefix('#').unwrap_or(s);
150
151 if hex.is_empty() {
152 return Err(ParseColorError("empty hex color string".into()));
153 }
154
155 match hex.len() {
158 3 => {
160 let r = u8::from_str_radix(&hex[0..1], 16)
161 .map_err(|e| ParseColorError(format!("invalid red component: {e}")))?;
162 let g = u8::from_str_radix(&hex[1..2], 16)
163 .map_err(|e| ParseColorError(format!("invalid green component: {e}")))?;
164 let b = u8::from_str_radix(&hex[2..3], 16)
165 .map_err(|e| ParseColorError(format!("invalid blue component: {e}")))?;
166 Ok(Rgba::rgb(r * 17, g * 17, b * 17))
167 }
168 4 => {
170 let r = u8::from_str_radix(&hex[0..1], 16)
171 .map_err(|e| ParseColorError(format!("invalid red component: {e}")))?;
172 let g = u8::from_str_radix(&hex[1..2], 16)
173 .map_err(|e| ParseColorError(format!("invalid green component: {e}")))?;
174 let b = u8::from_str_radix(&hex[2..3], 16)
175 .map_err(|e| ParseColorError(format!("invalid blue component: {e}")))?;
176 let a = u8::from_str_radix(&hex[3..4], 16)
177 .map_err(|e| ParseColorError(format!("invalid alpha component: {e}")))?;
178 Ok(Rgba::rgba(r * 17, g * 17, b * 17, a * 17))
179 }
180 6 => {
182 let r = u8::from_str_radix(&hex[0..2], 16)
183 .map_err(|e| ParseColorError(format!("invalid red component: {e}")))?;
184 let g = u8::from_str_radix(&hex[2..4], 16)
185 .map_err(|e| ParseColorError(format!("invalid green component: {e}")))?;
186 let b = u8::from_str_radix(&hex[4..6], 16)
187 .map_err(|e| ParseColorError(format!("invalid blue component: {e}")))?;
188 Ok(Rgba::rgb(r, g, b))
189 }
190 8 => {
192 let r = u8::from_str_radix(&hex[0..2], 16)
193 .map_err(|e| ParseColorError(format!("invalid red component: {e}")))?;
194 let g = u8::from_str_radix(&hex[2..4], 16)
195 .map_err(|e| ParseColorError(format!("invalid green component: {e}")))?;
196 let b = u8::from_str_radix(&hex[4..6], 16)
197 .map_err(|e| ParseColorError(format!("invalid blue component: {e}")))?;
198 let a = u8::from_str_radix(&hex[6..8], 16)
199 .map_err(|e| ParseColorError(format!("invalid alpha component: {e}")))?;
200 Ok(Rgba::rgba(r, g, b, a))
201 }
202 other => Err(ParseColorError(format!(
203 "invalid hex color length {other}: expected 3, 4, 6, or 8 hex digits"
204 ))),
205 }
206 }
207}
208
209impl Serialize for Rgba {
210 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
211 serializer.serialize_str(&self.to_string())
212 }
213}
214
215impl<'de> Deserialize<'de> for Rgba {
216 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
217 let s = String::deserialize(deserializer)?;
218 Rgba::from_str(&s).map_err(de::Error::custom)
219 }
220}
221
222#[allow(dead_code)]
232pub(crate) fn unpremultiply_alpha(buffer: &mut [u8]) {
233 for pixel in buffer.chunks_exact_mut(4) {
234 let a = pixel[3] as u16;
235 if a > 0 && a < 255 {
236 pixel[0] = ((pixel[0] as u16 * 255) / a).min(255) as u8;
237 pixel[1] = ((pixel[1] as u16 * 255) / a).min(255) as u8;
238 pixel[2] = ((pixel[2] as u16 * 255) / a).min(255) as u8;
239 }
240 }
241}
242
243#[cfg(test)]
244#[allow(clippy::unwrap_used, clippy::expect_used)]
245mod tests {
246 use super::*;
247
248 #[test]
251 fn rgb_constructor_sets_alpha_255() {
252 let c = Rgba::rgb(61, 174, 233);
253 assert_eq!(
254 c,
255 Rgba {
256 r: 61,
257 g: 174,
258 b: 233,
259 a: 255
260 }
261 );
262 }
263
264 #[test]
265 fn rgba_constructor_sets_all_fields() {
266 let c = Rgba::rgba(61, 174, 233, 128);
267 assert_eq!(
268 c,
269 Rgba {
270 r: 61,
271 g: 174,
272 b: 233,
273 a: 128
274 }
275 );
276 }
277
278 #[test]
281 fn parse_6_digit_hex_with_hash() {
282 let c: Rgba = "#3daee9".parse().unwrap();
283 assert_eq!(c, Rgba::rgb(61, 174, 233));
284 }
285
286 #[test]
287 fn parse_8_digit_hex_with_hash() {
288 let c: Rgba = "#3daee980".parse().unwrap();
289 assert_eq!(c, Rgba::rgba(61, 174, 233, 128));
290 }
291
292 #[test]
293 fn parse_6_digit_hex_without_hash() {
294 let c: Rgba = "3daee9".parse().unwrap();
295 assert_eq!(c, Rgba::rgb(61, 174, 233));
296 }
297
298 #[test]
299 fn parse_3_digit_shorthand() {
300 let c: Rgba = "#abc".parse().unwrap();
301 assert_eq!(c, Rgba::rgb(0xaa, 0xbb, 0xcc));
302 }
303
304 #[test]
305 fn parse_4_digit_shorthand() {
306 let c: Rgba = "#abcd".parse().unwrap();
307 assert_eq!(c, Rgba::rgba(0xaa, 0xbb, 0xcc, 0xdd));
308 }
309
310 #[test]
311 fn parse_uppercase_hex() {
312 let c: Rgba = "#AABBCC".parse().unwrap();
313 assert_eq!(c, Rgba::rgb(0xaa, 0xbb, 0xcc));
314 }
315
316 #[test]
317 fn parse_empty_string_is_error() {
318 assert!("".parse::<Rgba>().is_err());
319 }
320
321 #[test]
322 fn parse_invalid_hex_chars_is_error() {
323 assert!("#gggggg".parse::<Rgba>().is_err());
324 }
325
326 #[test]
327 fn parse_invalid_length_5_chars_is_error() {
328 assert!("#12345".parse::<Rgba>().is_err());
329 }
330
331 #[test]
334 fn display_omits_alpha_when_255() {
335 assert_eq!(Rgba::rgb(61, 174, 233).to_string(), "#3daee9");
336 }
337
338 #[test]
339 fn display_includes_alpha_when_not_255() {
340 assert_eq!(Rgba::rgba(61, 174, 233, 128).to_string(), "#3daee980");
341 }
342
343 #[test]
346 fn serde_json_round_trip() {
347 let c = Rgba::rgb(61, 174, 233);
348 let json = serde_json::to_string(&c).unwrap();
349 assert_eq!(json, "\"#3daee9\"");
350 let deserialized: Rgba = serde_json::from_str(&json).unwrap();
351 assert_eq!(deserialized, c);
352 }
353
354 #[test]
355 fn serde_toml_round_trip() {
356 #[derive(Debug, PartialEq, Serialize, Deserialize)]
357 struct Wrapper {
358 color: Rgba,
359 }
360 let w = Wrapper {
361 color: Rgba::rgba(61, 174, 233, 128),
362 };
363 let toml_str = toml::to_string(&w).unwrap();
364 let deserialized: Wrapper = toml::from_str(&toml_str).unwrap();
365 assert_eq!(deserialized, w);
366 }
367
368 #[test]
371 fn to_f32_array_black() {
372 let arr = Rgba::rgb(0, 0, 0).to_f32_array();
373 assert_eq!(arr, [0.0, 0.0, 0.0, 1.0]);
374 }
375
376 #[test]
377 fn to_f32_array_white_transparent() {
378 let arr = Rgba::rgba(255, 255, 255, 0).to_f32_array();
379 assert_eq!(arr, [1.0, 1.0, 1.0, 0.0]);
380 }
381
382 #[test]
385 fn rgba_is_copy() {
386 let a = Rgba::rgb(1, 2, 3);
387 let b = a; assert_eq!(a, b); }
390
391 #[test]
392 fn rgba_default_is_transparent_black() {
393 let d = Rgba::default();
394 assert_eq!(
395 d,
396 Rgba {
397 r: 0,
398 g: 0,
399 b: 0,
400 a: 0
401 }
402 );
403 }
404
405 #[test]
406 fn rgba_is_hash() {
407 use std::collections::HashSet;
408 let mut set = HashSet::new();
409 set.insert(Rgba::rgb(1, 2, 3));
410 assert!(set.contains(&Rgba::rgb(1, 2, 3)));
411 }
412
413 #[test]
416 fn from_f32_basic() {
417 let c = Rgba::from_f32(1.0, 0.5, 0.0, 1.0);
418 assert_eq!(c.r, 255);
419 assert_eq!(c.g, 128); assert_eq!(c.b, 0);
421 assert_eq!(c.a, 255);
422 }
423
424 #[test]
425 fn from_f32_clamps_out_of_range() {
426 let c = Rgba::from_f32(-0.5, 1.5, 0.0, 0.0);
427 assert_eq!(c.r, 0);
428 assert_eq!(c.g, 255);
429 }
430
431 #[test]
434 fn to_f32_tuple_matches_array() {
435 let c = Rgba::rgb(128, 64, 32);
436 let arr = c.to_f32_array();
437 let tup = c.to_f32_tuple();
438 assert_eq!(tup, (arr[0], arr[1], arr[2], arr[3]));
439 }
440}