use crate::{colors, ByteExt, Error, Stream};
#[cfg(not(feature = "std"))]
use kurbo::common::FloatFuncs;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[allow(missing_docs)]
pub struct Color {
pub red: u8,
pub green: u8,
pub blue: u8,
pub alpha: u8,
}
impl Color {
#[inline]
pub fn new_rgb(red: u8, green: u8, blue: u8) -> Self {
Self {
red,
green,
blue,
alpha: 255,
}
}
#[inline]
pub fn new_rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Self {
Self {
red,
green,
blue,
alpha,
}
}
#[inline]
pub fn black() -> Self {
Self::new_rgb(0, 0, 0)
}
#[inline]
pub fn white() -> Self {
Self::new_rgb(255, 255, 255)
}
#[inline]
pub fn gray() -> Self {
Self::new_rgb(128, 128, 128)
}
#[inline]
pub fn red() -> Self {
Self::new_rgb(255, 0, 0)
}
#[inline]
pub fn green() -> Self {
Self::new_rgb(0, 128, 0)
}
#[inline]
pub fn blue() -> Self {
Self::new_rgb(0, 0, 255)
}
}
impl core::str::FromStr for Color {
type Err = Error;
fn from_str(text: &str) -> Result<Self, Error> {
let mut s = Stream::from(text);
let color = s.parse_color()?;
s.skip_spaces();
if !s.at_end() {
return Err(Error::UnexpectedData(s.calc_char_pos()));
}
Ok(color)
}
}
impl Stream<'_> {
pub fn try_parse_color(&mut self) -> Option<Color> {
let mut s = *self;
if let Ok(color) = s.parse_color() {
*self = s;
Some(color)
} else {
None
}
}
pub fn parse_color(&mut self) -> Result<Color, Error> {
self.skip_spaces();
let mut color = Color::black();
if self.curr_byte()? == b'#' {
self.advance(1);
let color_str = self.consume_bytes(|_, c| c.is_hex_digit()).as_bytes();
match color_str.len() {
6 => {
color.red = hex_pair(color_str[0], color_str[1]);
color.green = hex_pair(color_str[2], color_str[3]);
color.blue = hex_pair(color_str[4], color_str[5]);
}
8 => {
color.red = hex_pair(color_str[0], color_str[1]);
color.green = hex_pair(color_str[2], color_str[3]);
color.blue = hex_pair(color_str[4], color_str[5]);
color.alpha = hex_pair(color_str[6], color_str[7]);
}
3 => {
color.red = short_hex(color_str[0]);
color.green = short_hex(color_str[1]);
color.blue = short_hex(color_str[2]);
}
4 => {
color.red = short_hex(color_str[0]);
color.green = short_hex(color_str[1]);
color.blue = short_hex(color_str[2]);
color.alpha = short_hex(color_str[3]);
}
_ => {
return Err(Error::InvalidValue);
}
}
} else {
let name = self.consume_ascii_ident().to_ascii_lowercase();
if name == "rgb" || name == "rgba" {
self.consume_byte(b'(')?;
let mut is_percent = false;
let value = self.parse_number()?;
if self.starts_with(b"%") {
self.advance(1);
is_percent = true;
}
self.skip_spaces();
self.parse_list_separator();
if is_percent {
color.red = ((value / 100.0) * 255.0).round() as u8;
color.green = (self.parse_list_number_or_percent()? * 255.0).round() as u8;
color.blue = (self.parse_list_number_or_percent()? * 255.0).round() as u8;
} else {
color.red = value.round() as u8;
color.green = self.parse_list_number()?.round() as u8;
color.blue = self.parse_list_number()?.round() as u8;
}
self.skip_spaces();
if !self.starts_with(b")") {
color.alpha = (self.parse_list_number()? * 255.0).round() as u8;
}
self.skip_spaces();
self.consume_byte(b')')?;
} else if name == "hsl" || name == "hsla" {
self.consume_byte(b'(')?;
let mut hue = self.parse_list_number()?;
hue = ((hue % 360.0) + 360.0) % 360.0;
let saturation = f64_bound(0.0, self.parse_list_number_or_percent()?, 1.0);
let lightness = f64_bound(0.0, self.parse_list_number_or_percent()?, 1.0);
color = hsl_to_rgb(hue as f32 / 60.0, saturation as f32, lightness as f32);
self.skip_spaces();
if !self.starts_with(b")") {
color.alpha = (self.parse_list_number()? * 255.0).round() as u8;
}
self.skip_spaces();
self.consume_byte(b')')?;
} else {
match colors::from_str(&name) {
Some(c) => {
color = c;
}
None => {
return Err(Error::InvalidValue);
}
}
}
}
Ok(color)
}
}
#[inline]
fn from_hex(c: u8) -> u8 {
match c {
b'0'..=b'9' => c - b'0',
b'a'..=b'f' => c - b'a' + 10,
b'A'..=b'F' => c - b'A' + 10,
_ => b'0',
}
}
#[inline]
fn short_hex(c: u8) -> u8 {
let h = from_hex(c);
(h << 4) | h
}
#[inline]
fn hex_pair(c1: u8, c2: u8) -> u8 {
let h1 = from_hex(c1);
let h2 = from_hex(c2);
(h1 << 4) | h2
}
fn hsl_to_rgb(hue: f32, saturation: f32, lightness: f32) -> Color {
let t2 = if lightness <= 0.5 {
lightness * (saturation + 1.0)
} else {
lightness + saturation - (lightness * saturation)
};
let t1 = lightness * 2.0 - t2;
let red = hue_to_rgb(t1, t2, hue + 2.0);
let green = hue_to_rgb(t1, t2, hue);
let blue = hue_to_rgb(t1, t2, hue - 2.0);
Color::new_rgb(
(red * 255.0).round() as u8,
(green * 255.0).round() as u8,
(blue * 255.0).round() as u8,
)
}
fn hue_to_rgb(t1: f32, t2: f32, mut hue: f32) -> f32 {
if hue < 0.0 {
hue += 6.0;
}
if hue >= 6.0 {
hue -= 6.0;
}
if hue < 1.0 {
(t2 - t1) * hue + t1
} else if hue < 3.0 {
t2
} else if hue < 4.0 {
(t2 - t1) * (4.0 - hue) + t1
} else {
t1
}
}
#[inline]
fn f64_bound(min: f64, val: f64, max: f64) -> f64 {
debug_assert!(val.is_finite());
val.clamp(min, max)
}
#[rustfmt::skip]
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use core::str::FromStr;
use crate::Color;
macro_rules! test {
($name:ident, $text:expr, $color:expr) => {
#[test]
fn $name() {
assert_eq!(Color::from_str($text).unwrap(), $color);
}
};
}
test!(
rrggbb,
"#ff0000",
Color::new_rgb(255, 0, 0)
);
test!(
rrggbb_upper,
"#FF0000",
Color::new_rgb(255, 0, 0)
);
test!(
rgb_hex,
"#f00",
Color::new_rgb(255, 0, 0)
);
test!(
rrggbbaa,
"#ff0000ff",
Color::new_rgba(255, 0, 0, 255)
);
test!(
rrggbbaa_upper,
"#FF0000FF",
Color::new_rgba(255, 0, 0, 255)
);
test!(
rgba_hex,
"#f00f",
Color::new_rgba(255, 0, 0, 255)
);
test!(
rrggbb_spaced,
" #ff0000 ",
Color::new_rgb(255, 0, 0)
);
test!(
rgb_numeric,
"rgb(254, 203, 231)",
Color::new_rgb(254, 203, 231)
);
test!(
rgb_numeric_spaced,
" rgb( 77 , 77 , 77 ) ",
Color::new_rgb(77, 77, 77)
);
test!(
rgb_percentage,
"rgb(50%, 50%, 50%)",
Color::new_rgb(128, 128, 128)
);
test!(
rgb_percentage_overflow,
"rgb(140%, -10%, 130%)",
Color::new_rgb(255, 0, 255)
);
test!(
rgb_percentage_float,
"rgb(33.333%,46.666%,93.333%)",
Color::new_rgb(85, 119, 238)
);
test!(
rgb_numeric_upper_case,
"RGB(254, 203, 231)",
Color::new_rgb(254, 203, 231)
);
test!(
rgb_numeric_mixed_case,
"RgB(254, 203, 231)",
Color::new_rgb(254, 203, 231)
);
test!(
rgb_numeric_red_float,
"rgb(3.141592653, 110, 201)",
Color::new_rgb(3, 110, 201)
);
test!(
rgb_numeric_green_float,
"rgb(254, 150.829521289232389, 210)",
Color::new_rgb(254, 151, 210)
);
test!(
rgb_numeric_blue_float,
"rgb(96, 255, 0.2)",
Color::new_rgb(96, 255, 0)
);
test!(
rgb_numeric_all_float,
"rgb(0.0, 129.82, 231.092)",
Color::new_rgb(0, 130, 231)
);
test!(
rgb_numeric_all_float_with_alpha,
"rgb(0.0, 129.82, 231.092, 0.5)",
Color::new_rgba(0, 130, 231, 128)
);
test!(
rgb_numeric_all_float_overflow,
"rgb(290.2, 255.9, 300.0)",
Color::new_rgb(255, 255, 255)
);
test!(
name_red,
"red",
Color::new_rgb(255, 0, 0)
);
test!(
name_red_spaced,
" red ",
Color::new_rgb(255, 0, 0)
);
test!(
name_red_upper_case,
"RED",
Color::new_rgb(255, 0, 0)
);
test!(
name_red_mixed_case,
"ReD",
Color::new_rgb(255, 0, 0)
);
test!(
name_cornflowerblue,
"cornflowerblue",
Color::new_rgb(100, 149, 237)
);
test!(
transparent,
"transparent",
Color::new_rgba(0, 0, 0, 0)
);
test!(
rgba_half,
"rgba(10, 20, 30, 0.5)",
Color::new_rgba(10, 20, 30, 128)
);
test!(
rgba_numeric_red_float,
"rgba(3.141592653, 110, 201, 1.0)",
Color::new_rgba(3, 110, 201, 255)
);
test!(
rgba_numeric_all_float,
"rgba(0.0, 129.82, 231.092, 1.5)",
Color::new_rgba(0, 130, 231, 255)
);
test!(
rgba_negative,
"rgba(10, 20, 30, -2)",
Color::new_rgba(10, 20, 30, 0)
);
test!(
rgba_large_alpha,
"rgba(10, 20, 30, 2)",
Color::new_rgba(10, 20, 30, 255)
);
test!(
rgb_with_alpha,
"rgb(10, 20, 30, 0.5)",
Color::new_rgba(10, 20, 30, 128)
);
test!(
hsl_green,
"hsl(120, 100%, 75%)",
Color::new_rgba(128, 255, 128, 255)
);
test!(
hsl_yellow,
"hsl(60, 100%, 50%)",
Color::new_rgba(255, 255, 0, 255)
);
test!(
hsl_hue_360,
"hsl(360, 100%, 100%)",
Color::new_rgba(255, 255, 255, 255)
);
test!(
hsl_out_of_bounds,
"hsl(800, 150%, -50%)",
Color::new_rgba(0, 0, 0, 255)
);
test!(
hsla_green,
"hsla(120, 100%, 75%, 0.5)",
Color::new_rgba(128, 255, 128, 128)
);
test!(
hsl_with_alpha,
"hsl(120, 100%, 75%, 0.5)",
Color::new_rgba(128, 255, 128, 128)
);
test!(
hsl_to_rgb_red_round_up,
"hsl(230, 57%, 54%)",
Color::new_rgba(71, 93, 205, 255)
);
test!(
hsl_with_hue_float,
"hsl(120.152, 100%, 75%)",
Color::new_rgba(128, 255, 128, 255)
);
test!(
hsla_with_hue_float,
"hsla(120.152, 100%, 75%, 0.5)",
Color::new_rgba(128, 255, 128, 128)
);
macro_rules! test_err {
($name:ident, $text:expr, $err:expr) => {
#[test]
fn $name() {
assert_eq!(Color::from_str($text).unwrap_err().to_string(), $err);
}
};
}
test_err!(
not_a_color_1,
"text",
"invalid value"
);
test_err!(
icc_color_not_supported_1,
"#CD853F icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)",
"unexpected data at position 9"
);
test_err!(
icc_color_not_supported_2,
"red icc-color(acmecmyk, 0.11, 0.48, 0.83, 0.00)",
"unexpected data at position 5"
);
test_err!(
invalid_input_1,
"rgb(-0\x0d",
"unexpected end of stream"
);
test_err!(
invalid_input_2,
"#9ߞpx! ;",
"invalid value"
);
test_err!(
rgba_with_percent_alpha,
"rgba(10, 20, 30, 5%)",
"expected ')' not '%' at position 19"
);
test_err!(
rgb_mixed_units,
"rgb(140%, -10mm, 130pt)",
"invalid number at position 14"
);
}