mod ansi16;
mod ansi256;
mod cmyk;
mod error;
mod hsl;
mod hsv;
mod hue;
mod lab;
mod lchab;
mod lchuv;
mod luv;
mod oklab;
mod oklch;
mod rgb;
mod white_point;
mod xyz;
use std::{
fmt,
fmt::{Display, Formatter},
marker::PhantomData,
str::FromStr,
};
pub use ansi16::Ansi16;
pub use ansi256::Ansi256;
pub use cmyk::CMYK;
pub use hsl::HSL;
pub use hsv::HSV;
pub use hue::Hue;
pub(crate) use lab::xyz_to_lab;
pub use lab::Lab;
pub use lchab::LCHab;
pub use lchuv::LCHuv;
pub use luv::Luv;
pub use oklab::Oklab;
pub use oklch::Oklch;
pub use rgb::RGB;
pub use white_point::*;
pub(crate) use xyz::rgb_to_xyz;
pub use xyz::XYZ;
use crate::{color::error::ColorError, math::FloatNumber};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Color<T, W = D65>
where
T: FloatNumber,
{
pub(super) l: T,
pub(super) a: T,
pub(super) b: T,
_marker: PhantomData<W>,
}
impl<T, W> Color<T, W>
where
T: FloatNumber,
W: WhitePoint,
{
#[must_use]
pub(crate) fn new(l: T, a: T, b: T) -> Self {
Self {
l,
a,
b,
_marker: PhantomData,
}
}
#[must_use]
pub fn is_light(&self) -> bool {
self.l > T::from_f32(50.0)
}
#[must_use]
pub fn is_dark(&self) -> bool {
!self.is_light()
}
#[must_use]
pub fn lightness(&self) -> T {
self.l
}
#[must_use]
pub fn chroma(&self) -> T {
(self.a.powi(2) + self.b.powi(2)).sqrt()
}
#[inline]
#[must_use]
pub fn delta_e(&self, other: &Self) -> T {
let delta_l = self.l - other.l;
let delta_a = self.a - other.a;
let delta_b = self.b - other.b;
(delta_l.powi(2) + delta_a.powi(2) + delta_b.powi(2)).sqrt()
}
#[must_use]
pub fn hue(&self) -> Hue<T> {
let degrees = self.b.atan2(self.a).to_degrees();
Hue::from_degrees(degrees)
}
#[must_use]
pub fn to_hex_string(&self) -> String {
let RGB { r, g, b } = self.to_rgb();
format!("#{:02X}{:02X}{:02X}", r, g, b)
}
#[must_use]
pub fn to_rgb(&self) -> RGB {
RGB::from(&self.to_xyz())
}
#[must_use]
pub fn to_cmyk(&self) -> CMYK<T> {
CMYK::from(&self.to_rgb())
}
#[must_use]
pub fn to_hsl(&self) -> HSL<T> {
HSL::from(&self.to_rgb())
}
#[must_use]
pub fn to_hsv(&self) -> HSV<T> {
HSV::from(&self.to_rgb())
}
#[must_use]
pub fn to_xyz(&self) -> XYZ<T> {
XYZ::from(&self.to_lab())
}
#[must_use]
pub fn to_luv(&self) -> Luv<T, W> {
Luv::<T, W>::from(&self.to_xyz())
}
#[must_use]
pub fn to_lchuv(&self) -> LCHuv<T, W> {
LCHuv::<T, W>::from(&self.to_luv())
}
#[must_use]
pub fn to_lab(&self) -> Lab<T, W> {
Lab::<T, W>::new(self.l, self.a, self.b)
}
#[must_use]
pub fn to_lchab(&self) -> LCHab<T, W> {
LCHab::<T, W>::from(&self.to_lab())
}
#[must_use]
pub fn to_oklab(&self) -> Oklab<T> {
Oklab::from(&self.to_xyz())
}
#[must_use]
pub fn to_oklch(&self) -> Oklch<T> {
Oklch::from(&self.to_oklab())
}
#[must_use]
pub fn to_ansi16(&self) -> Ansi16 {
Ansi16::from(&self.to_rgb())
}
#[must_use]
pub fn to_ansi256(&self) -> Ansi256 {
Ansi256::from(&self.to_rgb())
}
#[must_use]
pub fn to_rgb_int(&self) -> u32 {
let rgb = self.to_rgb();
let r = rgb.r as u32;
let g = rgb.g as u32;
let b = rgb.b as u32;
(r << 16) | (g << 8) | b
}
#[must_use]
pub fn to_rgba_int(&self, alpha: u8) -> u32 {
let rgb = self.to_rgb();
let r = rgb.r as u32;
let g = rgb.g as u32;
let b = rgb.b as u32;
(r << 24) | (g << 16) | (b << 8) | alpha as u32
}
}
impl<T> Display for Color<T>
where
T: FloatNumber,
{
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"Color(l: {:.2}, a: {:.2}, b: {:.2})",
self.l, self.a, self.b
)
}
}
impl<T> From<u32> for Color<T>
where
T: FloatNumber,
{
fn from(value: u32) -> Self {
let r = (value >> 16) & 0xFF;
let g = (value >> 8) & 0xFF;
let b = value & 0xFF;
let (x, y, z) = rgb_to_xyz::<T>(r as u8, g as u8, b as u8);
let (l, a, b) = xyz_to_lab::<T, D65>(x, y, z);
Self::new(l, a, b)
}
}
impl<T> FromStr for Color<T>
where
T: FloatNumber,
{
type Err = ColorError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.starts_with("#") {
return Err(ColorError::InvalidHexValue(s.to_string()));
}
let (r, g, b) = match s.len() {
4 => {
let r = u8::from_str_radix(&s[1..2].repeat(2), 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let g = u8::from_str_radix(&s[2..3].repeat(2), 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let b = u8::from_str_radix(&s[3..4].repeat(2), 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
(r, g, b)
}
5 => {
let r = u8::from_str_radix(&s[1..2].repeat(2), 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let g = u8::from_str_radix(&s[2..3].repeat(2), 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let b = u8::from_str_radix(&s[3..4].repeat(2), 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let _ = u8::from_str_radix(&s[4..5].repeat(2), 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
(r, g, b)
}
7 => {
let r = u8::from_str_radix(&s[1..3], 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let g = u8::from_str_radix(&s[3..5], 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let b = u8::from_str_radix(&s[5..7], 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
(r, g, b)
}
9 => {
let r = u8::from_str_radix(&s[1..3], 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let g = u8::from_str_radix(&s[3..5], 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let b = u8::from_str_radix(&s[5..7], 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
let _ = u8::from_str_radix(&s[7..9], 16)
.map_err(|_| ColorError::InvalidHexValue(s.to_string()))?;
(r, g, b)
}
_ => return Err(ColorError::InvalidHexValue(s.to_string())),
};
let (x, y, z) = rgb_to_xyz::<T>(r, g, b);
let (l, a, b) = xyz_to_lab::<T, D65>(x, y, z);
Ok(Self::new(l, a, b))
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::assert_approx_eq;
#[test]
fn test_new() {
let actual: Color<f32> = Color::new(80.0, 0.0, 0.0);
assert_eq!(actual.l, 80.0);
assert_eq!(actual.a, 0.0);
assert_eq!(actual.b, 0.0);
}
#[rstest]
#[case((0.0, 0.0, 0.0), false)]
#[case((50.0, 0.0, 0.0), false)]
#[case((50.1, 0.0, 0.0), true)]
#[case((80.0, 0.0, 0.0), true)]
fn test_color_is_light(#[case] input: (f32, f32, f32), #[case] expected: bool) {
let color: Color<f32> = Color::new(input.0, input.1, input.2);
let actual = color.is_light();
assert_eq!(actual, expected);
}
#[rstest]
#[case((0.0, 0.0, 0.0), true)]
#[case((50.0, 0.0, 0.0), true)]
#[case((50.1, 0.0, 0.0), false)]
#[case((80.0, 0.0, 0.0), false)]
fn test_color_is_dark(#[case] input: (f32, f32, f32), #[case] expected: bool) {
let color: Color<f32> = Color::new(input.0, input.1, input.2);
let actual = color.is_dark();
assert_eq!(actual, expected);
}
#[test]
fn test_lightness() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.lightness();
assert_approx_eq!(actual, 91.1120);
}
#[test]
fn test_chroma() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.chroma();
assert_approx_eq!(actual, 50.120_117);
}
#[test]
fn test_delta_e() {
let color1: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let color2: Color<f32> = Color::new(53.237_144, 80.088_320, 67.199_460);
let actual = color1.delta_e(&color2);
assert_approx_eq!(actual, 156.460388);
}
#[rstest]
#[case::black((0.0, 0.0, 0.0), 0.0)]
#[case::white((100.0, - 0.002_443, 0.011_384), 102.111_946)]
#[case::red((53.237_144, 80.088_320, 67.199_460), 39.998_900)]
#[case::green((87.735_535, - 86.183_550, 83.179_924), 136.016_020)]
#[case::blue((32.300_800, 79.194_260, - 107.868_910), 306.28503)]
#[case::cyan((91.114_750, - 48.080_950, - 14.142_858), 196.391_080,)]
#[case::magenta((60.322_700, 98.235_580, - 60.842_370), 328.227_940,)]
#[case::yellow((97.138_580, - 21.562_368, 94.476_760), 102.856_380)]
fn test_hue(#[case] input: (f32, f32, f32), #[case] expected: f32) {
let color: Color<f32> = Color::new(input.0, input.1, input.2);
let actual = color.hue();
assert_approx_eq!(actual.to_degrees(), expected, 1e-3);
}
#[test]
fn test_to_hex_string() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_hex_string();
assert_eq!(actual, "#00FFFF");
}
#[test]
fn test_to_rgb() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_rgb();
assert_eq!(actual, RGB::new(0, 255, 255));
}
#[test]
fn test_to_cmyk() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_cmyk();
assert_eq!(actual, CMYK::new(1.0, 0.0, 0.0, 0.0));
}
#[test]
fn test_to_hsl() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_hsl();
assert_approx_eq!(actual.h.to_degrees(), 180.0, 1e-3);
assert_approx_eq!(actual.s, 1.0);
assert_approx_eq!(actual.l, 0.5);
}
#[test]
fn test_to_hsv() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_hsv();
assert_approx_eq!(actual.h.to_degrees(), 180.0, 1e-3);
assert_approx_eq!(actual.s, 1.0);
assert_approx_eq!(actual.v, 1.0);
}
#[test]
fn test_to_xyz() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual: XYZ<f32> = color.to_xyz();
assert_approx_eq!(actual.x, 0.5380, 1e-3);
assert_approx_eq!(actual.y, 0.7873, 1e-3);
assert_approx_eq!(actual.z, 1.0690, 1e-3);
}
#[test]
fn test_to_luv() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_luv();
assert_approx_eq!(actual.l, 91.1120);
assert_approx_eq!(actual.u, -70.480, 1e-3);
assert_approx_eq!(actual.v, -15.240, 1e-3);
}
#[test]
fn test_to_lchuv() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_lchuv();
assert_approx_eq!(actual.l, 91.1120);
assert_approx_eq!(actual.c, 72.109, 1e-3);
assert_approx_eq!(actual.h.to_degrees(), 192.202, 1e-3);
}
#[test]
fn test_to_lab() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_lab();
assert_approx_eq!(actual.l, 91.1120);
assert_approx_eq!(actual.a, -48.0806);
assert_approx_eq!(actual.b, -14.1521);
}
#[test]
fn test_to_oklab() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_oklab();
assert_approx_eq!(actual.l, 0.905, 1e-3);
assert_approx_eq!(actual.a, -0.149, 1e-3);
assert_approx_eq!(actual.b, -0.040, 1e-3);
}
#[test]
fn test_to_oklch() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_oklch();
assert_approx_eq!(actual.l, 0.905, 1e-3);
assert_approx_eq!(actual.c, 0.155, 1e-3);
assert_approx_eq!(actual.h.to_degrees(), 194.82, 1e-3);
}
#[test]
fn test_to_lchab() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_lchab();
assert_approx_eq!(actual.l, 91.1120, 1e-3);
assert_approx_eq!(actual.c, 50.120, 1e-3);
assert_approx_eq!(actual.h.to_degrees(), 196.401, 1e-3);
}
#[test]
fn test_to_anis16() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_ansi16();
assert_eq!(actual, Ansi16::bright_cyan());
}
#[test]
fn test_to_ansi256() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_ansi256();
assert_eq!(actual, Ansi256::new(51));
}
#[test]
fn test_to_rgb_int() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_rgb_int();
assert_eq!(actual, 0x00ffff);
}
#[test]
fn test_to_rgba_int() {
let color: Color<f32> = Color::new(91.1120, -48.0806, -14.1521);
let actual = color.to_rgba_int(128);
assert_eq!(actual, 0x00ffff80);
}
#[test]
fn test_fmt() {
let color: Color<f32> = Color::new(91.114_750, -48.080_950, -14.142_8581);
assert_eq!(
format!("{}", color),
"Color(l: 91.11, a: -48.08, b: -14.14)"
);
}
#[test]
fn test_from_u32() {
let actual: Color<f32> = Color::from(0xff0080);
assert_approx_eq!(actual.l, 54.8888, 1e-3);
assert_approx_eq!(actual.a, 84.5321, 1e-3);
assert_approx_eq!(actual.b, 4.0656, 1e-3);
}
#[rstest]
#[case::black_rgb("#000", 0.0, 0.0, 0.0)]
#[case::white_rgb("#fff", 100.0, - 0.002_443, 0.011_384)]
#[case::red_rgb("#f00", 53.237_144, 80.088_320, 67.199_460)]
#[case::green_rgb("#0f0", 87.735_535, - 86.183_550, 83.179_924)]
#[case::blue_rgb("#00f", 32.300_800, 79.194_260, - 107.868_910)]
#[case::cyan_rgb("#0ff", 91.114_750, - 48.080_950, - 14.142_858)]
#[case::magenta_rgb("#f0f", 60.322_700, 98.235_580, - 60.842_370)]
#[case::yellow_rgb("#ff0", 97.138_580, - 21.562_368, 94.476_760)]
#[case::black_rgba("#0000", 0.0, 0.0, 0.0)]
#[case::white_rgba("#ffff", 100.0, - 0.002_443, 0.011_384)]
#[case::red_rgba("#f00f", 53.237_144, 80.088_320, 67.199_460)]
#[case::green_rgba("#0f0f", 87.735_535, - 86.183_550, 83.179_924)]
#[case::blue_rgba("#00ff", 32.300_800, 79.194_260, - 107.868_910)]
#[case::cyan_rgba("#0fff", 91.114_750, - 48.080_950, - 14.142_858)]
#[case::magenta_rgba("#f0ff", 60.322_700, 98.235_580, - 60.842_370)]
#[case::yellow_rgba("#ff0f", 97.138_580, - 21.562_368, 94.476_760)]
#[case::black_rrggbb("#000000", 0.0, 0.0, 0.0)]
#[case::white_rrggbb("#ffffff", 100.0, - 0.002_443, 0.011_384)]
#[case::red_rrggbb("#ff0000", 53.237_144, 80.088_320, 67.199_460)]
#[case::green_rrggbb("#00ff00", 87.735_535, - 86.183_550, 83.179_924)]
#[case::blue_rrggbb("#0000ff", 32.300_800, 79.194_260, - 107.868_910)]
#[case::cyan_rrggbb("#00ffff", 91.114_750, - 48.080_950, - 14.142_858)]
#[case::magenta_rrggbb("#ff00ff", 60.322_700, 98.235_580, - 60.842_370)]
#[case::yellow_rrggbb("#ffff00", 97.138_580, - 21.562_368, 94.476_760)]
#[case::black_rrggbbaa("#000000ff", 0.0, 0.0, 0.0)]
#[case::white_rrggbbaa("#ffffffff", 100.0, - 0.002_443, 0.011_384)]
#[case::red_rrggbbaa("#ff0000ff", 53.237_144, 80.088_320, 67.199_460)]
#[case::green_rrggbbaa("#00ff00ff", 87.735_535, - 86.183_550, 83.179_924)]
#[case::blue_rrggbbaa("#0000ffff", 32.300_800, 79.194_260, - 107.868_910)]
#[case::cyan_rrggbbaa("#00ffffff", 91.114_750, - 48.080_950, - 14.142_858)]
#[case::magenta_rrggbbaa("#ff00ffff", 60.322_700, 98.235_580, - 60.842_370)]
#[case::yellow_rrggbbaa("#ffff00ff", 97.138_580, - 21.562_368, 94.476_760)]
fn test_from_str(#[case] input: &str, #[case] l: f32, #[case] a: f32, #[case] b: f32) {
let actual: Color<f32> = Color::from_str(input).unwrap();
assert_approx_eq!(actual.l, l, 1e-3);
assert_approx_eq!(actual.a, a, 1e-3);
assert_approx_eq!(actual.b, b, 1e-3);
}
#[rstest]
#[case::empty("")]
#[case::invalid("123456")]
#[case::invalid_length("#12345")]
#[case::invalid_prefix("123456")]
#[case::invalid_rgb_r("#g00")]
#[case::invalid_rgb_g("#0g0")]
#[case::invalid_rgb_b("#00g")]
#[case::invalid_rrggbb_r("#0g0000")]
#[case::invalid_rrggbb_g("#000g00")]
#[case::invalid_rrggbb_b("#00000g")]
#[case::invalid_rrggbbaa_r("#0g000000")]
#[case::invalid_rrggbbaa_g("#000g0000")]
#[case::invalid_rrggbbaa_b("#00000g00")]
#[case::invalid_rrggbbaa_a("#0000000g")]
fn test_from_str_error(#[case] input: &str) {
let actual = Color::<f32>::from_str(input);
assert!(actual.is_err());
assert_eq!(
actual.unwrap_err(),
ColorError::InvalidHexValue(input.to_string())
);
}
}