diff --git a/palette/src/chromatic_adaptation.rs b/palette/src/chromatic_adaptation.rs index 24e56bf3f..d9364400e 100644 --- a/palette/src/chromatic_adaptation.rs +++ b/palette/src/chromatic_adaptation.rs @@ -26,6 +26,10 @@ use crate::{ convert::{FromColorUnclamped, IntoColorUnclamped}, + lms::{ + self, + meta::{LmsToXyz, XyzToLms}, + }, matrix::{multiply_3x3, multiply_xyz, Mat3}, num::{Arithmetics, Real, Zero}, white_point::{Any, WhitePoint}, @@ -95,44 +99,20 @@ where match *self { Method::Bradford => { ConeResponseMatrices:: { - ma: [ - T::from_f64(0.8951000), T::from_f64(0.2664000), T::from_f64(-0.1614000), - T::from_f64(-0.7502000), T::from_f64(1.7135000), T::from_f64(0.0367000), - T::from_f64(0.0389000), T::from_f64(-0.0685000), T::from_f64(1.0296000) - ], - inv_ma: [ - T::from_f64(0.9869929), T::from_f64(-0.1470543), T::from_f64(0.1599627), - T::from_f64(0.4323053), T::from_f64(0.5183603), T::from_f64(0.0492912), - T::from_f64(-0.0085287), T::from_f64(0.0400428), T::from_f64(0.9684867) - ], + ma: lms::meta::Bradford::xyz_to_lms_matrix(), + inv_ma: lms::meta::Bradford::lms_to_xyz_matrix(), } } Method::VonKries => { ConeResponseMatrices:: { - ma: [ - T::from_f64(0.4002400), T::from_f64(0.7076000), T::from_f64(-0.0808100), - T::from_f64(-0.2263000), T::from_f64(1.1653200), T::from_f64(0.0457000), - T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(0.9182200) - ], - inv_ma: [ - T::from_f64(1.8599364), T::from_f64(-1.1293816), T::from_f64(0.2198974), - T::from_f64(0.3611914), T::from_f64(0.6388125), T::from_f64(-0.0000064), - T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(1.0890636) - ], + ma: lms::meta::VonKries::xyz_to_lms_matrix(), + inv_ma: lms::meta::VonKries::lms_to_xyz_matrix(), } } Method::XyzScaling => { ConeResponseMatrices:: { - ma: [ - T::from_f64(1.0000000), T::from_f64(0.0000000), T::from_f64(0.0000000), - T::from_f64(0.0000000), T::from_f64(1.0000000), T::from_f64(0.0000000), - T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(1.0000000) - ], - inv_ma: [ - T::from_f64(1.0000000), T::from_f64(0.0000000), T::from_f64(0.0000000), - T::from_f64(0.0000000), T::from_f64(1.0000000), T::from_f64(0.0000000), - T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(1.0000000) - ], + ma: lms::meta::UnitMatrix::xyz_to_lms_matrix(), + inv_ma: lms::meta::UnitMatrix::lms_to_xyz_matrix(), } } } diff --git a/palette/src/lib.rs b/palette/src/lib.rs index 847207af4..bf0d9a41a 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -359,6 +359,7 @@ pub mod hwb; pub mod lab; pub mod lch; pub mod lchuv; +pub mod lms; pub mod luma; pub mod luv; mod luv_bounds; diff --git a/palette/src/lms.rs b/palette/src/lms.rs new file mode 100644 index 000000000..2328a60b5 --- /dev/null +++ b/palette/src/lms.rs @@ -0,0 +1,22 @@ +//! Types for the LMS color space. + +#[allow(clippy::module_inception)] +mod lms; + +pub mod meta; + +use crate::Alpha; + +pub use self::lms::*; + +/// LMS that uses the von Kries matrix. +pub type VonKriesLms = Lms, T>; + +/// LMSA that uses the von Kries matrix. +pub type VonKriesLmsa = Alpha, T>, T>; + +/// LMS that uses the Bradford matrix. +pub type BradfordLms = Lms, T>; + +/// LMSA that uses the Bradford matrix. +pub type BradfordLmsa = Alpha, T>, T>; diff --git a/palette/src/lms/lms.rs b/palette/src/lms/lms.rs new file mode 100644 index 000000000..0314ad053 --- /dev/null +++ b/palette/src/lms/lms.rs @@ -0,0 +1,427 @@ +use core::marker::PhantomData; + +use crate::{ + bool_mask::HasBoolMask, + convert::FromColorUnclamped, + matrix::multiply_3x3_and_vec3, + num::{Arithmetics, Zero}, + stimulus::{FromStimulus, Stimulus, StimulusColor}, + xyz::meta::HasXyzMeta, + Alpha, Xyz, +}; + +use super::meta::{HasLmsMatrix, XyzToLms}; + +/// Generic LMS with an alpha component. See [`Lmsa` implementation in +/// `Alpha`][crate::Alpha#Lmsa]. +pub type Lmsa = Alpha, T>; + +/// Generic LMS. +/// +/// LMS represents the response of the eye's cone cells. L, M and S are for +/// "long", "medium" and "short" wavelengths, roughly corresponding to red, +/// green and blue. Many newer mentions of an LMS representation use the letters +/// R, G and B instead (or sometimes ρ, γ, β), but this library sticks to LMS to +/// differentiate it from [`Rgb`][crate::rgb::Rgb]. +/// +/// The LMS color space is a model of the physiological response to color +/// stimuli. It has some mathematical shortcomings that [`Xyz`] improves on, +/// such as severe spectral sensitivity overlap between L, M and S. Despite +/// this, LMS has a lot of uses, include chromatic adaptation and emulating +/// different types of color vision deficiency, and it's sometimes part of the +/// conversion process between other color spaces. +/// +/// # Creating a Value +/// +/// An LMS value is often derived from another color space, through a conversion +/// matrix. Two such matrices are [`VonKries`][super::meta::VonKries] and +/// [`Bradford`][super::meta::Bradford], and Palette offers type aliases in the +/// [`lms`][crate::lms] module to make using them a bit more convenient. It's of +/// course also possible to simply use [`Lms::new`], but it may not be as +/// intuitive. +/// +/// ``` +/// use palette::{ +/// lms::{Lms, VonKriesLms, meta::VonKries}, +/// white_point::D65, +/// Srgb, FromColor +/// }; +/// +/// let von_kries_lms = Lms::::new(0.1, 0.2, 0.3); +/// let von_kries_d65_lms = VonKriesLms::::new(0.1, 0.2, 0.3); +/// +/// // `new` is also `const`: +/// const VON_KRIES_LMS: Lms = Lms::new(0.1, 0.2, 0.3); +/// +/// // Von Kries LMS from sRGB: +/// let lms_from_srgb = VonKriesLms::::from_color(Srgb::new(0.3f32, 0.8, 0.1)); +/// ``` +#[derive(Debug, ArrayCast, FromColorUnclamped, WithAlpha)] +#[cfg_attr(feature = "serializing", derive(Serialize, Deserialize))] +#[palette(palette_internal, component = "T", skip_derives(Xyz))] +#[repr(C)] +pub struct Lms { + /// Stimulus from long wavelengths, or red, or ρ. The typical range is + /// between 0.0 and 1.0, but it doesn't have an actual upper bound. + pub long: T, + + /// Stimulus from medium wavelengths, or green, or γ. The typical range is + /// between 0.0 and 1.0, but it doesn't have an actual upper bound. + pub medium: T, + + /// Stimulus from short wavelengths, or blue, or β. The typical range is + /// between 0.0 and 1.0, but it doesn't have an actual upper bound. + pub short: T, + + /// Type level meta information, such as reference white, or which matrix + /// was used when converting from XYZ. + #[cfg_attr(feature = "serializing", serde(skip))] + #[palette(unsafe_zero_sized)] + pub meta: PhantomData, +} + +impl Lms { + /// Create a new LMS color. + pub const fn new(long: T, medium: T, short: T) -> Self { + Self { + long, + medium, + short, + meta: PhantomData, + } + } + + /// Convert the LMS components into another number type. + /// + /// ``` + /// use palette::{ + /// lms::VonKriesLms, + /// white_point::D65, + /// }; + /// + /// let lms_f64: VonKriesLms = VonKriesLms::new(0.3f32, 0.7, 0.2).into_format(); + /// ``` + pub fn into_format(self) -> Lms + where + U: FromStimulus, + { + Lms { + long: U::from_stimulus(self.long), + medium: U::from_stimulus(self.medium), + short: U::from_stimulus(self.short), + meta: PhantomData, + } + } + + /// Convert the LMS components from another number type. + /// + /// ``` + /// use palette::{ + /// lms::VonKriesLms, + /// white_point::D65, + /// }; + /// + /// let lms_f64 = VonKriesLms::::from_format(VonKriesLms::new(0.3f32, 0.7, 0.2)); + /// ``` + pub fn from_format(color: Lms) -> Self + where + T: FromStimulus, + { + color.into_format() + } + + /// Convert to a `(long, medium, short)` tuple. + pub fn into_components(self) -> (T, T, T) { + (self.long, self.medium, self.short) + } + + /// Convert from a `(long, medium, short)` tuple. + pub fn from_components((long, medium, short): (T, T, T)) -> Self { + Self::new(long, medium, short) + } + + /// Changes the meta type without changing the color value. + /// + /// This function doesn't change the numerical values, and thus the stimuli + /// it represents in an absolute sense. However, the appearance of the color + /// may not be the same. The effect may be similar to taking a photo with an + /// incorrect white balance. + pub fn with_meta(self) -> Lms { + Lms { + long: self.long, + medium: self.medium, + short: self.short, + meta: PhantomData, + } + } +} + +impl Lms +where + T: Zero, +{ + /// Return the `short` value minimum. + pub fn min_short() -> T { + T::zero() + } + + /// Return the `medium` value minimum. + pub fn min_medium() -> T { + T::zero() + } + + /// Return the `long` value minimum. + pub fn min_long() -> T { + T::zero() + } +} + +/// [`Lmsa`][Lmsa] implementations. +impl Alpha, A> { + /// Create an LMSA color. + pub const fn new(red: T, green: T, blue: T, alpha: A) -> Self { + Alpha { + color: Lms::new(red, green, blue), + alpha, + } + } + + /// Convert the LMSA components into other number types. + /// + /// ``` + /// use palette::{ + /// lms::VonKriesLmsa, + /// white_point::D65, + /// }; + /// + /// let lmsa_f64: VonKriesLmsa = VonKriesLmsa::new(0.3f32, 0.7, 0.2, 0.5).into_format(); + /// ``` + pub fn into_format(self) -> Alpha, B> + where + U: FromStimulus, + B: FromStimulus, + { + Alpha { + color: self.color.into_format(), + alpha: B::from_stimulus(self.alpha), + } + } + + /// Convert the LMSA components from other number types. + /// + /// ``` + /// use palette::{ + /// lms::VonKriesLmsa, + /// white_point::D65, + /// }; + /// + /// let lmsa_f64 = VonKriesLmsa::::from_format(VonKriesLmsa::new(0.3f32, 0.7, 0.2, 0.5)); + /// ``` + pub fn from_format(color: Alpha, B>) -> Self + where + T: FromStimulus, + A: FromStimulus, + { + color.into_format() + } + + /// Convert to a `(long, medium, short, alpha)` tuple. + pub fn into_components(self) -> (T, T, T, A) { + ( + self.color.long, + self.color.medium, + self.color.short, + self.alpha, + ) + } + + /// Convert from a `(long, medium, short, alpha)` tuple. + pub fn from_components((long, medium, short, alpha): (T, T, T, A)) -> Self { + Self::new(long, medium, short, alpha) + } + + /// Changes the meta type without changing the color value. + /// + /// This function doesn't change the numerical values, and thus the stimuli + /// it represents in an absolute sense. However, the appearance of the color + /// may not be the same. The effect may be similar to taking a photo with an + /// incorrect white balance. + pub fn with_meta(self) -> Alpha, A> { + Alpha { + color: self.color.with_meta(), + alpha: self.alpha, + } + } +} + +impl FromColorUnclamped> for Lms +where + M: HasLmsMatrix + HasXyzMeta, + M::LmsMatrix: XyzToLms, + T: Arithmetics, +{ + fn from_color_unclamped(val: Xyz) -> Self { + multiply_3x3_and_vec3(M::LmsMatrix::xyz_to_lms_matrix(), val.into()).into() + } +} + +impl StimulusColor for Lms where T: Stimulus {} + +impl HasBoolMask for Lms +where + T: HasBoolMask, +{ + type Mask = T::Mask; +} + +impl Default for Lms +where + T: Default, +{ + fn default() -> Lms { + Lms::new(T::default(), T::default(), T::default()) + } +} + +impl From> for Lms { + #[inline] + fn from(color: Lms) -> Self { + color.into_format() + } +} + +impl From> for Lmsa { + #[inline] + fn from(color: Lmsa) -> Self { + color.into_format() + } +} + +impl From> for Lms { + #[inline] + fn from(color: Lms) -> Self { + color.into_format() + } +} + +impl From> for Lmsa { + #[inline] + fn from(color: Lmsa) -> Self { + color.into_format() + } +} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Zeroable for Lms where T: bytemuck::Zeroable {} + +#[cfg(feature = "bytemuck")] +unsafe impl bytemuck::Pod for Lms where T: bytemuck::Pod {} + +impl_reference_component_methods!(Lms, [long, medium, short], meta); +impl_struct_of_arrays_methods!(Lms, [long, medium, short], meta); + +impl_is_within_bounds! { + Lms { + long => [Self::min_long(), None], + medium => [Self::min_medium(), None], + short => [Self::min_short(), None] + } + where T: Stimulus +} +impl_clamp! { + Lms { + long => [Self::min_long()], + medium => [Self::min_medium()], + short => [Self::min_short()] + } + other {meta} + where T: Stimulus +} + +impl_mix!(Lms); +impl_premultiply!(Lms {long, medium, short} phantom: meta); +impl_euclidean_distance!(Lms {long, medium, short}); + +impl_color_add!(Lms, [long, medium, short], meta); +impl_color_sub!(Lms, [long, medium, short], meta); +impl_color_mul!(Lms, [long, medium, short], meta); +impl_color_div!(Lms, [long, medium, short], meta); + +impl_tuple_conversion!(Lms as (T, T, T)); +impl_array_casts!(Lms, [T; 3]); +impl_simd_array_conversion!(Lms, [long, medium, short], meta); +impl_struct_of_array_traits!(Lms, [long, medium, short], meta); + +impl_eq!(Lms, [long, medium, short]); +impl_copy_clone!(Lms, [long, medium, short], meta); + +impl_rand_traits_cartesian!(UniformLms, Lms {long, medium, short} phantom: meta: PhantomData); + +#[cfg(test)] +mod test { + use crate::{lms::VonKriesLms, white_point::D65}; + + #[cfg(feature = "alloc")] + use super::Lmsa; + + #[cfg(feature = "random")] + use super::Lms; + + #[cfg(feature = "approx")] + use crate::{convert::FromColorUnclamped, lms::BradfordLms, Xyz}; + + test_convert_into_from_xyz!(VonKriesLms); + raw_pixel_conversion_tests!(VonKriesLms: long, medium, short); + raw_pixel_conversion_fail_tests!(VonKriesLms: long, medium, short); + + #[cfg(feature = "approx")] + #[test] + fn von_kries_xyz_roundtrip() { + let xyz = Xyz::new(0.2f32, 0.4, 0.8); + let lms = VonKriesLms::::from_color_unclamped(xyz); + assert_relative_eq!(Xyz::from_color_unclamped(lms), xyz); + } + + #[cfg(feature = "approx")] + #[test] + fn bradford_xyz_roundtrip() { + let xyz = Xyz::new(0.2f32, 0.4, 0.8); + let lms = BradfordLms::::from_color_unclamped(xyz); + assert_relative_eq!(Xyz::from_color_unclamped(lms), xyz); + } + + #[cfg(feature = "serializing")] + #[test] + fn serialize() { + let serialized = + ::serde_json::to_string(&VonKriesLms::::new(0.3, 0.8, 0.1)).unwrap(); + + assert_eq!(serialized, r#"{"long":0.3,"medium":0.8,"short":0.1}"#); + } + + #[cfg(feature = "serializing")] + #[test] + fn deserialize() { + let deserialized: VonKriesLms = + ::serde_json::from_str(r#"{"long":0.3,"medium":0.8,"short":0.1}"#).unwrap(); + + assert_eq!(deserialized, VonKriesLms::::new(0.3, 0.8, 0.1)); + } + + struct_of_arrays_tests!( + VonKriesLms[long, medium, short] phantom: meta, + Lmsa::new(0.1f32, 0.2, 0.3, 0.4), + Lmsa::new(0.2, 0.3, 0.4, 0.5), + Lmsa::new(0.3, 0.4, 0.5, 0.6) + ); + + test_uniform_distribution! { + VonKriesLms { + long: (0.0, 1.0), + medium: (0.0, 1.0), + short: (0.0, 1.0) + }, + min: Lms::new(0.0f32, 0.0, 0.0), + max: Lms::new(1.0, 1.0, 1.0) + } +} diff --git a/palette/src/lms/meta.rs b/palette/src/lms/meta.rs new file mode 100644 index 000000000..96aa89b44 --- /dev/null +++ b/palette/src/lms/meta.rs @@ -0,0 +1,202 @@ +//! Meta types and traits for [`Lms`][super::Lms]. + +use core::marker::PhantomData; + +use crate::{num::Real, white_point::Any, xyz::meta::HasXyzMeta, Mat3}; + +/// Implemented by meta types that contain an LMS matrix. +pub trait HasLmsMatrix { + /// The LMS matrix meta type. + type LmsMatrix; +} + +/// Provides a matrix for converting from [`Xyz`][crate::Xyz] to +/// [`Lms`][super::Lms]. +pub trait XyzToLms { + /// Get an [`Xyz`][crate::Xyz] to [`Lms`][super::Lms] conversion matrix with + /// elements of type `T`. + fn xyz_to_lms_matrix() -> Mat3; +} + +/// Provides a matrix for converting from [`Lms`][super::Lms] to +/// [`Xyz`][crate::Xyz]. +pub trait LmsToXyz { + /// Get an [`Lms`][super::Lms] to [`Xyz`][crate::Xyz] conversion matrix with + /// elements of type `T`. + fn lms_to_xyz_matrix() -> Mat3; +} + +/// Adds an LMS matrix `Matrix` to another meta type `T`. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct WithLmsMatrix(pub PhantomData<(M, Matrix)>); + +impl XyzToLms for WithLmsMatrix +where + Matrix: XyzToLms, +{ + #[inline] + fn xyz_to_lms_matrix() -> Mat3 { + Matrix::xyz_to_lms_matrix() + } +} + +impl LmsToXyz for WithLmsMatrix +where + Matrix: LmsToXyz, +{ + #[inline] + fn lms_to_xyz_matrix() -> Mat3 { + Matrix::lms_to_xyz_matrix() + } +} + +impl HasXyzMeta for WithLmsMatrix +where + M: HasXyzMeta, +{ + type XyzMeta = M::XyzMeta; +} + +impl HasLmsMatrix for WithLmsMatrix { + type LmsMatrix = Matrix; +} + +/// Represents the matrix used with the von Kries transform method +/// (MvonKries). +/// +/// It's also known as the Hunt-Pointer-Estevez matrix (MHPE) and was +/// originally used in conjunction with the von Kries method for chromatic +/// adaptation. It's also used in the Hunt and RLAB color appearance models. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct VonKries; + +impl XyzToLms for VonKries +where + T: Real, +{ + #[rustfmt::skip] + #[inline] + fn xyz_to_lms_matrix() -> Mat3 { + [ + T::from_f64( 0.4002400), T::from_f64(0.7076000), T::from_f64(-0.0808100), + T::from_f64(-0.2263000), T::from_f64(1.1653200), T::from_f64( 0.0457000), + T::from_f64( 0.0000000), T::from_f64(0.0000000), T::from_f64( 0.9182200), + ] + } +} + +impl LmsToXyz for VonKries +where + T: Real, +{ + #[rustfmt::skip] + #[inline] + fn lms_to_xyz_matrix() -> Mat3 { + [ + T::from_f64(1.8599364), T::from_f64(-1.1293816), T::from_f64( 0.2198974), + T::from_f64(0.3611914), T::from_f64( 0.6388125), T::from_f64(-0.0000064), + T::from_f64(0.0000000), T::from_f64( 0.0000000), T::from_f64( 1.0890636), + ] + } +} + +impl HasXyzMeta for VonKries { + type XyzMeta = Any; +} + +impl HasLmsMatrix for VonKries { + type LmsMatrix = Self; +} + +/// Represents Bradford's spectrally sharpening matrix (MBFD). +/// +/// The "spectral sharpening" effect of the Bradford matrix is believed to +/// improve chromatic adaptation, by narrowing the response curves and making L +/// and M more distinct. *It does however not really reflect cone cells*. +/// +/// The Bradford matrix is also used in CIECAM97 and LLAB. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Bradford; + +impl XyzToLms for Bradford +where + T: Real, +{ + #[rustfmt::skip] + #[inline] + fn xyz_to_lms_matrix() -> Mat3 { + [ + T::from_f64( 0.8951000), T::from_f64( 0.2664000), T::from_f64(-0.1614000), + T::from_f64(-0.7502000), T::from_f64( 1.7135000), T::from_f64( 0.0367000), + T::from_f64( 0.0389000), T::from_f64(-0.0685000), T::from_f64( 1.0296000), + ] + } +} + +impl LmsToXyz for Bradford +where + T: Real, +{ + #[rustfmt::skip] + #[inline] + fn lms_to_xyz_matrix() -> Mat3 { + [ + T::from_f64( 0.9869929), T::from_f64(-0.1470543), T::from_f64(0.1599627), + T::from_f64( 0.4323053), T::from_f64( 0.5183603), T::from_f64(0.0492912), + T::from_f64(-0.0085287), T::from_f64( 0.0400428), T::from_f64(0.9684867), + ] + } +} + +impl HasXyzMeta for Bradford { + type XyzMeta = Any; +} + +impl HasLmsMatrix for Bradford { + type LmsMatrix = Self; +} + +/// Represents a unit matrix, for a 1:1 conversion between XYZ to LMS. +/// +/// This matrix may be useful in chromatic adaptation, but does otherwise not +/// represent an actual conversion to and from cone cell responses. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct UnitMatrix; + +impl XyzToLms for UnitMatrix +where + T: Real, +{ + #[rustfmt::skip] + #[inline] + fn xyz_to_lms_matrix() -> Mat3 { + [ + T::from_f64(1.0000000), T::from_f64(0.0000000), T::from_f64(0.0000000), + T::from_f64(0.0000000), T::from_f64(1.0000000), T::from_f64(0.0000000), + T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(1.0000000), + ] + } +} + +impl LmsToXyz for UnitMatrix +where + T: Real, +{ + #[rustfmt::skip] + #[inline] + fn lms_to_xyz_matrix() -> Mat3 { + [ + T::from_f64(1.0000000), T::from_f64(0.0000000), T::from_f64(0.0000000), + T::from_f64(0.0000000), T::from_f64(1.0000000), T::from_f64(0.0000000), + T::from_f64(0.0000000), T::from_f64(0.0000000), T::from_f64(1.0000000), + ] + } +} + +impl HasXyzMeta for UnitMatrix { + type XyzMeta = Any; +} + +impl HasLmsMatrix for UnitMatrix { + type LmsMatrix = Self; +} diff --git a/palette/src/matrix.rs b/palette/src/matrix.rs index e26000425..bc808a34d 100644 --- a/palette/src/matrix.rs +++ b/palette/src/matrix.rs @@ -14,32 +14,40 @@ use crate::{ /// A 9 element array representing a 3x3 matrix. pub type Mat3 = [T; 9]; +pub type Vec3 = [T; 3]; /// Multiply the 3x3 matrix with an XYZ color. #[inline] -pub fn multiply_xyz(c: Mat3, f: Xyz) -> Xyz +pub fn multiply_xyz(matrix: Mat3, color: Xyz) -> Xyz where T: Arithmetics, { - // Input Mat3 is destructured to avoid panic paths - let [c0, c1, c2, c3, c4, c5, c6, c7, c8] = c; + multiply_3x3_and_vec3(matrix, color.into()).into() +} - let x1 = c0 * &f.x; - let y1 = c3 * &f.x; - let z1 = c6 * f.x; - let x2 = c1 * &f.y; - let y2 = c4 * &f.y; - let z2 = c7 * f.y; - let x3 = c2 * &f.z; - let y3 = c5 * &f.z; - let z3 = c8 * f.z; +/// Multiply the 3x3 matrix with an XYZ color. +#[inline] +pub fn multiply_3x3_and_vec3(matrix: Mat3, vector: Vec3) -> Vec3 +where + T: Arithmetics, +{ + // Input Mat3 and Vec3 are destructured to avoid panic paths. + let [m0, m1, m2, m3, m4, m5, m6, m7, m8] = matrix; + let [x, y, z] = vector; - Xyz { - x: x1 + x2 + x3, - y: y1 + y2 + y3, - z: z1 + z2 + z3, - white_point: PhantomData, - } + let x1 = m0 * &x; + let x2 = m1 * &y; + let x3 = m2 * &z; + + let y1 = m3 * &x; + let y2 = m4 * &y; + let y3 = m5 * &z; + + let z1 = m6 * x; + let z2 = m7 * y; + let z3 = m8 * z; + + [x1 + x2 + x3, y1 + y2 + y3, z1 + z2 + z3] } /// Multiply the 3x3 matrix with an XYZ color to return an RGB color. #[inline] diff --git a/palette/src/rgb/rgb.rs b/palette/src/rgb/rgb.rs index 6d6588556..b3c99c838 100644 --- a/palette/src/rgb/rgb.rs +++ b/palette/src/rgb/rgb.rs @@ -488,7 +488,7 @@ where /// [`Rgba`](crate::rgb::Rgba) implementations. impl Alpha, A> { - /// Non-linear RGB. + /// Create an RGBA color. pub const fn new(red: T, green: T, blue: T, alpha: A) -> Self { Alpha { color: Rgb::new(red, green, blue), diff --git a/palette/src/white_point.rs b/palette/src/white_point.rs index d74c6659c..c7bd3200e 100644 --- a/palette/src/white_point.rs +++ b/palette/src/white_point.rs @@ -6,7 +6,7 @@ //! daylight. Defining "white" as daylight will give unacceptable results when //! attempting to color-correct a photograph taken with incandescent lighting. -use crate::{num::Real, Xyz}; +use crate::{num::Real, xyz::meta::HasXyzMeta, Xyz}; /// Represents an unspecified reference white point. /// @@ -16,6 +16,10 @@ use crate::{num::Real, Xyz}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct Any; +impl HasXyzMeta for Any { + type XyzMeta = Self; +} + /// WhitePoint defines the Xyz color co-ordinates for a given white point. /// /// A white point (often referred to as reference white or target white in @@ -45,6 +49,10 @@ impl WhitePoint for A { Xyz::new(T::from_f64(1.09850), T::from_f64(1.0), T::from_f64(0.35585)) } } + +impl HasXyzMeta for A { + type XyzMeta = Self; +} /// CIE standard illuminant B /// /// CIE standard illuminant B represents noon sunlight, with a correlated color @@ -57,6 +65,11 @@ impl WhitePoint for B { Xyz::new(T::from_f64(0.99072), T::from_f64(1.0), T::from_f64(0.85223)) } } + +impl HasXyzMeta for B { + type XyzMeta = Self; +} + /// CIE standard illuminant C /// /// CIE standard illuminant C represents the average day light with a CCT of @@ -69,6 +82,11 @@ impl WhitePoint for C { Xyz::new(T::from_f64(0.98074), T::from_f64(1.0), T::from_f64(1.18232)) } } + +impl HasXyzMeta for C { + type XyzMeta = Self; +} + /// CIE D series standard illuminant - D50 /// /// D50 White Point is the natural daylight with a color temperature of around @@ -81,6 +99,11 @@ impl WhitePoint for D50 { Xyz::new(T::from_f64(0.96422), T::from_f64(1.0), T::from_f64(0.82521)) } } + +impl HasXyzMeta for D50 { + type XyzMeta = Self; +} + /// CIE D series standard illuminant - D55 /// /// D55 White Point is the natural daylight with a color temperature of around @@ -93,6 +116,11 @@ impl WhitePoint for D55 { Xyz::new(T::from_f64(0.95682), T::from_f64(1.0), T::from_f64(0.92149)) } } + +impl HasXyzMeta for D55 { + type XyzMeta = Self; +} + /// CIE D series standard illuminant - D65 /// /// D65 White Point is the natural daylight with a color temperature of 6500K @@ -105,6 +133,11 @@ impl WhitePoint for D65 { Xyz::new(T::from_f64(0.95047), T::from_f64(1.0), T::from_f64(1.08883)) } } + +impl HasXyzMeta for D65 { + type XyzMeta = Self; +} + /// CIE D series standard illuminant - D75 /// /// D75 White Point is the natural daylight with a color temperature of around @@ -117,6 +150,11 @@ impl WhitePoint for D75 { Xyz::new(T::from_f64(0.94972), T::from_f64(1.0), T::from_f64(1.22638)) } } + +impl HasXyzMeta for D75 { + type XyzMeta = Self; +} + /// CIE standard illuminant E /// /// CIE standard illuminant E represents the equal energy radiator @@ -129,6 +167,11 @@ impl WhitePoint for E { Xyz::new(T::from_f64(1.0), T::from_f64(1.0), T::from_f64(1.0)) } } + +impl HasXyzMeta for E { + type XyzMeta = Self; +} + /// CIE fluorescent illuminant series - F2 /// /// F2 represents a semi-broadband fluorescent lamp for 2° Standard Observer. @@ -140,6 +183,11 @@ impl WhitePoint for F2 { Xyz::new(T::from_f64(0.99186), T::from_f64(1.0), T::from_f64(0.67393)) } } + +impl HasXyzMeta for F2 { + type XyzMeta = Self; +} + /// CIE fluorescent illuminant series - F7 /// /// F7 represents a broadband fluorescent lamp for 2° Standard Observer. @@ -151,6 +199,11 @@ impl WhitePoint for F7 { Xyz::new(T::from_f64(0.95041), T::from_f64(1.0), T::from_f64(1.08747)) } } + +impl HasXyzMeta for F7 { + type XyzMeta = Self; +} + /// CIE fluorescent illuminant series - F11 /// /// F11 represents a narrowband fluorescent lamp for 2° Standard Observer. @@ -162,6 +215,11 @@ impl WhitePoint for F11 { Xyz::new(T::from_f64(1.00962), T::from_f64(1.0), T::from_f64(0.64350)) } } + +impl HasXyzMeta for F11 { + type XyzMeta = Self; +} + /// CIE D series standard illuminant - D50 /// /// D50 White Point is the natural daylight with a color temperature of around @@ -174,6 +232,11 @@ impl WhitePoint for D50Degree10 { Xyz::new(T::from_f64(0.9672), T::from_f64(1.0), T::from_f64(0.8143)) } } + +impl HasXyzMeta for D50Degree10 { + type XyzMeta = Self; +} + /// CIE D series standard illuminant - D55 /// /// D55 White Point is the natural daylight with a color temperature of around @@ -186,6 +249,11 @@ impl WhitePoint for D55Degree10 { Xyz::new(T::from_f64(0.958), T::from_f64(1.0), T::from_f64(0.9093)) } } + +impl HasXyzMeta for D55Degree10 { + type XyzMeta = Self; +} + /// CIE D series standard illuminant - D65 /// /// D65 White Point is the natural daylight with a color temperature of 6500K @@ -198,6 +266,11 @@ impl WhitePoint for D65Degree10 { Xyz::new(T::from_f64(0.9481), T::from_f64(1.0), T::from_f64(1.073)) } } + +impl HasXyzMeta for D65Degree10 { + type XyzMeta = Self; +} + /// CIE D series standard illuminant - D75 /// /// D75 White Point is the natural daylight with a color temperature of around @@ -210,3 +283,7 @@ impl WhitePoint for D75Degree10 { Xyz::new(T::from_f64(0.94416), T::from_f64(1.0), T::from_f64(1.2064)) } } + +impl HasXyzMeta for D75Degree10 { + type XyzMeta = Self; +} diff --git a/palette/src/xyz.rs b/palette/src/xyz.rs index 3139aadd0..616bf29c6 100644 --- a/palette/src/xyz.rs +++ b/palette/src/xyz.rs @@ -1,5 +1,7 @@ //! Types for the CIE 1931 XYZ color space. +pub mod meta; + use core::{marker::PhantomData, ops::Mul}; use crate::{ @@ -11,8 +13,14 @@ use crate::{ }, convert::{FromColorUnclamped, IntoColorUnclamped}, encoding::IntoLinear, + lms::{ + meta::{HasLmsMatrix, LmsToXyz}, + Lms, + }, luma::LumaStandard, - matrix::{matrix_map, multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix}, + matrix::{ + matrix_map, multiply_3x3_and_vec3, multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix, + }, num::{ Abs, Arithmetics, FromScalar, IsValidDivisor, One, PartialCmp, Powf, Powi, Real, Recip, Signum, Sqrt, Trigonometry, Zero, @@ -24,6 +32,8 @@ use crate::{ Alpha, Lab, Luma, Luv, Oklab, Yxy, }; +use self::meta::HasXyzMeta; + /// CIE 1931 XYZ with an alpha component. See the [`Xyza` implementation in /// `Alpha`](crate::Alpha#Xyza). pub type Xyza = Alpha, T>; @@ -43,7 +53,7 @@ pub type Xyza = Alpha, T>; palette_internal, white_point = "Wp", component = "T", - skip_derives(Xyz, Yxy, Luv, Rgb, Lab, Oklab, Luma) + skip_derives(Xyz, Yxy, Luv, Rgb, Lab, Oklab, Luma, Lms) )] #[repr(C)] pub struct Xyz { @@ -326,6 +336,17 @@ where } } +impl FromColorUnclamped> for Xyz +where + M: HasLmsMatrix + HasXyzMeta, + M::LmsMatrix: LmsToXyz, + T: Arithmetics, +{ + fn from_color_unclamped(val: Lms) -> Self { + multiply_3x3_and_vec3(M::LmsMatrix::lms_to_xyz_matrix(), val.into()).into() + } +} + impl FromCam16Unclamped> for Xyz where WpParam: WhitePointParameter, diff --git a/palette/src/xyz/meta.rs b/palette/src/xyz/meta.rs new file mode 100644 index 000000000..da83bec3e --- /dev/null +++ b/palette/src/xyz/meta.rs @@ -0,0 +1,7 @@ +//! Meta types and traits for [`Xyz`][super::Xyz]. + +/// Implemented by meta types that contain a meta type for [`Xyz`][super::Xyz]. +pub trait HasXyzMeta { + /// A meta type that can be used in [`Xyz`][super::Xyz]. + type XyzMeta; +} diff --git a/palette_derive/src/color_types.rs b/palette_derive/src/color_types.rs index a33d9f296..ab0da138a 100644 --- a/palette_derive/src/color_types.rs +++ b/palette_derive/src/color_types.rs @@ -286,6 +286,19 @@ pub(crate) static XYZ_COLORS: ColorGroup = ColorGroup { infer_group: true, preferred_source: "Luv", }, + ColorType { + info: ColorInfo { + name: "Lms", + module: Some("lms"), + default_white_point: InternalExternal { + internal: None, + external: Some(&["white_point", "D65"]), + }, + get_meta_type: Some(get_lms_meta), + }, + infer_group: true, + preferred_source: "Xyz", + }, ColorType { info: ColorInfo { name: "Luv", @@ -664,6 +677,41 @@ fn get_white_point( Ok(white_point.clone()) } +fn get_lms_meta( + self_color: &ColorInfo, + meta_type_source: MetaTypeSource, + white_point: &Type, + used_input: &mut UsedInput, + user: InputUser, + meta: &TypeItemAttributes, +) -> syn::Result { + match meta_type_source { + MetaTypeSource::Generics(generics) => { + used_input.white_point.set_used(user); + + let has_xyz_meta_path = util::path(["xyz", "meta", "HasXyzMeta"], meta.internal); + + generics.params.push(GenericParam::Type( + Ident::new("_LmsM", Span::call_site()).into(), + )); + + generics + .make_where_clause() + .predicates + .push(parse_quote!(_LmsM: #has_xyz_meta_path)); + + Ok(parse_quote!(_LmsM)) + } + MetaTypeSource::OtherColor(other_color) => Err(syn::parse::Error::new( + Span::call_site(), + format!( + "could not determine the LMS meta when converting to and from `{}` via `{}`", + other_color.name, self_color.name + ), + )), + } +} + pub(crate) enum MetaTypeSource<'a> { OtherColor(&'a ColorInfo), Generics(&'a mut Generics),