1use compact_str::CompactString;
11
12use crate::errors::{Result, RichError};
13use crate::markup::py_repr_str;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
21pub enum ColorSystem {
22 Standard,
24 EightBit,
26 Truecolor,
28 Windows,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
34pub enum ColorType {
35 Default,
37 Standard,
39 EightBit,
41 Truecolor,
43 Windows,
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
49pub struct ColorTriplet {
50 pub red: u8,
51 pub green: u8,
52 pub blue: u8,
53}
54
55impl ColorTriplet {
56 pub const fn new(red: u8, green: u8, blue: u8) -> Self {
57 Self { red, green, blue }
58 }
59
60 pub fn hex(&self) -> String {
67 self.hex_name().into()
68 }
69
70 pub(crate) fn hex_name(&self) -> CompactString {
72 const DIGITS: &[u8; 16] = b"0123456789abcdef";
73 let mut hex = *b"#000000";
76 for (i, value) in [self.red, self.green, self.blue].into_iter().enumerate() {
77 hex[1 + 2 * i] = DIGITS[usize::from(value >> 4)];
78 hex[2 + 2 * i] = DIGITS[usize::from(value & 0xf)];
79 }
80 CompactString::from_utf8(hex).unwrap_or_default()
82 }
83
84 pub fn rgb(&self) -> String {
86 format!("rgb({},{},{})", self.red, self.green, self.blue)
87 }
88
89 pub fn normalized(&self) -> (f64, f64, f64) {
92 (
93 self.red as f64 / 255.0,
94 self.green as f64 / 255.0,
95 self.blue as f64 / 255.0,
96 )
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub struct Color {
103 pub name: crate::CompactString,
107 pub kind: ColorType,
108 pub number: Option<u8>,
110 pub triplet: Option<ColorTriplet>,
112}
113
114impl Color {
115 pub fn is_default(&self) -> bool {
117 self.kind == ColorType::Default
118 }
119
120 pub fn default_color() -> Self {
122 Color {
123 name: CompactString::const_new("default"),
124 kind: ColorType::Default,
125 number: None,
126 triplet: None,
127 }
128 }
129
130 fn named(name: &str, number: u8) -> Self {
133 let kind = if number < 16 {
134 ColorType::Standard
135 } else {
136 ColorType::EightBit
137 };
138 Color {
139 name: name.into(),
140 kind,
141 number: Some(number),
142 triplet: None,
143 }
144 }
145
146 pub fn from_ansi(number: u8) -> Self {
149 Color {
150 name: compact_str::format_compact!("color({number})"),
151 kind: if number < 16 {
152 ColorType::Standard
153 } else {
154 ColorType::EightBit
155 },
156 number: Some(number),
157 triplet: None,
158 }
159 }
160
161 pub fn from_rgb(red: u8, green: u8, blue: u8) -> Self {
164 let triplet = ColorTriplet::new(red, green, blue);
165 Color {
166 name: triplet.hex_name(),
167 kind: ColorType::Truecolor,
168 number: None,
169 triplet: Some(triplet),
170 }
171 }
172
173 pub fn parse(color: &str) -> Result<Self> {
180 let original = color;
181 let color = color.to_lowercase();
182 let color = color.trim();
183 let invalid =
184 || RichError::ColorParse(format!("{} is not a valid color", py_repr_str(original)));
185
186 if color == "default" {
187 return Ok(Color::default_color());
188 }
189 if let Some(number) = crate::color_names::ansi_color_number(color) {
190 return Ok(Color::named(color, number));
191 }
192 if let Some(hex) = color.strip_prefix('#') {
195 if hex.len() != 6 || !hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
196 return Err(invalid());
197 }
198 return Ok(Color {
199 name: color.into(),
200 kind: ColorType::Truecolor,
201 number: None,
202 triplet: Some(parse_rgb_hex(hex)),
203 });
204 }
205 if let Some(digits) = color
206 .strip_prefix("color(")
207 .and_then(|s| s.strip_suffix(')'))
208 {
209 if digits.is_empty() || digits.len() > 3 || !digits.bytes().all(|b| b.is_ascii_digit())
210 {
211 return Err(invalid());
212 }
213 let number: u16 = digits.parse().map_err(|_| invalid())?;
214 if number > 255 {
215 return Err(RichError::ColorParse(format!(
216 "color number must be <= 255 in {}",
217 py_repr_str(color)
218 )));
219 }
220 return Ok(Color {
221 name: color.into(),
222 kind: if number < 16 {
223 ColorType::Standard
224 } else {
225 ColorType::EightBit
226 },
227 number: Some(number as u8),
228 triplet: None,
229 });
230 }
231 if let Some(inner) = color.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
232 if inner.is_empty()
233 || !inner
234 .chars()
235 .all(|c| c.is_ascii_digit() || c == ',' || c.is_whitespace())
236 {
237 return Err(invalid());
238 }
239 let components: Vec<&str> = inner.split(',').collect();
240 if components.len() != 3 {
241 return Err(RichError::ColorParse(format!(
242 "expected three components in {}",
243 py_repr_str(original)
244 )));
245 }
246 let mut values = [0u8; 3];
247 for (slot, component) in values.iter_mut().zip(&components) {
248 let digits = component.trim();
250 if digits.is_empty() || digits.contains(char::is_whitespace) {
251 return Err(RichError::ColorParse(format!(
252 "invalid literal for int() with base 10: {}",
253 py_repr_str(component)
254 )));
255 }
256 match digits.parse::<u16>() {
257 Ok(value) if value <= 255 => *slot = value as u8,
258 _ => {
259 return Err(RichError::ColorParse(format!(
260 "color components must be <= 255 in {}",
261 py_repr_str(original)
262 )))
263 }
264 }
265 }
266 return Ok(Color {
267 name: color.into(),
268 kind: ColorType::Truecolor,
269 number: None,
270 triplet: Some(ColorTriplet::new(values[0], values[1], values[2])),
271 });
272 }
273 Err(invalid())
274 }
275
276 pub fn from_triplet(triplet: ColorTriplet) -> Self {
279 Color {
280 name: triplet.hex_name(),
281 kind: ColorType::Truecolor,
282 number: None,
283 triplet: Some(triplet),
284 }
285 }
286
287 pub fn system(&self) -> ColorSystem {
290 match self.kind {
291 ColorType::Default | ColorType::Standard => ColorSystem::Standard,
292 ColorType::EightBit => ColorSystem::EightBit,
293 ColorType::Truecolor => ColorSystem::Truecolor,
294 ColorType::Windows => ColorSystem::Windows,
295 }
296 }
297
298 pub fn is_system_defined(&self) -> bool {
301 !matches!(
302 self.system(),
303 ColorSystem::EightBit | ColorSystem::Truecolor
304 )
305 }
306
307 pub fn get_truecolor_for(
312 &self,
313 theme: Option<&crate::terminal_theme::TerminalTheme>,
314 foreground: bool,
315 ) -> ColorTriplet {
316 let theme = theme.unwrap_or(&crate::terminal_theme::DEFAULT_TERMINAL_THEME);
317 theme.resolve(self, foreground)
318 }
319
320 pub fn rich_text(&self) -> crate::text::Text {
323 let kind = match self.kind {
324 ColorType::Default => "default",
325 ColorType::Standard => "standard",
326 ColorType::EightBit => "eight_bit",
327 ColorType::Truecolor => "truecolor",
328 ColorType::Windows => "windows",
329 };
330 let mut text =
331 crate::text::Text::new(format!("<color {} ({kind})", py_repr_str(&self.name)));
332 let dot = crate::style::Style::from_color(Some(self.clone()), None);
333 text.append("⬤", Some(dot.into()));
334 text.append(" >", None);
335 text
336 }
337
338 pub fn get_truecolor(&self) -> Option<ColorTriplet> {
342 match self.kind {
343 ColorType::Default => None,
344 ColorType::Truecolor => self.triplet,
345 ColorType::Standard => self.number.map(|n| ANSI_BASE_PALETTE[n as usize]),
346 ColorType::Windows => self.number.map(|n| WINDOWS_PALETTE[(n & 15) as usize]),
348 ColorType::EightBit => self.number.map(eight_bit_triplet),
349 }
350 }
351
352 pub fn ansi_codes(&self, foreground: bool) -> Vec<String> {
356 match self.kind {
357 ColorType::Default => vec![if foreground { "39" } else { "49" }.to_string()],
358 ColorType::Windows | ColorType::Standard => {
359 let number = self.number.unwrap_or(0);
360 let (fore, back) = if number < 8 { (30, 40) } else { (82, 92) };
361 vec![(if foreground { fore } else { back } + number as u32).to_string()]
362 }
363 ColorType::EightBit => {
364 let number = self.number.unwrap_or(0);
365 vec![
366 if foreground { "38" } else { "48" }.to_string(),
367 "5".to_string(),
368 number.to_string(),
369 ]
370 }
371 ColorType::Truecolor => {
372 let t = self.triplet.unwrap_or(ColorTriplet::new(0, 0, 0));
373 vec![
374 if foreground { "38" } else { "48" }.to_string(),
375 "2".to_string(),
376 t.red.to_string(),
377 t.green.to_string(),
378 t.blue.to_string(),
379 ]
380 }
381 }
382 }
383
384 pub fn downgrade(&self, system: ColorSystem) -> Color {
390 let same_system = matches!(
391 (self.kind, system),
392 (ColorType::Standard, ColorSystem::Standard)
393 | (ColorType::EightBit, ColorSystem::EightBit)
394 | (ColorType::Truecolor, ColorSystem::Truecolor)
395 | (ColorType::Windows, ColorSystem::Windows)
396 );
397 if self.kind == ColorType::Default || same_system {
398 return self.clone();
399 }
400 let is_truecolor = self.kind == ColorType::Truecolor;
401 let source_triplet = || {
404 if is_truecolor {
405 self.triplet
406 } else {
407 self.number.map(eight_bit_triplet)
408 }
409 };
410 match system {
411 ColorSystem::EightBit if is_truecolor => {
412 let Some(triplet) = self.triplet else {
413 return self.clone();
414 };
415 Color {
416 name: self.name.clone(),
417 kind: ColorType::EightBit,
418 number: Some(truecolor_to_eight_bit(triplet)),
419 triplet: None,
420 }
421 }
422 ColorSystem::Standard => {
423 let Some(triplet) = source_triplet() else {
424 return self.clone();
425 };
426 Color {
427 name: self.name.clone(),
428 kind: ColorType::Standard,
429 number: Some(match_color(&STANDARD_PALETTE, triplet)),
430 triplet: None,
431 }
432 }
433 ColorSystem::Windows => {
434 if !is_truecolor {
435 if let Some(number) = self.number.filter(|&number| number < 16) {
436 return Color {
437 name: self.name.clone(),
438 kind: ColorType::Windows,
439 number: Some(number),
440 triplet: None,
441 };
442 }
443 }
444 let Some(triplet) = source_triplet() else {
445 return self.clone();
446 };
447 Color {
448 name: self.name.clone(),
449 kind: ColorType::Windows,
450 number: Some(match_color(&WINDOWS_PALETTE, triplet)),
451 triplet: None,
452 }
453 }
454 _ => self.clone(),
455 }
456 }
457}
458
459pub fn parse_rgb_hex(hex_color: &str) -> ColorTriplet {
465 assert_eq!(hex_color.len(), 6, "must be 6 characters");
466 let channel = |range: std::ops::Range<usize>| {
467 u8::from_str_radix(&hex_color[range], 16).expect("hex digits")
468 };
469 ColorTriplet::new(channel(0..2), channel(2..4), channel(4..6))
470}
471
472pub fn blend_rgb(color1: ColorTriplet, color2: ColorTriplet, cross_fade: f64) -> ColorTriplet {
476 let mix = |a: u8, b: u8| (a as f64 + (b as f64 - a as f64) * cross_fade) as u8;
477 ColorTriplet::new(
478 mix(color1.red, color2.red),
479 mix(color1.green, color2.green),
480 mix(color1.blue, color2.blue),
481 )
482}
483
484pub const ANSI_BASE_PALETTE: [ColorTriplet; 16] = [
493 ColorTriplet::new(0, 0, 0),
494 ColorTriplet::new(128, 0, 0),
495 ColorTriplet::new(0, 128, 0),
496 ColorTriplet::new(128, 128, 0),
497 ColorTriplet::new(0, 0, 128),
498 ColorTriplet::new(128, 0, 128),
499 ColorTriplet::new(0, 128, 128),
500 ColorTriplet::new(192, 192, 192),
501 ColorTriplet::new(128, 128, 128),
502 ColorTriplet::new(255, 0, 0),
503 ColorTriplet::new(0, 255, 0),
504 ColorTriplet::new(255, 255, 0),
505 ColorTriplet::new(0, 0, 255),
506 ColorTriplet::new(255, 0, 255),
507 ColorTriplet::new(0, 255, 255),
508 ColorTriplet::new(255, 255, 255),
509];
510
511pub const STANDARD_PALETTE: [ColorTriplet; 16] = [
518 ColorTriplet::new(0, 0, 0),
519 ColorTriplet::new(170, 0, 0),
520 ColorTriplet::new(0, 170, 0),
521 ColorTriplet::new(170, 85, 0),
522 ColorTriplet::new(0, 0, 170),
523 ColorTriplet::new(170, 0, 170),
524 ColorTriplet::new(0, 170, 170),
525 ColorTriplet::new(170, 170, 170),
526 ColorTriplet::new(85, 85, 85),
527 ColorTriplet::new(255, 85, 85),
528 ColorTriplet::new(85, 255, 85),
529 ColorTriplet::new(255, 255, 85),
530 ColorTriplet::new(85, 85, 255),
531 ColorTriplet::new(255, 85, 255),
532 ColorTriplet::new(85, 255, 255),
533 ColorTriplet::new(255, 255, 255),
534];
535
536pub const WINDOWS_PALETTE: [ColorTriplet; 16] = [
540 ColorTriplet::new(12, 12, 12),
541 ColorTriplet::new(197, 15, 31),
542 ColorTriplet::new(19, 161, 14),
543 ColorTriplet::new(193, 156, 0),
544 ColorTriplet::new(0, 55, 218),
545 ColorTriplet::new(136, 23, 152),
546 ColorTriplet::new(58, 150, 221),
547 ColorTriplet::new(204, 204, 204),
548 ColorTriplet::new(118, 118, 118),
549 ColorTriplet::new(231, 72, 86),
550 ColorTriplet::new(22, 198, 12),
551 ColorTriplet::new(249, 241, 165),
552 ColorTriplet::new(59, 120, 255),
553 ColorTriplet::new(180, 0, 158),
554 ColorTriplet::new(97, 214, 214),
555 ColorTriplet::new(242, 242, 242),
556];
557
558static EIGHT_BIT_PALETTE: [ColorTriplet; 256] = build_eight_bit_palette();
561
562const fn build_eight_bit_palette() -> [ColorTriplet; 256] {
563 let mut palette = [ColorTriplet::new(0, 0, 0); 256];
564 let mut i = 0;
566 while i < 16 {
567 palette[i] = ANSI_BASE_PALETTE[i];
568 i += 1;
569 }
570 let levels = [0u8, 95, 135, 175, 215, 255];
572 let mut r = 0;
573 while r < 6 {
574 let mut g = 0;
575 while g < 6 {
576 let mut b = 0;
577 while b < 6 {
578 let index = 16 + 36 * r + 6 * g + b;
579 palette[index] = ColorTriplet::new(levels[r], levels[g], levels[b]);
580 b += 1;
581 }
582 g += 1;
583 }
584 r += 1;
585 }
586 let mut n = 0;
588 while n < 24 {
589 let value = 8 + 10 * n as u8;
590 palette[232 + n] = ColorTriplet::new(value, value, value);
591 n += 1;
592 }
593 palette
594}
595
596fn eight_bit_triplet(number: u8) -> ColorTriplet {
597 EIGHT_BIT_PALETTE[number as usize]
598}
599
600fn rgb_to_ls(red: f64, green: f64, blue: f64) -> (f64, f64) {
606 let max = red.max(green).max(blue);
607 let min = red.min(green).min(blue);
608 let lightness = (max + min) / 2.0;
609 if max == min {
610 return (lightness, 0.0);
611 }
612 let saturation = if lightness <= 0.5 {
613 (max - min) / (max + min)
614 } else {
615 (max - min) / (2.0 - max - min)
616 };
617 (lightness, saturation)
618}
619
620fn truecolor_to_eight_bit(color: ColorTriplet) -> u8 {
629 let (red, green, blue) = (
630 color.red as f64 / 255.0,
631 color.green as f64 / 255.0,
632 color.blue as f64 / 255.0,
633 );
634 let (lightness, saturation) = rgb_to_ls(red, green, blue);
635
636 if saturation < 0.15 {
638 let gray = (lightness * 25.0).round_ties_even() as i64;
641 return match gray {
642 0 => 16,
643 25 => 231,
644 other => (231 + other) as u8,
645 };
646 }
647
648 let axis = |component: u8| -> f64 {
650 let value = component as f64;
651 if value < 95.0 {
652 value / 95.0
653 } else {
654 1.0 + (value - 95.0) / 40.0
655 }
656 };
657 let six_red = axis(color.red).round_ties_even();
658 let six_green = axis(color.green).round_ties_even();
659 let six_blue = axis(color.blue).round_ties_even();
660 (16.0 + 36.0 * six_red + 6.0 * six_green + six_blue) as u8
661}
662
663fn match_color(palette: &[ColorTriplet], color: ColorTriplet) -> u8 {
664 let (red1, green1, blue1) = (color.red as i64, color.green as i64, color.blue as i64);
665 let mut best_index = 0usize;
666 let mut best_distance = i64::MAX;
667 for (index, candidate) in palette.iter().enumerate() {
668 let (red2, green2, blue2) = (
669 candidate.red as i64,
670 candidate.green as i64,
671 candidate.blue as i64,
672 );
673 let red_mean = (red1 + red2) / 2;
674 let red = red1 - red2;
675 let green = green1 - green2;
676 let blue = blue1 - blue2;
677 let distance = (((512 + red_mean) * red * red) >> 8)
680 + 4 * green * green
681 + (((767 - red_mean) * blue * blue) >> 8);
682 if distance < best_distance {
683 best_distance = distance;
684 best_index = index;
685 }
686 }
687 best_index as u8
688}
689
690#[derive(Debug, Clone, PartialEq, Eq)]
692pub struct Palette {
693 colors: Vec<ColorTriplet>,
694}
695
696impl Palette {
697 pub fn new(colors: Vec<ColorTriplet>) -> Self {
699 Palette { colors }
700 }
701
702 pub fn get(&self, number: usize) -> Option<ColorTriplet> {
704 self.colors.get(number).copied()
705 }
706
707 pub fn colors(&self) -> &[ColorTriplet] {
709 &self.colors
710 }
711
712 pub fn len(&self) -> usize {
714 self.colors.len()
715 }
716
717 pub fn is_empty(&self) -> bool {
719 self.colors.is_empty()
720 }
721
722 pub fn match_color(&self, color: ColorTriplet) -> usize {
729 assert!(!self.colors.is_empty(), "match on an empty palette");
730 match_color(&self.colors, color) as usize
731 }
732}
733
734impl std::ops::Index<usize> for Palette {
735 type Output = ColorTriplet;
736
737 fn index(&self, number: usize) -> &ColorTriplet {
738 &self.colors[number]
739 }
740}
741
742impl crate::protocol::Renderable for Palette {
744 fn rich_render(
745 &self,
746 console: &crate::console::Console,
747 options: &crate::console::ConsoleOptions,
748 ) -> Vec<crate::segment::Segment> {
749 self.table().rich_render(console, options)
750 }
751
752 fn measure(
753 &self,
754 console: &crate::console::Console,
755 options: &crate::console::ConsoleOptions,
756 ) -> crate::measure::Measurement {
757 self.table().measure(console, options)
758 }
759}
760
761impl Palette {
762 fn table(&self) -> crate::table::Table {
764 use crate::table::{Cell, Table};
765 use crate::text::Text;
766 let mut table = Table::new()
767 .title("Palette")
768 .caption(format!("{} colors", self.colors.len()))
769 .highlight(true)
770 .caption_justify(crate::console::Justify::Right);
771 table.add_column("index");
772 table.add_column("RGB");
773 table.add_column("Color");
774 for (index, color) in self.colors.iter().enumerate() {
775 let swatch = crate::style::Style::from_color(None, Some(Color::from_triplet(*color)));
776 table.add_row_cells(vec![
777 Cell::Markup(index.to_string()),
778 Cell::Markup(format!("({}, {}, {})", color.red, color.green, color.blue)),
779 Cell::Text(Text::styled(" ".repeat(16), swatch)),
780 ]);
781 }
782 table
783 }
784}
785
786impl crate::protocol::Renderable for Color {
789 fn rich_render(
790 &self,
791 console: &crate::console::Console,
792 options: &crate::console::ConsoleOptions,
793 ) -> Vec<crate::segment::Segment> {
794 self.rich_text().rich_render(console, options)
795 }
796
797 fn measure(
798 &self,
799 console: &crate::console::Console,
800 options: &crate::console::ConsoleOptions,
801 ) -> crate::measure::Measurement {
802 self.rich_text().measure(console, options)
803 }
804
805 fn printed_text(&self) -> Option<crate::text::Text> {
806 Some(self.rich_text())
807 }
808}
809
810#[cfg(test)]
811mod tests {
812 use super::*;
813
814 #[test]
817 fn hex_matches_format() {
818 for value in 0..=255u8 {
819 let other = value.wrapping_mul(97).wrapping_add(13);
820 for (r, g, b) in [(value, other, 0), (other, value, 255), (0, other, value)] {
821 assert_eq!(
822 ColorTriplet::new(r, g, b).hex(),
823 format!("#{r:02x}{g:02x}{b:02x}")
824 );
825 }
826 }
827 }
828
829 #[test]
833 fn windows_downgrade_uses_the_windows_palette() {
834 let grey = Color::parse("#808080")
835 .unwrap()
836 .downgrade(ColorSystem::Windows);
837 assert_eq!((grey.kind, grey.number), (ColorType::Windows, Some(8)));
838 let low = Color::parse("color(9)")
839 .unwrap()
840 .downgrade(ColorSystem::Windows);
841 assert_eq!((low.kind, low.number), (ColorType::Windows, Some(9)));
842 assert!(Color::parse("grey").is_err());
843 assert!(Color::parse("gray").is_err());
844 }
845
846 #[test]
847 fn parses_standard_name() {
848 let c = Color::parse("red").unwrap();
849 assert_eq!(c.kind, ColorType::Standard);
850 assert_eq!(c.number, Some(1));
851 assert_eq!(c.ansi_codes(true), vec!["31"]);
852 assert_eq!(c.ansi_codes(false), vec!["41"]);
853 }
854
855 #[test]
856 fn extended_name_is_eight_bit() {
857 let c = Color::parse("orange1").unwrap();
859 assert_eq!(c.kind, ColorType::EightBit);
860 assert_eq!(c.number, Some(214));
861 assert_eq!(c.ansi_codes(true), vec!["38", "5", "214"]);
862 }
863
864 #[test]
865 fn bright_color_uses_high_intensity_sgr() {
866 let c = Color::parse("bright_red").unwrap();
867 assert_eq!(c.number, Some(9));
868 assert_eq!(c.ansi_codes(true), vec!["91"]);
870 }
871
872 #[test]
873 fn parses_hex_truecolor() {
874 let c = Color::parse("#ff8800").unwrap();
875 assert_eq!(c.kind, ColorType::Truecolor);
876 assert_eq!(c.triplet, Some(ColorTriplet::new(0xff, 0x88, 0x00)));
877 assert_eq!(c.ansi_codes(true), vec!["38", "2", "255", "136", "0"]);
878 }
879
880 #[test]
887 fn downgrade_to_standard_matches_upstream() {
888 let standard = |hex: &str| {
889 let down = Color::parse(hex).unwrap().downgrade(ColorSystem::Standard);
890 assert_eq!(down.kind, ColorType::Standard);
891 down.number
892 };
893 assert_eq!(standard("#ff0000"), Some(1));
894 assert_eq!(standard("#00ff00"), Some(2));
895 assert_eq!(standard("#0000ff"), Some(4));
896 assert_eq!(standard("#ffffff"), Some(15));
897 assert_eq!(standard("#808080"), Some(7));
898 assert_eq!(standard("#ff8800"), Some(9));
899 }
900
901 #[test]
905 fn downgrade_to_eight_bit_matches_upstream() {
906 let eight_bit = |hex: &str| {
907 let down = Color::parse(hex).unwrap().downgrade(ColorSystem::EightBit);
908 assert_eq!(down.kind, ColorType::EightBit);
909 down.number
910 };
911 assert_eq!(eight_bit("#ff0000"), Some(196));
912 assert_eq!(eight_bit("#00ff00"), Some(46));
913 assert_eq!(eight_bit("#0000ff"), Some(21));
914 assert_eq!(eight_bit("#ff8800"), Some(208));
915 assert_eq!(eight_bit("#ffffff"), Some(231));
917 assert_eq!(eight_bit("#808080"), Some(244));
918 }
919
920 #[test]
921 fn eight_bit_palette_cube_is_correct() {
922 assert_eq!(eight_bit_triplet(196), ColorTriplet::new(255, 0, 0));
924 }
925}