use crate::error::{MunsellError, Result};
use crate::iscc::{ColorMetadata, IsccNbsClassifier};
use crate::semantic_overlay::{parse_munsell_notation, MunsellSpec};
use crate::types::MunsellColor;
use crate::unified_cache::hex_to_rgb;
use crate::MunsellConverter;
#[allow(deprecated)]
use crate::semantic_overlay::{closest_overlay, matching_overlays, semantic_overlay};
use super::characterization::ColorCharacterization;
use super::descriptor::ColorDescriptor;
use super::modifier::ColorModifier;
pub struct ColorClassifier {
converter: MunsellConverter,
iscc: IsccNbsClassifier,
}
impl ColorClassifier {
pub fn new() -> Result<Self> {
Ok(Self {
converter: MunsellConverter::new()?,
iscc: IsccNbsClassifier::new()?,
})
}
pub fn classify_srgb(&self, rgb: [u8; 3]) -> Result<ColorDescriptor> {
let munsell = self.converter.srgb_to_munsell(rgb)?;
self.classify_munsell_color(&munsell)
}
pub fn classify_hex(&self, hex: &str) -> Result<ColorDescriptor> {
let rgb = hex_to_rgb(hex)?;
self.classify_srgb(rgb)
}
pub fn classify_lab(&self, lab: [f64; 3]) -> Result<ColorDescriptor> {
let munsell = self.converter.lab_to_munsell(lab)?;
self.classify_munsell_color(&munsell)
}
pub fn classify_munsell(&self, notation: &str) -> Result<ColorDescriptor> {
let munsell = MunsellColor::from_notation(notation)?;
self.classify_munsell_color(&munsell)
}
pub fn characterize_srgb(&self, rgb: [u8; 3]) -> Result<ColorCharacterization> {
let munsell = self.converter.srgb_to_munsell(rgb)?;
self.characterize_munsell_color(&munsell)
}
pub fn characterize_hex(&self, hex: &str) -> Result<ColorCharacterization> {
let rgb = hex_to_rgb(hex)?;
self.characterize_srgb(rgb)
}
pub fn characterize_lab(&self, lab: [f64; 3]) -> Result<ColorCharacterization> {
let munsell = self.converter.lab_to_munsell(lab)?;
self.characterize_munsell_color(&munsell)
}
pub fn characterize_munsell_notation(&self, notation: &str) -> Result<ColorCharacterization> {
let munsell = MunsellColor::from_notation(notation)?;
self.characterize_munsell_color(&munsell)
}
#[allow(deprecated)] fn characterize_munsell_color(&self, munsell: &MunsellColor) -> Result<ColorCharacterization> {
let (iscc_number, iscc_meta) = self.get_iscc_classification(munsell)?;
let munsell_spec = self.munsell_color_to_spec(munsell);
let (semantic_matches, nearest) = if let Some(ref spec) = munsell_spec {
let all_matches: Vec<String> =
matching_overlays(spec).iter().map(|s| s.to_string()).collect();
let nearest = closest_overlay(spec).map(|(name, dist)| (name.to_string(), dist));
(all_matches, nearest)
} else {
(vec![], None)
};
let modifier = iscc_meta
.iscc_nbs_formatter
.as_ref()
.map(|f| ColorModifier::from_formatter(f))
.unwrap_or(ColorModifier::None);
Ok(ColorCharacterization {
munsell: munsell_spec.unwrap_or_else(|| MunsellSpec::new(0.0, munsell.value, 0.0)),
iscc_nbs_number: iscc_number,
iscc_base_color: iscc_meta.iscc_nbs_color_name.clone(),
iscc_extended_name: iscc_meta.alt_color_name.clone(),
modifier,
semantic_matches,
nearest_semantic: nearest,
shade: iscc_meta.color_shade.clone(),
})
}
fn classify_munsell_color(&self, munsell: &MunsellColor) -> Result<ColorDescriptor> {
let char = self.characterize_munsell_color(munsell)?;
let semantic_name = char.semantic_matches.first().cloned();
let semantic_alternates: Vec<String> =
char.semantic_matches.into_iter().skip(1).collect();
Ok(ColorDescriptor {
iscc_nbs_number: char.iscc_nbs_number,
modifier: char.modifier,
standard_name: char.iscc_base_color,
extended_name: char.iscc_extended_name,
semantic_name,
semantic_alternates,
nearest_semantic: char.nearest_semantic,
shade: char.shade,
})
}
#[allow(deprecated)] pub fn semantic_name(&self, rgb: [u8; 3]) -> Result<Option<String>> {
let munsell = self.converter.srgb_to_munsell(rgb)?;
if let Some(spec) = self.munsell_color_to_spec(&munsell) {
Ok(semantic_overlay(&spec).map(|s| s.to_string()))
} else {
Ok(None)
}
}
#[allow(deprecated)] pub fn semantic_matches(&self, rgb: [u8; 3]) -> Result<Vec<String>> {
let munsell = self.converter.srgb_to_munsell(rgb)?;
if let Some(spec) = self.munsell_color_to_spec(&munsell) {
Ok(matching_overlays(&spec)
.iter()
.map(|s| s.to_string())
.collect())
} else {
Ok(vec![])
}
}
pub fn all_iscc_matches(&self, rgb: [u8; 3]) -> Result<Vec<u16>> {
let munsell = self.converter.srgb_to_munsell(rgb)?;
if let (Some(hue), Some(chroma)) = (&munsell.hue, munsell.chroma) {
self.iscc
.find_all_colors_at_point(hue, munsell.value, chroma)
} else {
Ok(vec![])
}
}
fn get_iscc_classification(&self, munsell: &MunsellColor) -> Result<(u16, ColorMetadata)> {
if let (Some(hue), Some(chroma)) = (&munsell.hue, munsell.chroma) {
let color_numbers = self
.iscc
.find_all_colors_at_point(hue, munsell.value, chroma)?;
if let Some(&color_number) = color_numbers.first() {
if let Some(metadata) = self.iscc.classify_munsell(hue, munsell.value, chroma)? {
return Ok((color_number, metadata));
}
}
}
if munsell.is_neutral() {
let neutral_name = self.get_neutral_name(munsell.value);
let metadata = ColorMetadata {
iscc_nbs_color_name: neutral_name.to_string(),
iscc_nbs_formatter: None,
alt_color_name: neutral_name.to_string(),
color_shade: neutral_name.to_string(),
};
let color_number = self.get_neutral_color_number(munsell.value);
return Ok((color_number, metadata));
}
Err(MunsellError::ConversionError {
message: format!(
"Could not classify Munsell color: {}",
munsell.notation
),
})
}
fn get_neutral_name(&self, value: f64) -> &'static str {
if value <= 0.5 {
"black"
} else if value >= 9.5 {
"white"
} else {
"gray"
}
}
fn get_neutral_color_number(&self, value: f64) -> u16 {
if value <= 0.5 {
267 } else if value >= 9.5 {
263 } else if value >= 7.5 {
264 } else if value >= 4.5 {
265 } else {
266 }
}
fn munsell_color_to_spec(&self, munsell: &MunsellColor) -> Option<MunsellSpec> {
parse_munsell_notation(&munsell.notation)
}
}
unsafe impl Send for ColorClassifier {}
unsafe impl Sync for ColorClassifier {}
#[cfg(test)]
mod tests {
use super::*;
fn classifier() -> ColorClassifier {
ColorClassifier::new().expect("Failed to create classifier")
}
#[test]
fn test_classify_srgb_red() {
let c = classifier();
let desc = c.classify_srgb([255, 0, 0]).expect("Classification failed");
assert!(desc.standard_name.contains("red") || desc.shade == "red");
assert!(desc.modifier.is_vivid() || desc.modifier == ColorModifier::Strong);
}
#[test]
fn test_classify_srgb_blue() {
let c = classifier();
let desc = c.classify_srgb([0, 0, 255]).expect("Classification failed");
assert!(desc.standard_name.contains("blue") || desc.shade == "blue");
}
#[test]
fn test_classify_hex() {
let c = classifier();
let desc = c.classify_hex("#FF0000").expect("Classification failed");
assert!(desc.standard_name.contains("red") || desc.shade == "red");
}
#[test]
fn test_classify_hex_short() {
let c = classifier();
let desc = c.classify_hex("#F00").expect("Classification failed");
assert!(desc.standard_name.contains("red") || desc.shade == "red");
}
#[test]
fn test_classify_munsell_notation() {
let c = classifier();
let desc = c.classify_munsell("5R 4/10").expect("Classification failed");
assert!(desc.standard_name.contains("red") || desc.shade == "red");
}
#[test]
fn test_semantic_name_coral_region() {
let c = classifier();
let name = c.semantic_name([255, 127, 80]).expect("Classification failed");
if let Some(n) = name {
assert!(
n == "coral" || n == "rose" || n == "peach" || n == "orange" || n == "pink",
"Expected coral-like color, got: {}",
n
);
}
}
#[test]
fn test_semantic_matches() {
let c = classifier();
let matches = c
.semantic_matches([200, 100, 80])
.expect("Classification failed");
assert!(matches.len() <= 10); }
#[test]
fn test_descriptor_formatting() {
let c = classifier();
let desc = c.classify_srgb([200, 50, 50]).expect("Classification failed");
let standard = desc.standard_descriptor();
assert!(!standard.is_empty());
let extended = desc.extended_descriptor();
assert!(!extended.is_empty());
}
#[test]
fn test_modifier_extraction() {
let c = classifier();
let desc = c.classify_srgb([255, 0, 0]).expect("Classification failed");
assert!(
desc.modifier.is_vivid() || desc.modifier == ColorModifier::Strong,
"Expected vivid/strong modifier for pure red, got: {:?}",
desc.modifier
);
}
#[test]
fn test_shade_extraction() {
let c = classifier();
let desc = c.classify_srgb([255, 0, 0]).expect("Classification failed");
assert!(!desc.shade.is_empty());
}
#[test]
fn test_gray_classification() {
let c = classifier();
let desc = c.classify_srgb([128, 128, 128]).expect("Classification failed");
assert!(
desc.standard_name.contains("gray") || desc.standard_name.contains("grey"),
"Expected gray, got: {}",
desc.standard_name
);
}
#[test]
fn test_white_classification() {
let c = classifier();
let desc = c.classify_srgb([255, 255, 255]).expect("Classification failed");
assert!(
desc.standard_name == "white"
|| desc.standard_name.contains("white")
|| desc.shade == "white",
"Expected white, got: {}",
desc.standard_name
);
}
#[test]
fn test_black_classification() {
let c = classifier();
let desc = c.classify_srgb([0, 0, 0]).expect("Classification failed");
assert!(
desc.standard_name == "black"
|| desc.standard_name.contains("black")
|| desc.shade == "black",
"Expected black, got: {}",
desc.standard_name
);
}
#[test]
fn test_has_semantic_match() {
let c = classifier();
let desc = c.classify_srgb([255, 100, 100]).expect("Classification failed");
let _ = desc.has_semantic_match();
}
#[test]
fn test_nearest_semantic() {
let c = classifier();
let desc = c.classify_srgb([200, 100, 80]).expect("Classification failed");
if let Some((name, dist)) = desc.nearest_semantic.as_ref() {
assert!(!name.is_empty());
assert!(*dist >= 0.0);
}
}
#[test]
fn test_all_iscc_matches() {
let c = classifier();
let matches = c
.all_iscc_matches([200, 100, 80])
.expect("Classification failed");
assert!(!matches.is_empty());
}
#[test]
fn test_display_trait() {
let c = classifier();
let desc = c.classify_srgb([180, 80, 60]).expect("Classification failed");
let display = format!("{}", desc);
assert_eq!(display, desc.standard_descriptor());
}
}