#![cfg_attr(not(feature = "std"), no_std)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, allow(unused_attributes))]
#![deny(missing_docs)]
#![deny(unsafe_code)]
mod generated;
mod nearest;
pub use generated::{COLORS, Family, Kind};
#[doc(hidden)]
pub mod __bench {
pub use crate::nearest::{
cie94::{delta_e_94_sq, nearest_idx as cie94_nearest_idx},
ciede2000::{
delta_e_2000_sq, nearest_idx as ciede2000_nearest_idx,
nearest_idx_prefiltered as ciede2000_prefiltered_nearest_idx,
},
scalar::{delta_e_76_sq, nearest_idx as scalar_nearest_idx},
};
#[cfg(feature = "lut")]
pub use crate::nearest::ciede2000_lut::nearest_idx as ciede2000_lut_nearest_idx;
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
pub use crate::nearest::cie94_aarch64_neon::nearest_idx as cie94_aarch64_neon_nearest_idx;
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
pub use crate::nearest::cie94_wasm_simd128::nearest_idx as cie94_wasm_simd128_nearest_idx;
#[cfg(target_arch = "x86_64")]
pub use crate::nearest::cie94_x86_avx2::nearest_idx as cie94_x86_avx2_nearest_idx;
#[cfg(target_arch = "x86_64")]
pub use crate::nearest::cie94_x86_avx512::nearest_idx as cie94_x86_avx512_nearest_idx;
#[cfg(target_arch = "x86_64")]
pub use crate::nearest::cie94_x86_sse41::nearest_idx as cie94_x86_sse41_nearest_idx;
#[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
pub use crate::nearest::aarch64_neon::nearest_idx as aarch64_neon_nearest_idx;
#[cfg(target_arch = "x86_64")]
pub use crate::nearest::x86_avx2::nearest_idx as x86_avx2_nearest_idx;
#[cfg(target_arch = "x86_64")]
pub use crate::nearest::x86_avx512::nearest_idx as x86_avx512_nearest_idx;
#[cfg(target_arch = "x86_64")]
pub use crate::nearest::x86_sse41::nearest_idx as x86_sse41_nearest_idx;
#[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
pub use crate::nearest::wasm_simd128::nearest_idx as wasm_simd128_nearest_idx;
pub fn rgb_to_lab(rgb: [u8; 3]) -> [f32; 3] {
super::rgb_to_lab(rgb)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub(crate) name: &'static str,
pub(crate) hex: &'static str,
pub(crate) rgb: [u8; 3],
pub(crate) lab: [f32; 3],
pub(crate) design_name: &'static str,
pub(crate) design_hex: &'static str,
pub(crate) design_rgb: [u8; 3],
pub(crate) common_name: &'static str,
pub(crate) common_hex: &'static str,
pub(crate) common_rgb: [u8; 3],
pub(crate) family: Family,
pub(crate) kind: Kind,
pub(crate) is_neutral: bool,
}
impl Color {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn name(&self) -> &'static str {
self.name
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn hex(&self) -> &'static str {
self.hex
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn rgb(&self) -> [u8; 3] {
self.rgb
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn lab(&self) -> [f32; 3] {
self.lab
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn design_name(&self) -> &'static str {
self.design_name
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn design_hex(&self) -> &'static str {
self.design_hex
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn design_rgb(&self) -> [u8; 3] {
self.design_rgb
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn common_name(&self) -> &'static str {
self.common_name
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn common_hex(&self) -> &'static str {
self.common_hex
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn common_rgb(&self) -> [u8; 3] {
self.common_rgb
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn family(&self) -> Family {
self.family
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn kind(&self) -> Kind {
self.kind
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn is_neutral(&self) -> bool {
self.is_neutral
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn all() -> &'static [&'static Color] {
COLORS
}
pub fn nearest_to(rgb: [u8; 3]) -> &'static Color {
crate::nearest::nearest(rgb_to_lab(rgb))
}
pub fn nearest_to_ciede2000(rgb: [u8; 3]) -> &'static Color {
crate::nearest::nearest_ciede2000(rgb)
}
pub fn nearest_to_ciede2000_exact(rgb: [u8; 3]) -> &'static Color {
crate::nearest::nearest_ciede2000(rgb)
}
pub fn nearest_to_cie94(rgb: [u8; 3]) -> &'static Color {
crate::nearest::nearest_cie94(rgb_to_lab(rgb))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
#[non_exhaustive]
#[repr(u8)]
pub enum Algorithm {
DeltaE76,
Cie94,
Ciede2000,
#[default]
Ciede2000Exact,
}
impl Algorithm {
#[inline]
pub fn extract(&self, rgb: [u8; 3]) -> &'static Color {
match self {
Self::DeltaE76 => Color::nearest_to(rgb),
Self::Cie94 => Color::nearest_to_cie94(rgb),
Self::Ciede2000 => Color::nearest_to_ciede2000(rgb),
Self::Ciede2000Exact => Color::nearest_to_ciede2000_exact(rgb),
}
}
#[inline]
pub const fn as_str(&self) -> &'static str {
match self {
Self::DeltaE76 => "delta-e-76",
Self::Cie94 => "cie94",
Self::Ciede2000 => "ciede2000",
Self::Ciede2000Exact => "ciede2000-exact",
}
}
}
pub(crate) fn rgb_to_lab(rgb: [u8; 3]) -> [f32; 3] {
let r = srgb_to_linear(rgb[0] as f32 / 255.0);
let g = srgb_to_linear(rgb[1] as f32 / 255.0);
let b = srgb_to_linear(rgb[2] as f32 / 255.0);
let x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375;
let y = r * 0.2126729 + g * 0.7151522 + b * 0.072175;
let z = r * 0.0193339 + g * 0.119192 + b * 0.9503041;
const XN: f32 = 0.95047;
const YN: f32 = 1.00000;
const ZN: f32 = 1.08883;
let fx = lab_f(x / XN);
let fy = lab_f(y / YN);
let fz = lab_f(z / ZN);
let l = 116.0 * fy - 16.0;
let a = 500.0 * (fx - fy);
let b_lab = 200.0 * (fy - fz);
[l, a, b_lab]
}
fn srgb_to_linear(c: f32) -> f32 {
if c <= 0.04045 {
c / 12.92
} else {
libm::powf((c + 0.055) / 1.055, 2.4)
}
}
fn lab_f(t: f32) -> f32 {
const DELTA_CUBED: f32 = 216.0 / 24389.0; const KAPPA_OVER_3: f32 = 841.0 / 108.0; const OFFSET: f32 = 4.0 / 29.0;
if t > DELTA_CUBED {
libm::cbrtf(t)
} else {
KAPPA_OVER_3 * t + OFFSET
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dataset_is_non_empty() {
assert!(!COLORS.is_empty());
}
#[test]
fn dataset_entry_count_matches_csv() {
assert_eq!(
COLORS.len(),
949,
"regenerate via `cargo xtask codegen` if the upstream CSV changed",
);
}
#[test]
fn nearest_to_pure_red_is_in_red_family() {
let c = Color::nearest_to([255, 0, 0]);
assert!(
c.family().as_str().contains("red") || c.name().contains("red"),
"nearest to pure red was name={:?} family={:?}",
c.name(),
c.family().as_str(),
);
}
#[test]
fn nearest_to_pure_blue_is_in_blue_family() {
let c = Color::nearest_to([0, 0, 255]);
assert!(
c.family().as_str().contains("blue") || c.name().contains("blue"),
"nearest to pure blue was name={:?} family={:?}",
c.name(),
c.family().as_str(),
);
}
#[test]
fn nearest_to_mid_gray_is_neutral() {
let c = Color::nearest_to([128, 128, 128]);
assert!(
c.is_neutral(),
"nearest to (128,128,128) was name={:?} is_neutral={}",
c.name(),
c.is_neutral(),
);
}
#[test]
fn rgb_to_lab_d65_white() {
let lab = rgb_to_lab([255, 255, 255]);
assert!((lab[0] - 100.0).abs() < 0.01, "L was {}", lab[0]);
assert!(lab[1].abs() < 0.01, "a was {}", lab[1]);
assert!(lab[2].abs() < 0.01, "b was {}", lab[2]);
}
#[test]
fn rgb_to_lab_black() {
let lab = rgb_to_lab([0, 0, 0]);
assert!(lab[0].abs() < 1e-6, "L was {}", lab[0]);
assert!(lab[1].abs() < 1e-6, "a was {}", lab[1]);
assert!(lab[2].abs() < 1e-6, "b was {}", lab[2]);
}
}