From c8eb560fbc276b325d59b49625204f8833ef4f26 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sun, 11 Aug 2024 19:49:39 -0700 Subject: [PATCH 1/4] Add DCI-P3, DCI-P3+, and Display P3 --- palette/src/encoding.rs | 2 + palette/src/encoding/p3.rs | 277 +++++++++++++++++++++++++++++++++++++ palette/src/lib.rs | 5 +- palette/src/rgb.rs | 53 +++++++ 4 files changed, 335 insertions(+), 2 deletions(-) create mode 100644 palette/src/encoding/p3.rs diff --git a/palette/src/encoding.rs b/palette/src/encoding.rs index 8ea6bb0dd..d7aee29d4 100644 --- a/palette/src/encoding.rs +++ b/palette/src/encoding.rs @@ -8,12 +8,14 @@ pub use self::adobe::AdobeRgb; pub use self::gamma::{F2p2, Gamma}; pub use self::linear::Linear; +pub use self::p3::{DciP3, DciP3Plus, DisplayP3}; pub use self::rec_standards::{Rec2020, Rec709}; pub use self::srgb::Srgb; pub mod adobe; pub mod gamma; pub mod linear; +pub mod p3; pub mod rec_standards; pub mod srgb; diff --git a/palette/src/encoding/p3.rs b/palette/src/encoding/p3.rs new file mode 100644 index 000000000..aebe3b734 --- /dev/null +++ b/palette/src/encoding/p3.rs @@ -0,0 +1,277 @@ +//! The P3 color space(s) and standards. + +use core::marker::PhantomData; + +use crate::{ + encoding::{FromLinear, IntoLinear, Srgb}, + luma::LumaStandard, + num::{Powf, Real}, + rgb::{Primaries, RgbSpace, RgbStandard}, + white_point::{Any, WhitePoint, D65}, + Mat3, Xyz, Yxy, +}; + +/// The white point of DCI-P3 (Theatrical) is based on a projector with a xenon bulb +/// with a color temperature of ~6300K +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct XenonBulb; + +impl WhitePoint for XenonBulb { + fn get_xyz() -> Xyz { + Xyz::new( + T::from_f64(0.314 / 0.351), + T::from_f64(1.0), + T::from_f64(0.335 / 0.351), + ) + } +} + +/// The theatrical DCI-P3 standard. +/// +/// This standard uses a gamma 2.6 transfer function and a white point of ~6300K that +/// matches the color of xenon bulbs used in theater projectors +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct DciP3; + +impl Primaries for DciP3 { + fn red() -> Yxy { + Yxy::new(T::from_f64(0.680), T::from_f64(0.320), T::from_f64(0.2095)) + } + fn green() -> Yxy { + Yxy::new(T::from_f64(0.265), T::from_f64(0.690), T::from_f64(0.7216)) + } + fn blue() -> Yxy { + Yxy::new(T::from_f64(0.150), T::from_f64(0.060), T::from_f64(0.0689)) + } +} + +impl RgbSpace for DciP3 { + type Primaries = DciP3; + type WhitePoint = XenonBulb; + + #[rustfmt::skip] + #[inline(always)] + fn rgb_to_xyz_matrix() -> Option> { + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm + Some([ + 0.4451698, 0.2771344, 0.1722827, + 0.2094917, 0.7215953, 0.0689131, + 0.0000000, 0.0470606, 0.9073554, + ]) + } + + #[rustfmt::skip] + #[inline(always)] + fn xyz_to_rgb_matrix() -> Option> { + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm + Some([ + 2.7253940, -1.0180030, -0.4401632, + -0.7951680, 1.6897321, 0.0226472, + 0.0412419, -0.0876390, 1.1009294, + ]) + } +} + +impl RgbStandard for DciP3 { + type Space = DciP3; + type TransferFn = P3Gamma; +} + +impl LumaStandard for DciP3 { + type WhitePoint = XenonBulb; + type TransferFn = P3Gamma; +} + +/// The Canon DCI-P3+ color space and standard. +/// +/// This standard has the same white point as [`DciP3`], but has a much wider gamut and +/// no standardized transfer function (left to user preference). The generic `F` in +/// this struct represents the chosen transfer function. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct DciP3Plus(PhantomData); + +impl Primaries for DciP3Plus { + fn red() -> Yxy { + Yxy::new(T::from_f64(0.740), T::from_f64(0.270), T::from_f64(0.2040)) + } + fn green() -> Yxy { + Yxy::new(T::from_f64(0.220), T::from_f64(0.780), T::from_f64(0.8826)) + } + fn blue() -> Yxy { + Yxy::new( + T::from_f64(0.090), + T::from_f64(-0.090), + T::from_f64(-0.0866), + ) + } +} + +impl RgbSpace for DciP3Plus { + type Primaries = DciP3Plus; + type WhitePoint = XenonBulb; + + #[rustfmt::skip] + #[inline(always)] + fn rgb_to_xyz_matrix() -> Option> { + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm + Some([ + 0.5590736, 0.2489359, 0.0865774, + 0.2039863, 0.8825911, -0.0865774, + -0.0075550, 0.0000000, 0.9619710, + ]) + } + + #[rustfmt::skip] + #[inline(always)] + fn xyz_to_rgb_matrix() -> Option> { + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm + Some([ + 1.9904035, -0.5613959, -0.2296619, + -0.4584928, 1.2623460, 0.1548755, + 0.0156321, -0.0044090, 1.0377287, + ]) + } +} + +impl RgbStandard for DciP3Plus { + type Space = DciP3Plus; + type TransferFn = F; +} + +impl LumaStandard for DciP3Plus { + type WhitePoint = XenonBulb; + type TransferFn = F; +} + +/// A gamma 2.6 transfer function used by some P3 variants +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct P3Gamma; + +impl IntoLinear for P3Gamma +where + T: Real + Powf, +{ + #[inline] + fn into_linear(encoded: T) -> T { + encoded.powf(T::from_f64(2.6)) + } +} + +impl FromLinear for P3Gamma +where + T: Real + Powf, +{ + #[inline] + fn from_linear(linear: T) -> T { + linear.powf(T::from_f64(1.0 / 2.6)) + } +} + +/// The Display P3 standard. +/// +/// This standard uses the same primaries as [`DciP3`] but with a [`D65`] white point +/// and the [`Srgb`] transfer function. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct DisplayP3; + +impl Primaries for DisplayP3 { + fn red() -> Yxy { + Yxy::new(T::from_f64(0.680), T::from_f64(0.320), T::from_f64(0.2290)) + } + fn green() -> Yxy { + Yxy::new(T::from_f64(0.265), T::from_f64(0.690), T::from_f64(0.6917)) + } + fn blue() -> Yxy { + Yxy::new(T::from_f64(0.150), T::from_f64(0.060), T::from_f64(0.0793)) + } +} +impl RgbSpace for DisplayP3 { + type Primaries = DisplayP3; + type WhitePoint = D65; + + #[rustfmt::skip] + #[inline(always)] + fn rgb_to_xyz_matrix() -> Option> { + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm + Some([ + 0.4866327, 0.2656632, 0.1981742, + 0.2290036, 0.6917267, 0.0792697, + 0.0000000, 0.0451126, 1.0437174, + ]) + } + + #[rustfmt::skip] + #[inline(always)] + fn xyz_to_rgb_matrix() -> Option> { + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm + Some([ + 2.4931808, -0.9312655, -0.4026597, + -0.8295031, 1.7626941, 0.0236251, + 0.0358536, -0.0761890, 0.9570926, + ]) + } +} + +impl RgbStandard for DisplayP3 { + type Space = DisplayP3; + type TransferFn = Srgb; +} + +impl LumaStandard for DisplayP3 { + type WhitePoint = D65; + type TransferFn = Srgb; +} + +#[cfg(test)] +mod test { + #[cfg(feature = "approx")] + mod conversion { + use crate::{ + encoding::p3::{DciP3, DciP3Plus, DisplayP3, P3Gamma}, + matrix::{matrix_inverse, rgb_to_xyz_matrix}, + rgb::RgbSpace, + }; + + #[test] + fn rgb_to_xyz_display_p3() { + let dynamic = rgb_to_xyz_matrix::(); + let constant = DisplayP3::rgb_to_xyz_matrix().unwrap(); + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); + } + + #[test] + fn xyz_to_rgb_display_p3() { + let dynamic = matrix_inverse(rgb_to_xyz_matrix::()); + let constant = DisplayP3::xyz_to_rgb_matrix().unwrap(); + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); + } + + #[test] + fn rgb_to_xyz_dci_p3() { + let dynamic = rgb_to_xyz_matrix::(); + let constant = DciP3::rgb_to_xyz_matrix().unwrap(); + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); + } + + #[test] + fn xyz_to_rgb_dci_p3() { + let dynamic = matrix_inverse(rgb_to_xyz_matrix::()); + let constant = DciP3::xyz_to_rgb_matrix().unwrap(); + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); + } + + #[test] + fn rgb_to_xyz_dci_p3_plus() { + let dynamic = rgb_to_xyz_matrix::, f64>(); + let constant = DciP3Plus::::rgb_to_xyz_matrix().unwrap(); + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); + } + + #[test] + fn xyz_to_rgb_dci_p3_plus() { + let dynamic = matrix_inverse(rgb_to_xyz_matrix::, f64>()); + let constant = DciP3Plus::::xyz_to_rgb_matrix().unwrap(); + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); + } + } +} diff --git a/palette/src/lib.rs b/palette/src/lib.rs index 048be73cd..e1816d9b2 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -313,8 +313,9 @@ pub use oklab::{Oklab, Oklaba}; pub use oklch::{Oklch, Oklcha}; #[doc(inline)] pub use rgb::{ - AdobeRgb, AdobeRgba, GammaSrgb, GammaSrgba, LinAdobeRgb, LinAdobeRgba, LinRec2020, LinSrgb, - LinSrgba, Rec2020, Rec709, Srgb, Srgba, + AdobeRgb, AdobeRgba, DciP3, DciP3Plus, DisplayP3, GammaSrgb, GammaSrgba, LinAdobeRgb, + LinAdobeRgba, LinDciP3, LinDciP3Plus, LinDisplayP3, LinRec2020, LinSrgb, LinSrgba, Rec2020, + Rec709, Srgb, Srgba, }; #[doc(inline)] pub use xyz::{Xyz, Xyza}; diff --git a/palette/src/rgb.rs b/palette/src/rgb.rs index 80814c690..09cd08ecc 100644 --- a/palette/src/rgb.rs +++ b/palette/src/rgb.rs @@ -150,6 +150,59 @@ pub type AdobeRgb = Rgb; /// create a value and use it. pub type AdobeRgba = Rgba; +/// Non-linear DCI-P3, an RGB format used for digital movie distribution. +/// +/// This is an RGB standard with a color gamut wider than that of [`Srgb`] and a +/// white point similar to that of a film projector's xenon bulb. +/// +/// See [`Rgb`] for more details on how to create a value and use it. +pub type DciP3 = Rgb; + +/// Non-linear Canon DCI-P3+, an RGB format with a very wide gamut. +/// +/// This is an RGB standard with a color gamut much wider than that of [`Srgb`]. +/// It uses the same white point as [`DciP3`], but uses a user-defined transfer +/// function, represented here by the generic `F`. +/// +/// See [`Rgb`] for more details on how to create a value and use it. +pub type DciP3Plus = Rgb, T>; + +/// Non-linear Display P3, an RGB format used developed by Apple for wide-gamut +/// displays. +/// +/// This is an RGB standard with the same white point and transfer function as +/// [`Srgb`], but with a wider color gamut. +/// +/// See [`Rgb`] for more details on how to create a value and use it. +pub type DisplayP3 = Rgb; + +/// Linear DCI-P3. +/// +/// You probably want [`DciP3`] if you are looking for an input or output format. +/// This is the linear version of DCI-P3, which is what you would usually convert +/// to before working with the color. +/// +/// See [`Rgb`] for more details on how to create a value and use it. +pub type LinDciP3 = Rgb, T>; + +/// Linear DCI-P3+. +/// +/// You probably want [`DciP3Plus`] if you are looking for an input or output format. +/// This is the linear version of DCI-P3+, which is what you would usually convert +/// to before working with the color. +/// +/// See [`Rgb`] for more details on how to create a value and use it. +pub type LinDciP3Plus = Rgb>, T>; + +/// Linear Display P3. +/// +/// You probably want [`DisplayP3`] if you are looking for an input or output format. +/// This is the linear version of Display P3, which is what you would usually convert +/// to before working with the color. +/// +/// See [`Rgb`] for more details on how to create a value and use it. +pub type LinDisplayP3 = Rgb, T>; + /// Linear Adobe RGB. /// /// You probably want [`AdobeRgb`] if you are looking for an input or output format. From 38bbd764ecab240b01a1f8763b22abb8041845e3 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 17 Aug 2024 09:39:16 -0700 Subject: [PATCH 2/4] Consolidate `XenonBulb` white point into `DciP3` --- palette/src/encoding/p3.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/palette/src/encoding/p3.rs b/palette/src/encoding/p3.rs index aebe3b734..9fa5d492d 100644 --- a/palette/src/encoding/p3.rs +++ b/palette/src/encoding/p3.rs @@ -11,21 +11,6 @@ use crate::{ Mat3, Xyz, Yxy, }; -/// The white point of DCI-P3 (Theatrical) is based on a projector with a xenon bulb -/// with a color temperature of ~6300K -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct XenonBulb; - -impl WhitePoint for XenonBulb { - fn get_xyz() -> Xyz { - Xyz::new( - T::from_f64(0.314 / 0.351), - T::from_f64(1.0), - T::from_f64(0.335 / 0.351), - ) - } -} - /// The theatrical DCI-P3 standard. /// /// This standard uses a gamma 2.6 transfer function and a white point of ~6300K that @@ -45,9 +30,19 @@ impl Primaries for DciP3 { } } +impl WhitePoint for DciP3 { + fn get_xyz() -> Xyz { + Xyz::new( + T::from_f64(0.314 / 0.351), + T::from_f64(1.0), + T::from_f64(0.335 / 0.351), + ) + } +} + impl RgbSpace for DciP3 { type Primaries = DciP3; - type WhitePoint = XenonBulb; + type WhitePoint = DciP3; #[rustfmt::skip] #[inline(always)] @@ -78,7 +73,7 @@ impl RgbStandard for DciP3 { } impl LumaStandard for DciP3 { - type WhitePoint = XenonBulb; + type WhitePoint = DciP3; type TransferFn = P3Gamma; } @@ -108,7 +103,7 @@ impl Primaries for DciP3Plus { impl RgbSpace for DciP3Plus { type Primaries = DciP3Plus; - type WhitePoint = XenonBulb; + type WhitePoint = DciP3; #[rustfmt::skip] #[inline(always)] @@ -139,7 +134,7 @@ impl RgbStandard for DciP3Plus { } impl LumaStandard for DciP3Plus { - type WhitePoint = XenonBulb; + type WhitePoint = DciP3; type TransferFn = F; } From 264df3e53ad1299c82d2d4bb78ecedc383e51ef0 Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 17 Aug 2024 09:59:06 -0700 Subject: [PATCH 3/4] Add tests for primary values (and adjust accordingly) --- palette/src/encoding/p3.rs | 62 ++++++++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/palette/src/encoding/p3.rs b/palette/src/encoding/p3.rs index 9fa5d492d..0bfc4feb9 100644 --- a/palette/src/encoding/p3.rs +++ b/palette/src/encoding/p3.rs @@ -20,13 +20,25 @@ pub struct DciP3; impl Primaries for DciP3 { fn red() -> Yxy { - Yxy::new(T::from_f64(0.680), T::from_f64(0.320), T::from_f64(0.2095)) + Yxy::new( + T::from_f64(0.680), + T::from_f64(0.320), + T::from_f64(0.209492), + ) } fn green() -> Yxy { - Yxy::new(T::from_f64(0.265), T::from_f64(0.690), T::from_f64(0.7216)) + Yxy::new( + T::from_f64(0.265), + T::from_f64(0.690), + T::from_f64(0.721595), + ) } fn blue() -> Yxy { - Yxy::new(T::from_f64(0.150), T::from_f64(0.060), T::from_f64(0.0689)) + Yxy::new( + T::from_f64(0.150), + T::from_f64(0.060), + T::from_f64(0.068913), + ) } } @@ -87,16 +99,16 @@ pub struct DciP3Plus(PhantomData); impl Primaries for DciP3Plus { fn red() -> Yxy { - Yxy::new(T::from_f64(0.740), T::from_f64(0.270), T::from_f64(0.2040)) + Yxy::new(T::from_f64(0.740), T::from_f64(0.270), T::from_f64(0.203986)) } fn green() -> Yxy { - Yxy::new(T::from_f64(0.220), T::from_f64(0.780), T::from_f64(0.8826)) + Yxy::new(T::from_f64(0.220), T::from_f64(0.780), T::from_f64(0.882591)) } fn blue() -> Yxy { Yxy::new( T::from_f64(0.090), T::from_f64(-0.090), - T::from_f64(-0.0866), + T::from_f64(-0.086577), ) } } @@ -171,13 +183,13 @@ pub struct DisplayP3; impl Primaries for DisplayP3 { fn red() -> Yxy { - Yxy::new(T::from_f64(0.680), T::from_f64(0.320), T::from_f64(0.2290)) + Yxy::new(T::from_f64(0.680), T::from_f64(0.320), T::from_f64(0.22900)) } fn green() -> Yxy { - Yxy::new(T::from_f64(0.265), T::from_f64(0.690), T::from_f64(0.6917)) + Yxy::new(T::from_f64(0.265), T::from_f64(0.690), T::from_f64(0.69173)) } fn blue() -> Yxy { - Yxy::new(T::from_f64(0.150), T::from_f64(0.060), T::from_f64(0.0793)) + Yxy::new(T::from_f64(0.150), T::from_f64(0.060), T::from_f64(0.07927)) } } impl RgbSpace for DisplayP3 { @@ -222,9 +234,12 @@ mod test { #[cfg(feature = "approx")] mod conversion { use crate::{ + convert::IntoColorUnclamped, encoding::p3::{DciP3, DciP3Plus, DisplayP3, P3Gamma}, matrix::{matrix_inverse, rgb_to_xyz_matrix}, - rgb::RgbSpace, + rgb::{Primaries, RgbSpace}, + white_point::{Any, WhitePoint, D65}, + Xyz, }; #[test] @@ -268,5 +283,32 @@ mod test { let constant = DciP3Plus::::xyz_to_rgb_matrix().unwrap(); assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); } + + #[test] + fn primaries_display_p3() { + let red: Xyz = DisplayP3::red().into_color_unclamped(); + let green: Xyz = DisplayP3::green().into_color_unclamped(); + let blue: Xyz = DisplayP3::blue().into_color_unclamped(); + // Compare sum of primaries to white point. + assert_relative_eq!(red + green + blue, D65::get_xyz(), epsilon = 0.00001) + } + + #[test] + fn primaries_dci_p3() { + let red: Xyz = DciP3::red().into_color_unclamped(); + let green: Xyz = DciP3::green().into_color_unclamped(); + let blue: Xyz = DciP3::blue().into_color_unclamped(); + // Compare sum of primaries to white point. + assert_relative_eq!(red + green + blue, DciP3::get_xyz(), epsilon = 0.00001) + } + + #[test] + fn primaries_dci_p3_plus() { + let red: Xyz = DciP3Plus::::red().into_color_unclamped(); + let green: Xyz = DciP3Plus::::green().into_color_unclamped(); + let blue: Xyz = DciP3Plus::::blue().into_color_unclamped(); + // Compare sum of primaries to white point. + assert_relative_eq!(red + green + blue, DciP3::get_xyz(), epsilon = 0.00001) + } } } From 068eccc90dd56053bd2c59cf4423e08cdebf70ea Mon Sep 17 00:00:00 2001 From: BuildTools Date: Sat, 17 Aug 2024 10:01:08 -0700 Subject: [PATCH 4/4] Revert inclusion of P3 types in lib.rs --- palette/src/lib.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/palette/src/lib.rs b/palette/src/lib.rs index e1816d9b2..048be73cd 100644 --- a/palette/src/lib.rs +++ b/palette/src/lib.rs @@ -313,9 +313,8 @@ pub use oklab::{Oklab, Oklaba}; pub use oklch::{Oklch, Oklcha}; #[doc(inline)] pub use rgb::{ - AdobeRgb, AdobeRgba, DciP3, DciP3Plus, DisplayP3, GammaSrgb, GammaSrgba, LinAdobeRgb, - LinAdobeRgba, LinDciP3, LinDciP3Plus, LinDisplayP3, LinRec2020, LinSrgb, LinSrgba, Rec2020, - Rec709, Srgb, Srgba, + AdobeRgb, AdobeRgba, GammaSrgb, GammaSrgba, LinAdobeRgb, LinAdobeRgba, LinRec2020, LinSrgb, + LinSrgba, Rec2020, Rec709, Srgb, Srgba, }; #[doc(inline)] pub use xyz::{Xyz, Xyza};