use std::str::FromStr; use syntect::highlighting::Color as SynColor; use super::*; /// How a fill or stroke should be painted. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum Paint { /// A solid color. Solid(Color), } impl From for Paint where T: Into, { fn from(t: T) -> Self { Self::Solid(t.into()) } } impl Debug for Paint { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Solid(color) => color.fmt(f), } } } /// A color in a dynamic format. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub enum Color { /// An 8-bit luma color. Luma(LumaColor), /// An 8-bit RGBA color. Rgba(RgbaColor), /// An 8-bit CMYK color. Cmyk(CmykColor), } impl Color { pub const BLACK: Self = Self::Rgba(RgbaColor::new(0x00, 0x00, 0x00, 0xFF)); pub const GRAY: Self = Self::Rgba(RgbaColor::new(0xAA, 0xAA, 0xAA, 0xFF)); pub const SILVER: Self = Self::Rgba(RgbaColor::new(0xDD, 0xDD, 0xDD, 0xFF)); pub const WHITE: Self = Self::Rgba(RgbaColor::new(0xFF, 0xFF, 0xFF, 0xFF)); pub const NAVY: Self = Self::Rgba(RgbaColor::new(0x00, 0x1f, 0x3f, 0xFF)); pub const BLUE: Self = Self::Rgba(RgbaColor::new(0x00, 0x74, 0xD9, 0xFF)); pub const AQUA: Self = Self::Rgba(RgbaColor::new(0x7F, 0xDB, 0xFF, 0xFF)); pub const TEAL: Self = Self::Rgba(RgbaColor::new(0x39, 0xCC, 0xCC, 0xFF)); pub const EASTERN: Self = Self::Rgba(RgbaColor::new(0x23, 0x9D, 0xAD, 0xFF)); pub const PURPLE: Self = Self::Rgba(RgbaColor::new(0xB1, 0x0D, 0xC9, 0xFF)); pub const FUCHSIA: Self = Self::Rgba(RgbaColor::new(0xF0, 0x12, 0xBE, 0xFF)); pub const MAROON: Self = Self::Rgba(RgbaColor::new(0x85, 0x14, 0x4b, 0xFF)); pub const RED: Self = Self::Rgba(RgbaColor::new(0xFF, 0x41, 0x36, 0xFF)); pub const ORANGE: Self = Self::Rgba(RgbaColor::new(0xFF, 0x85, 0x1B, 0xFF)); pub const YELLOW: Self = Self::Rgba(RgbaColor::new(0xFF, 0xDC, 0x00, 0xFF)); pub const OLIVE: Self = Self::Rgba(RgbaColor::new(0x3D, 0x99, 0x70, 0xFF)); pub const GREEN: Self = Self::Rgba(RgbaColor::new(0x2E, 0xCC, 0x40, 0xFF)); pub const LIME: Self = Self::Rgba(RgbaColor::new(0x01, 0xFF, 0x70, 0xFF)); /// Convert this color to RGBA. pub fn to_rgba(self) -> RgbaColor { match self { Self::Luma(luma) => luma.to_rgba(), Self::Rgba(rgba) => rgba, Self::Cmyk(cmyk) => cmyk.to_rgba(), } } /// Lighten this color by the given factor. pub fn lighten(self, factor: Ratio) -> Self { match self { Self::Luma(luma) => Self::Luma(luma.lighten(factor)), Self::Rgba(rgba) => Self::Rgba(rgba.lighten(factor)), Self::Cmyk(cmyk) => Self::Cmyk(cmyk.lighten(factor)), } } /// Darken this color by the given factor. pub fn darken(self, factor: Ratio) -> Self { match self { Self::Luma(luma) => Self::Luma(luma.darken(factor)), Self::Rgba(rgba) => Self::Rgba(rgba.darken(factor)), Self::Cmyk(cmyk) => Self::Cmyk(cmyk.darken(factor)), } } /// Negate this color. pub fn negate(self) -> Self { match self { Self::Luma(luma) => Self::Luma(luma.negate()), Self::Rgba(rgba) => Self::Rgba(rgba.negate()), Self::Cmyk(cmyk) => Self::Cmyk(cmyk.negate()), } } } impl Debug for Color { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Luma(c) => Debug::fmt(c, f), Self::Rgba(c) => Debug::fmt(c, f), Self::Cmyk(c) => Debug::fmt(c, f), } } } /// An 8-bit grayscale color. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct LumaColor(pub u8); impl LumaColor { /// Construct a new luma color. pub const fn new(luma: u8) -> Self { Self(luma) } /// Convert to an opque RGBA color. pub const fn to_rgba(self) -> RgbaColor { RgbaColor::new(self.0, self.0, self.0, u8::MAX) } /// Convert to CMYK as a fraction of true black. pub fn to_cmyk(self) -> CmykColor { CmykColor::new( round_u8(self.0 as f64 * 0.75), round_u8(self.0 as f64 * 0.68), round_u8(self.0 as f64 * 0.67), round_u8(self.0 as f64 * 0.90), ) } /// Lighten this color by a factor. pub fn lighten(self, factor: Ratio) -> Self { let inc = round_u8((u8::MAX - self.0) as f64 * factor.get()); Self(self.0.saturating_add(inc)) } /// Darken this color by a factor. pub fn darken(self, factor: Ratio) -> Self { let dec = round_u8(self.0 as f64 * factor.get()); Self(self.0.saturating_sub(dec)) } /// Negate this color. pub fn negate(self) -> Self { Self(u8::MAX - self.0) } } impl Debug for LumaColor { fn fmt(&self, f: &mut Formatter) -> fmt::Result { write!(f, "luma({})", self.0) } } impl From for Color { fn from(luma: LumaColor) -> Self { Self::Luma(luma) } } /// An 8-bit RGBA color. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct RgbaColor { /// Red channel. pub r: u8, /// Green channel. pub g: u8, /// Blue channel. pub b: u8, /// Alpha channel. pub a: u8, } impl RgbaColor { /// Construct a new RGBA color. pub const fn new(r: u8, g: u8, b: u8, a: u8) -> Self { Self { r, g, b, a } } /// Lighten this color by a factor. /// /// The alpha channel is not affected. pub fn lighten(self, factor: Ratio) -> Self { let lighten = |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); Self { r: lighten(self.r), g: lighten(self.g), b: lighten(self.b), a: self.a, } } /// Darken this color by a factor. /// /// The alpha channel is not affected. pub fn darken(self, factor: Ratio) -> Self { let darken = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); Self { r: darken(self.r), g: darken(self.g), b: darken(self.b), a: self.a, } } /// Negate this color. /// /// The alpha channel is not affected. pub fn negate(self) -> Self { Self { r: u8::MAX - self.r, g: u8::MAX - self.g, b: u8::MAX - self.b, a: self.a, } } } impl FromStr for RgbaColor { type Err = &'static str; /// Constructs a new color from hex strings like the following: /// - `#aef` (shorthand, with leading hashtag), /// - `7a03c2` (without alpha), /// - `abcdefff` (with alpha). /// /// The hashtag is optional and both lower and upper case are fine. fn from_str(hex_str: &str) -> Result { let hex_str = hex_str.strip_prefix('#').unwrap_or(hex_str); if hex_str.chars().any(|c| !c.is_ascii_hexdigit()) { return Err("string contains non-hexadecimal letters"); } let len = hex_str.len(); let long = len == 6 || len == 8; let short = len == 3 || len == 4; let alpha = len == 4 || len == 8; if !long && !short { return Err("string has wrong length"); } let mut values: [u8; 4] = [u8::MAX; 4]; for elem in if alpha { 0 .. 4 } else { 0 .. 3 } { let item_len = if long { 2 } else { 1 }; let pos = elem * item_len; let item = &hex_str[pos .. (pos + item_len)]; values[elem] = u8::from_str_radix(item, 16).unwrap(); if short { // Duplicate number for shorthand notation, i.e. `a` -> `aa` values[elem] += values[elem] * 16; } } Ok(Self::new(values[0], values[1], values[2], values[3])) } } impl From for RgbaColor { fn from(SynColor { r, g, b, a }: SynColor) -> Self { Self { r, g, b, a } } } impl From for SynColor { fn from(RgbaColor { r, g, b, a }: RgbaColor) -> Self { Self { r, g, b, a } } } impl Debug for RgbaColor { fn fmt(&self, f: &mut Formatter) -> fmt::Result { if f.alternate() { write!(f, "rgba({}, {}, {}, {})", self.r, self.g, self.b, self.a,)?; } else { write!(f, "rgb(\"#{:02x}{:02x}{:02x}", self.r, self.g, self.b)?; if self.a != 255 { write!(f, "{:02x}", self.a)?; } write!(f, "\")")?; } Ok(()) } } impl From for Color where T: Into, { fn from(rgba: T) -> Self { Self::Rgba(rgba.into()) } } /// An 8-bit CMYK color. #[derive(Copy, Clone, Eq, PartialEq, Hash)] pub struct CmykColor { /// The cyan component. pub c: u8, /// The magenta component. pub m: u8, /// The yellow component. pub y: u8, /// The key (black) component. pub k: u8, } impl CmykColor { /// Construct a new CMYK color. pub const fn new(c: u8, m: u8, y: u8, k: u8) -> Self { Self { c, m, y, k } } /// Convert this color to RGBA. pub fn to_rgba(self) -> RgbaColor { let k = self.k as f64 / 255.0; let f = |c| { let c = c as f64 / 255.0; round_u8(255.0 * (1.0 - c) * (1.0 - k)) }; RgbaColor { r: f(self.c), g: f(self.m), b: f(self.y), a: 255, } } /// Lighten this color by a factor. pub fn lighten(self, factor: Ratio) -> Self { let lighten = |c: u8| c.saturating_sub(round_u8(c as f64 * factor.get())); Self { c: lighten(self.c), m: lighten(self.m), y: lighten(self.y), k: lighten(self.k), } } /// Darken this color by a factor. pub fn darken(self, factor: Ratio) -> Self { let darken = |c: u8| c.saturating_add(round_u8((u8::MAX - c) as f64 * factor.get())); Self { c: darken(self.c), m: darken(self.m), y: darken(self.y), k: darken(self.k), } } /// Negate this color. /// /// Does not affect the key component. pub fn negate(self) -> Self { Self { c: u8::MAX - self.c, m: u8::MAX - self.m, y: u8::MAX - self.y, k: self.k, } } } impl Debug for CmykColor { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let g = |c| 100.0 * (c as f64 / 255.0); write!( f, "cmyk({:.1}%, {:.1}%, {:.1}%, {:.1}%)", g(self.c), g(self.m), g(self.y), g(self.k), ) } } impl From for Color { fn from(cmyk: CmykColor) -> Self { Self::Cmyk(cmyk) } } /// Convert to the closest u8. fn round_u8(value: f64) -> u8 { value.round() as u8 } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_color_strings() { #[track_caller] fn test(hex: &str, r: u8, g: u8, b: u8, a: u8) { assert_eq!(RgbaColor::from_str(hex), Ok(RgbaColor::new(r, g, b, a))); } test("f61243ff", 0xf6, 0x12, 0x43, 0xff); test("b3d8b3", 0xb3, 0xd8, 0xb3, 0xff); test("fCd2a9AD", 0xfc, 0xd2, 0xa9, 0xad); test("233", 0x22, 0x33, 0x33, 0xff); test("111b", 0x11, 0x11, 0x11, 0xbb); } #[test] fn test_parse_invalid_colors() { #[track_caller] fn test(hex: &str, message: &str) { assert_eq!(RgbaColor::from_str(hex), Err(message)); } test("a5", "string has wrong length"); test("12345", "string has wrong length"); test("f075ff011", "string has wrong length"); test("hmmm", "string contains non-hexadecimal letters"); test("14B2AH", "string contains non-hexadecimal letters"); } }