1use crate::color_table::COLOR_LIB;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct RgbaColor {
11 pub r: u8,
12 pub g: u8,
13 pub b: u8,
14 pub a: u8,
15}
16
17impl Default for RgbaColor {
18 #[inline]
19 fn default() -> Self {
20 Self::BLACK
21 }
22}
23
24impl RgbaColor {
25 pub const BLACK: Self = Self::rgb(0, 0, 0);
27 pub const WHITE: Self = Self::rgb(255, 255, 255);
29 pub const TRANSPARENT: Self = Self::rgba(255, 255, 254, 0);
31
32 #[inline]
34 pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
35 Self { r, g, b, a }
36 }
37
38 #[inline]
40 pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
41 Self { r, g, b, a: 255 }
42 }
43
44 #[inline]
46 pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self {
47 Self { r, g, b, a }
48 }
49
50 #[inline]
52 pub const fn is_transparent(&self) -> bool {
53 self.a == 0
54 }
55
56 #[inline]
58 pub const fn is_opaque(&self) -> bool {
59 self.a == 255
60 }
61
62 #[inline]
64 pub fn opacity(&self) -> f64 {
65 f64::from(self.a) / 255.0
66 }
67
68 pub fn to_hex_rgb(&self) -> String {
70 format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
71 }
72
73 pub fn to_hex_rgba(&self) -> String {
75 format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
76 }
77
78 pub fn to_svg_hex(&self) -> String {
83 if self.a == 0 {
84 "none".to_string()
85 } else if self.a == 255 {
86 self.to_hex_rgb()
87 } else {
88 self.to_hex_rgba()
89 }
90 }
91
92 pub fn to_css_rgba(&self) -> String {
94 if self.a == 0 {
95 "rgba(0,0,0,0)".to_string()
96 } else if self.a == 255 {
97 format!("rgb({},{},{})", self.r, self.g, self.b)
98 } else {
99 let alpha = (f64::from(self.a) / 255.0 * 1000.0).round() / 1000.0;
100 format!("rgba({},{},{},{})", self.r, self.g, self.b, alpha)
101 }
102 }
103
104 #[inline]
106 pub fn to_svg_string(&self) -> String {
107 self.to_svg_hex()
108 }
109
110 pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
113 Self::from_hsva(h, s, v, 1.0)
114 }
115
116 pub fn from_hsva(h: f64, s: f64, v: f64, a: f64) -> Self {
119 let unit = |x: f64| if x.is_nan() { 1.0 } else { x.clamp(0.0, 1.0) };
121 let (h, s, v, a) = (unit(h), unit(s), unit(v), unit(a));
122 let (r, g, b) = hsv2rgb(h, s, v);
123 Self {
124 r: (r * 255.0) as u8,
125 g: (g * 255.0) as u8,
126 b: (b * 255.0) as u8,
127 a: (a * 255.0) as u8,
128 }
129 }
130
131 pub fn to_hsva(&self) -> (f64, f64, f64, f64) {
134 let r = f64::from(self.r) / 255.0;
135 let g = f64::from(self.g) / 255.0;
136 let b = f64::from(self.b) / 255.0;
137 let a = f64::from(self.a) / 255.0;
138 let (h, s, v) = rgb2hsv(r, g, b);
139 (h, s, v, a)
140 }
141}
142
143impl fmt::Display for RgbaColor {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 f.write_str(&self.to_svg_hex())
146 }
147}
148
149pub fn hsv2rgb(mut h: f64, s: f64, v: f64) -> (f64, f64, f64) {
151 if s <= 0.0 {
152 (v, v, v)
153 } else {
154 if h >= 1.0 {
155 h = 0.0;
156 }
157 h *= 6.0;
158 let i = h as i32;
159 let f = h - f64::from(i);
160 let p = v * (1.0 - s);
161 let q = v * (1.0 - s * f);
162 let t = v * (1.0 - s * (1.0 - f));
163 match i {
164 0 => (v, t, p),
165 1 => (q, v, p),
166 2 => (p, v, t),
167 3 => (p, q, v),
168 4 => (t, p, v),
169 _ => (v, p, q),
170 }
171 }
172}
173
174pub fn rgb2hsv(r: f64, g: f64, b: f64) -> (f64, f64, f64) {
176 let rgbmin = r.min(g.min(b));
177 let rgbmax = r.max(g.max(b));
178 let st = if rgbmax > 0.0 {
179 (rgbmax - rgbmin) / rgbmax
180 } else {
181 0.0
182 };
183 let mut ht = 0.0;
184 if st > 0.0 {
185 let denom = rgbmax - rgbmin;
186 let rc = (rgbmax - r) / denom;
187 let gc = (rgbmax - g) / denom;
188 let bc = (rgbmax - b) / denom;
189 if (r - rgbmax).abs() == 0.0 {
190 ht = bc - gc;
191 } else if (g - rgbmax).abs() == 0.0 {
192 ht = 2.0 + rc - bc;
193 } else if (b - rgbmax).abs() == 0.0 {
194 ht = 4.0 + gc - rc;
195 }
196 ht *= 60.0;
197 if ht < 0.0 {
198 ht += 360.0;
199 }
200 }
201 (ht / 360.0, st, rgbmax)
202}
203
204pub fn resolve_color(spec: &str) -> Option<RgbaColor> {
207 resolve_color_with_scheme(spec, None)
208}
209
210pub fn resolve_color_with_scheme(spec: &str, colorscheme: Option<&str>) -> Option<RgbaColor> {
214 let p = spec.trim_start_matches(' ');
215 if p.is_empty() {
216 return None;
217 }
218
219 if let Some(color) = parse_hex_color(p) {
221 return Some(color);
222 }
223
224 let first = p.as_bytes()[0];
226 if (first == b'.' || first.is_ascii_digit())
227 && let Some(hsv_color) = parse_hsv_color(p)
228 {
229 return Some(hsv_color);
230 }
231
232 let resolved_name = normalize_color_scheme_name(p, colorscheme);
234 lookup_named_color(&resolved_name)
235}
236
237pub fn warn_unknown_color(name: &str) {
240 use std::collections::HashSet;
241 use std::sync::{Mutex, OnceLock};
242 static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
243 let seen = SEEN.get_or_init(|| Mutex::new(HashSet::new()));
244 if let Ok(mut seen) = seen.lock()
245 && seen.insert(name.to_string())
246 {
247 eprintln!("Warning: {name} is not a known color.");
248 }
249}
250
251pub fn format_svg_color(spec: &str) -> String {
254 resolve_color(spec).unwrap_or(RgbaColor::BLACK).to_svg_hex()
255}
256
257fn scan_hex(s: &str, max: usize) -> Option<(u32, &str)> {
259 let s = s.trim_start();
260 let n = s
261 .bytes()
262 .take(max)
263 .take_while(u8::is_ascii_hexdigit)
264 .count();
265 if n == 0 {
266 return None;
267 }
268 Some((u32::from_str_radix(&s[..n], 16).ok()?, &s[n..]))
269}
270
271fn parse_hex_color(p: &str) -> Option<RgbaColor> {
272 let rest = p.strip_prefix('#')?;
273 let mut vals = [0u32, 0, 0, 255];
274 let mut cur = rest;
275 let mut count = 0;
276 for slot in vals.iter_mut() {
277 let Some((v, next)) = scan_hex(cur, 2) else {
278 break;
279 };
280 *slot = v;
281 cur = next;
282 count += 1;
283 }
284 if count >= 3 {
285 return Some(RgbaColor::rgba(
286 vals[0] as u8,
287 vals[1] as u8,
288 vals[2] as u8,
289 vals[3] as u8,
290 ));
291 }
292 if p.len() == 4 {
293 let (mut r, rest) = scan_hex(rest, 1)?;
294 let (mut g, rest) = scan_hex(rest, 1)?;
295 let (mut b, _) = scan_hex(rest, 1)?;
296 r |= r << 4;
297 g |= g << 4;
298 b |= b << 4;
299 return Some(RgbaColor::rgb(r as u8, g as u8, b as u8));
300 }
301 None
302}
303
304fn parse_hsv_color(p: &str) -> Option<RgbaColor> {
305 let canon: String = p.chars().map(|c| if c == ',' { ' ' } else { c }).collect();
308 let mut nums = [0.0_f64, 0.0, 0.0, 1.0];
309 let mut rest = canon.as_str();
310 let mut count = 0;
311 while count < 4 {
312 let Some((v, r)) = scan_lf(rest) else { break };
313 nums[count] = v;
314 rest = r;
315 count += 1;
316 }
317 if count >= 3 {
318 Some(RgbaColor::from_hsva(nums[0], nums[1], nums[2], nums[3]))
319 } else {
320 None
321 }
322}
323
324fn scan_lf(s: &str) -> Option<(f64, &str)> {
326 let t = s.trim_start_matches([' ', '\t', '\n', '\r', '\x0b', '\x0c']);
327 let b = t.as_bytes();
328 let mut i = usize::from(matches!(b.first(), Some(b'+' | b'-')));
329 let lower = t[i..]
330 .get(..8.min(t.len() - i))
331 .unwrap_or("")
332 .to_ascii_lowercase();
333 let neg = b.first() == Some(&b'-');
334 for (word, v) in [
335 ("infinity", f64::INFINITY),
336 ("inf", f64::INFINITY),
337 ("nan", f64::NAN),
338 ] {
339 if lower.starts_with(word) {
340 let v = if neg { -v } else { v };
341 return Some((v, &t[i + word.len()..]));
342 }
343 }
344 let start = i;
345 while i < b.len() && b[i].is_ascii_digit() {
346 i += 1;
347 }
348 let mut digits = i - start;
349 if i < b.len() && b[i] == b'.' {
350 let f = i + 1;
351 let mut j = f;
352 while j < b.len() && b[j].is_ascii_digit() {
353 j += 1;
354 }
355 digits += j - f;
356 if digits > 0 {
357 i = j;
358 }
359 }
360 if digits == 0 {
361 return None;
362 }
363 if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
364 let mut j = i + 1;
365 if j < b.len() && (b[j] == b'+' || b[j] == b'-') {
366 j += 1;
367 }
368 let e = j;
369 while j < b.len() && b[j].is_ascii_digit() {
370 j += 1;
371 }
372 if j > e {
373 i = j;
374 }
375 }
376 let text = t[..i].strip_suffix('.').unwrap_or(&t[..i]);
377 Some((text.parse().unwrap_or(0.0), &t[i..]))
378}
379
380fn normalize_color_scheme_name(str_in: &str, colorscheme: Option<&str>) -> String {
382 let lower = str_in.to_ascii_lowercase();
383 if lower == "black" || lower == "white" || lower == "lightgrey" {
384 return lower;
385 }
386
387 let is_non_dflt = |scheme: Option<&str>| -> Option<String> {
388 let s = scheme?;
389 if s.is_empty() || s.get(..3).is_some_and(|p| p.eq_ignore_ascii_case("x11")) {
390 None
391 } else {
392 Some(s.to_ascii_lowercase())
393 }
394 };
395
396 if let Some(c2) = lower.strip_prefix('/') {
397 if let Some(slash_idx) = c2.find('/') {
398 if let Some(after_double) = c2.strip_prefix('/') {
399 if let Some(scheme) = is_non_dflt(colorscheme) {
400 format!("/{scheme}/{after_double}")
401 } else {
402 after_double.to_string()
403 }
404 } else if c2.starts_with("x11/") {
405 c2[slash_idx + 1..].to_string()
406 } else {
407 lower
408 }
409 } else {
410 c2.to_string()
411 }
412 } else if let Some(scheme) = is_non_dflt(colorscheme) {
413 format!("/{scheme}/{lower}")
414 } else {
415 lower
416 }
417}
418
419fn lookup_named_color(name: &str) -> Option<RgbaColor> {
420 let key = name.to_ascii_lowercase();
421 let idx = COLOR_LIB
422 .binary_search_by(|(n, _)| (*n).cmp(key.as_str()))
423 .ok()?;
424 let [r, g, b, a] = COLOR_LIB[idx].1;
425 Some(RgbaColor::rgba(r, g, b, a))
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_named_colors_table_is_sorted() {
434 for window in COLOR_LIB.windows(2) {
435 assert!(
436 window[0].0 < window[1].0,
437 "{:?} >= {:?}",
438 window[0].0,
439 window[1].0
440 );
441 }
442 }
443
444 #[test]
445 fn test_x11_svg_named_colors() {
446 assert_eq!(
447 resolve_color("lightblue"),
448 Some(RgbaColor::rgb(173, 216, 230))
449 );
450 assert_eq!(
451 resolve_color("lightgrey"),
452 Some(RgbaColor::rgb(211, 211, 211))
453 );
454 assert_eq!(
455 resolve_color("LightGray"),
456 Some(RgbaColor::rgb(211, 211, 211))
457 );
458 assert_eq!(resolve_color("red"), Some(RgbaColor::rgb(255, 0, 0)));
459 assert_eq!(resolve_color("gold"), Some(RgbaColor::rgb(255, 215, 0)));
460 assert_eq!(
461 resolve_color("darkslategray"),
462 Some(RgbaColor::rgb(47, 79, 79))
463 );
464 assert_eq!(
465 resolve_color("darkslategrey"),
466 Some(RgbaColor::rgb(47, 79, 79))
467 );
468 assert_eq!(
469 resolve_color("transparent"),
470 Some(RgbaColor::rgba(255, 255, 254, 0))
471 );
472 assert_eq!(resolve_color("none").unwrap().to_svg_hex(), "none");
473 assert!(resolve_color("invis").unwrap().is_transparent());
474 }
475
476 #[test]
477 fn test_hex_colors() {
478 assert_eq!(resolve_color("#ff0000"), Some(RgbaColor::rgb(255, 0, 0)));
479 assert_eq!(
480 resolve_color("#ff000080"),
481 Some(RgbaColor::rgba(255, 0, 0, 128))
482 );
483 assert_eq!(
484 resolve_color("#f0a"),
485 Some(RgbaColor::rgb(0xff, 0x00, 0xaa))
486 );
487 assert_eq!(resolve_color("#1234"), None);
489 assert_eq!(resolve_color("#add8e6").unwrap().to_svg_hex(), "#add8e6");
490 assert_eq!(
491 resolve_color("#ff000080").unwrap().to_svg_hex(),
492 "#ff000080"
493 );
494 assert_eq!(
495 resolve_color("#12345"),
496 Some(RgbaColor::rgba(0x12, 0x34, 0x5, 255))
497 );
498 }
499
500 #[test]
501 fn test_hsv_colors() {
502 assert_eq!(
504 resolve_color("0.0,1.0,1.0"),
505 Some(RgbaColor::rgb(255, 0, 0))
506 );
507 let green = resolve_color("0.333333, 1.0, 1.0").unwrap();
509 assert_eq!(green.g, 255);
510 assert!(green.r <= 1 && green.b == 0);
511 let semi = resolve_color("0.0 0.0 0.5 0.5").unwrap();
513 assert_eq!(semi, RgbaColor::rgba(127, 127, 127, 127));
514 }
515
516 #[test]
517 fn test_full_tables_match_c() {
518 let hex = |spec: &str, scheme: Option<&str>| {
520 resolve_color_with_scheme(spec, scheme)
521 .unwrap_or_else(|| panic!("{spec} unresolved"))
522 .to_hex_rgb()
523 };
524 assert_eq!(hex("/blues9/5", None), "#6baed6");
525 assert_eq!(hex("/set312/7", None), "#b3de69");
526 assert_eq!(hex("/rdylgn11/3", None), "#f46d43");
527 assert_eq!(hex("/svg/aliceblue", None), "#f0f8ff");
528 assert_eq!(hex("/svg/yellowgreen", None), "#9acd32");
529 assert_eq!(hex("/x11/navy", None), "#000080");
530 assert_eq!(hex("5", Some("blues9")), "#6baed6");
531 assert_eq!(hex("//3", Some("blues9")), "#c6dbef");
532 assert_eq!(hex("navy", Some("X11")), "#000080");
533 }
534
535 #[test]
536 fn test_unknown_colors_are_not_resolved() {
537 assert_eq!(resolve_color("nosuchcolor"), None);
538 assert_eq!(resolve_color("/blues9/10"), None);
539 assert_eq!(resolve_color("9"), None);
540 assert_eq!(resolve_color_with_scheme("9", Some("blues3")), None);
541 assert_eq!(resolve_color_with_scheme("red", Some("blues9")), None);
542 assert_eq!(resolve_color("gray101"), None);
543 assert_eq!(resolve_color("#ffff"), None);
544 }
545}