mod chromaticity;
mod dominant;
pub use chromaticity::Chromaticity;
mod rel_xyz;
pub use rel_xyz::RelXYZ;
#[cfg(feature = "gamut-tables")]
pub use rel_xyz::RelXYZGamut;
use core::f64;
use std::fmt::Display;
use crate::{error::Error, observer::Observer, rgb::RgbSpace, rgb::WideRgb};
use approx::AbsDiffEq;
use nalgebra::{ArrayStorage, Vector3};
#[cfg(target_arch = "wasm32")]
mod wasm;
#[cfg_attr(target_arch = "wasm32", wasm_bindgen::prelude::wasm_bindgen)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct XYZ {
pub(crate) observer: Observer,
pub(crate) xyz: Vector3<f64>, }
impl XYZ {
pub const fn new(xyz: [f64; 3], observer: Observer) -> Self {
let xyz = Vector3::<f64>::from_array_storage(ArrayStorage([xyz]));
Self { observer, xyz }
}
pub const fn from_vec(xyz: Vector3<f64>, observer: Observer) -> XYZ {
Self { observer, xyz }
}
pub fn is_valid(&self) -> bool {
if self.xyz.iter().any(|v| *v < 0.0 || !v.is_finite()) {
return false;
}
self.observer
.spectral_locus()
.contains(self.chromaticity().to_array())
}
pub fn from_chromaticity(
chromaticity: Chromaticity,
l: Option<f64>,
observer: Option<Observer>,
) -> Result<XYZ, Error> {
let [x, y] = chromaticity.to_array();
let l = l.unwrap_or(100.0);
let observer = observer.unwrap_or_default();
if (x + y) > 1.0 + f64::EPSILON || x < 0.0 || y < 0.0 {
Err(Error::InvalidChromaticityValues)
} else {
let scale = l / y;
let xyz = Vector3::new(x * scale, l, (1.0 - x - y) * scale);
Ok(Self::from_vec(xyz, observer))
}
}
pub fn from_luv60(
u: f64,
v: f64,
l: Option<f64>,
observer: Option<Observer>,
) -> Result<XYZ, Error> {
let den = 2.0 * u - 8.0 * v + 4.0;
let x = (3.0 * u) / den;
let y = (2.0 * v) / den;
XYZ::from_chromaticity(Chromaticity::new(x, y), l, observer)
}
pub fn try_add(&self, other: XYZ) -> Result<XYZ, Error> {
if self.observer == other.observer {
let data = self.xyz + other.xyz;
Ok(XYZ::from_vec(data, self.observer))
} else {
Err(Error::RequireSameObserver)
}
}
pub fn x(&self) -> f64 {
self.to_array()[0]
}
pub fn y(&self) -> f64 {
self.to_array()[1]
}
pub fn z(&self) -> f64 {
self.to_array()[2]
}
pub fn observer(&self) -> Observer {
self.observer
}
pub fn to_array(&self) -> [f64; 3] {
*self.xyz.as_ref()
}
pub fn set_illuminance(mut self, illuminance: f64) -> Self {
if self.xyz.y > f64::EPSILON && illuminance > f64::EPSILON {
let s = illuminance / self.xyz.y;
self.xyz.iter_mut().for_each(|v| *v *= s);
self
} else {
XYZ::new([0.0, 0.0, 0.0], self.observer)
}
}
pub fn chromaticity(&self) -> Chromaticity {
let [x, y, z] = self.to_array();
let s = x + y + z;
Chromaticity::new(x / s, y / s)
}
pub fn try_chromaticity(&self) -> Option<Chromaticity> {
let [x, y, z] = self.to_array();
let s = x + y + z;
if s == 0.0 {
None
} else {
Some(Chromaticity::new(x / s, y / s))
}
}
pub fn uv60(&self) -> [f64; 2] {
let &[x, y, z] = self.xyz.as_ref();
let den = x + 15.0 * y + 3.0 * z;
[4.0 * x / den, 6.0 * y / den]
}
pub fn uvw64(&self, xyz_ref: XYZ) -> [f64; 3] {
let yy = self.y();
let [ur, vr] = xyz_ref.uv60();
let [u, v] = self.uv60();
let ww = 25.0 * yy.powf(1.0 / 3.0) - 17.0;
let uu = 13.0 * ww * (u - ur);
let vv = 13.0 * ww * (v - vr);
[uu, vv, ww]
}
pub fn uvprime(&self) -> [f64; 2] {
let &[x, y, z] = self.xyz.as_ref();
let den = x + 15.0 * y + 3.0 * z;
[4.0 * x / den, 9.0 * y / den]
}
pub fn uv_prime_distance(&self, other: &Self) -> f64 {
let [u1, v1] = self.uvprime();
let [u2, v2] = other.uvprime();
(v2 - v1).hypot(u2 - u1)
}
#[cfg(feature = "cct")]
pub fn cct(self) -> Result<crate::illuminant::CCT, Error> {
self.try_into()
}
pub fn rgb(&self, space: RgbSpace) -> WideRgb {
let xyz = self.xyz;
let d = xyz.map(|v| v / 100.0); let data = self.observer.xyz2rgb_matrix(space) * d;
WideRgb {
space,
observer: self.observer,
rgb: data,
}
}
}
impl From<XYZ> for [f64; 3] {
fn from(xyz: XYZ) -> Self {
xyz.to_array()
}
}
impl AbsDiffEq for XYZ {
type Epsilon = f64;
fn default_epsilon() -> Self::Epsilon {
f64::default_epsilon()
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
self.observer == other.observer && self.xyz.abs_diff_eq(&other.xyz, epsilon)
}
}
impl approx::UlpsEq for XYZ {
fn default_max_ulps() -> u32 {
f64::default_max_ulps()
}
fn ulps_eq(&self, other: &Self, epsilon: Self::Epsilon, max_ulps: u32) -> bool {
self.observer == other.observer && self.xyz.ulps_eq(&other.xyz, epsilon, max_ulps)
}
}
impl std::ops::Mul<f64> for XYZ {
type Output = XYZ;
fn mul(mut self, rhs: f64) -> Self::Output {
self.xyz *= rhs;
self
}
}
impl std::ops::Mul<XYZ> for f64 {
type Output = XYZ;
fn mul(self, mut rhs: XYZ) -> Self::Output {
rhs.xyz *= self;
rhs
}
}
impl std::ops::Add<XYZ> for XYZ {
type Output = XYZ;
fn add(mut self, rhs: XYZ) -> Self::Output {
assert!(
self.observer == rhs.observer,
"Cannot add XYZ values with different observers ({:?} vs {:?})",
self.observer,
rhs.observer
);
self.xyz += rhs.xyz;
self
}
}
impl std::ops::Sub<XYZ> for XYZ {
type Output = XYZ;
fn sub(mut self, rhs: XYZ) -> Self::Output {
assert!(
self.observer == rhs.observer,
"Cannot subtract XYZ values with different observers ({:?} vs {:?})",
self.observer,
rhs.observer
);
self.xyz -= rhs.xyz;
self
}
}
impl Display for XYZ {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[{:.3}, {:.3}, {:.3}] ({})",
self.x(),
self.y(),
self.z(),
self.observer
)
}
}
#[cfg(test)]
mod xyz_test {
use crate::{
observer::Observer::{self, Cie1931},
rgb::{RgbSpace, WideRgb},
xyz::XYZ,
};
use approx::assert_ulps_eq;
#[test]
fn xyz_d65_test() {
let d65 = Cie1931.xyz_d65();
let xyz: [f64; 3] = d65.into();
println!("{xyz:?}");
assert_ulps_eq!(
xyz.as_ref(),
[95.04, 100.0, 108.867].as_slice(),
epsilon = 1E-2
);
let xyz = d65.to_array();
assert_ulps_eq!(
xyz.as_ref(),
[95.04, 100.0, 108.867].as_slice(),
epsilon = 1E-2
);
}
#[test]
fn test_rgb_roundtrip() {
use crate::rgb::RgbSpace::SRGB;
let rgb_blue = WideRgb::new(0.0, 0.0, 1.0, Some(Observer::Cie1931), Some(RgbSpace::SRGB));
let xyz_blue = rgb_blue.xyz();
let xy_blue = xyz_blue.chromaticity().to_array();
assert_ulps_eq!(xy_blue.as_ref(), [0.15, 0.06].as_ref(), epsilon = 1E-5);
let rgbb = xyz_blue.rgb(SRGB);
assert_ulps_eq!(rgbb, rgb_blue, epsilon = 1E-6);
}
#[test]
fn ulps_xyz_test() {
use approx::assert_ulps_eq;
use nalgebra::Vector3;
let xyz0 = XYZ::from_vec(Vector3::zeros(), Observer::Cie1931);
let xyz1 = XYZ::from_vec(Vector3::new(0.0, 0.0, f64::EPSILON), Observer::Cie1931);
assert_ulps_eq!(xyz0, xyz1, epsilon = 1E-5);
let xyz2 = XYZ::from_vec(
Vector3::new(0.0, 0.0, 2.0 * f64::EPSILON),
Observer::Cie1931,
);
approx::assert_ulps_ne!(xyz0, xyz2);
let xyz3 = XYZ::from_vec(Vector3::zeros(), Observer::Cie1964);
approx::assert_ulps_ne!(xyz0, xyz3);
}
}