|
| 1 | +//! The P3 color space(s) and standards. |
| 2 | +
|
| 3 | +use core::marker::PhantomData; |
| 4 | + |
| 5 | +use crate::{ |
| 6 | + encoding::{FromLinear, IntoLinear, Srgb}, |
| 7 | + luma::LumaStandard, |
| 8 | + num::{Powf, Real}, |
| 9 | + rgb::{Primaries, RgbSpace, RgbStandard}, |
| 10 | + white_point::{Any, WhitePoint, D65}, |
| 11 | + Mat3, Xyz, Yxy, |
| 12 | +}; |
| 13 | + |
| 14 | +/// The theatrical DCI-P3 standard. |
| 15 | +/// |
| 16 | +/// This standard uses a gamma 2.6 transfer function and a white point of ~6300K that |
| 17 | +/// matches the color of xenon bulbs used in theater projectors |
| 18 | +#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
| 19 | +pub struct DciP3; |
| 20 | + |
| 21 | +impl<T: Real> Primaries<T> for DciP3 { |
| 22 | + fn red() -> Yxy<Any, T> { |
| 23 | + Yxy::new( |
| 24 | + T::from_f64(0.680), |
| 25 | + T::from_f64(0.320), |
| 26 | + T::from_f64(0.209492), |
| 27 | + ) |
| 28 | + } |
| 29 | + fn green() -> Yxy<Any, T> { |
| 30 | + Yxy::new( |
| 31 | + T::from_f64(0.265), |
| 32 | + T::from_f64(0.690), |
| 33 | + T::from_f64(0.721595), |
| 34 | + ) |
| 35 | + } |
| 36 | + fn blue() -> Yxy<Any, T> { |
| 37 | + Yxy::new( |
| 38 | + T::from_f64(0.150), |
| 39 | + T::from_f64(0.060), |
| 40 | + T::from_f64(0.068913), |
| 41 | + ) |
| 42 | + } |
| 43 | +} |
| 44 | + |
| 45 | +impl<T: Real> WhitePoint<T> for DciP3 { |
| 46 | + fn get_xyz() -> Xyz<Any, T> { |
| 47 | + Xyz::new( |
| 48 | + T::from_f64(0.314 / 0.351), |
| 49 | + T::from_f64(1.0), |
| 50 | + T::from_f64(0.335 / 0.351), |
| 51 | + ) |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +impl RgbSpace for DciP3 { |
| 56 | + type Primaries = DciP3; |
| 57 | + type WhitePoint = DciP3; |
| 58 | + |
| 59 | + #[rustfmt::skip] |
| 60 | + #[inline(always)] |
| 61 | + fn rgb_to_xyz_matrix() -> Option<Mat3<f64>> { |
| 62 | + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm |
| 63 | + Some([ |
| 64 | + 0.4451698, 0.2771344, 0.1722827, |
| 65 | + 0.2094917, 0.7215953, 0.0689131, |
| 66 | + 0.0000000, 0.0470606, 0.9073554, |
| 67 | + ]) |
| 68 | + } |
| 69 | + |
| 70 | + #[rustfmt::skip] |
| 71 | + #[inline(always)] |
| 72 | + fn xyz_to_rgb_matrix() -> Option<Mat3<f64>> { |
| 73 | + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm |
| 74 | + Some([ |
| 75 | + 2.7253940, -1.0180030, -0.4401632, |
| 76 | + -0.7951680, 1.6897321, 0.0226472, |
| 77 | + 0.0412419, -0.0876390, 1.1009294, |
| 78 | + ]) |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +impl RgbStandard for DciP3 { |
| 83 | + type Space = DciP3; |
| 84 | + type TransferFn = P3Gamma; |
| 85 | +} |
| 86 | + |
| 87 | +impl LumaStandard for DciP3 { |
| 88 | + type WhitePoint = DciP3; |
| 89 | + type TransferFn = P3Gamma; |
| 90 | +} |
| 91 | + |
| 92 | +/// The Canon DCI-P3+ color space and standard. |
| 93 | +/// |
| 94 | +/// This standard has the same white point as [`DciP3`], but has a much wider gamut and |
| 95 | +/// no standardized transfer function (left to user preference). The generic `F` in |
| 96 | +/// this struct represents the chosen transfer function. |
| 97 | +#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
| 98 | +pub struct DciP3Plus<F>(PhantomData<F>); |
| 99 | + |
| 100 | +impl<T: Real, F> Primaries<T> for DciP3Plus<F> { |
| 101 | + fn red() -> Yxy<Any, T> { |
| 102 | + Yxy::new(T::from_f64(0.740), T::from_f64(0.270), T::from_f64(0.203986)) |
| 103 | + } |
| 104 | + fn green() -> Yxy<Any, T> { |
| 105 | + Yxy::new(T::from_f64(0.220), T::from_f64(0.780), T::from_f64(0.882591)) |
| 106 | + } |
| 107 | + fn blue() -> Yxy<Any, T> { |
| 108 | + Yxy::new( |
| 109 | + T::from_f64(0.090), |
| 110 | + T::from_f64(-0.090), |
| 111 | + T::from_f64(-0.086577), |
| 112 | + ) |
| 113 | + } |
| 114 | +} |
| 115 | + |
| 116 | +impl<F> RgbSpace for DciP3Plus<F> { |
| 117 | + type Primaries = DciP3Plus<F>; |
| 118 | + type WhitePoint = DciP3; |
| 119 | + |
| 120 | + #[rustfmt::skip] |
| 121 | + #[inline(always)] |
| 122 | + fn rgb_to_xyz_matrix() -> Option<Mat3<f64>> { |
| 123 | + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm |
| 124 | + Some([ |
| 125 | + 0.5590736, 0.2489359, 0.0865774, |
| 126 | + 0.2039863, 0.8825911, -0.0865774, |
| 127 | + -0.0075550, 0.0000000, 0.9619710, |
| 128 | + ]) |
| 129 | + } |
| 130 | + |
| 131 | + #[rustfmt::skip] |
| 132 | + #[inline(always)] |
| 133 | + fn xyz_to_rgb_matrix() -> Option<Mat3<f64>> { |
| 134 | + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm |
| 135 | + Some([ |
| 136 | + 1.9904035, -0.5613959, -0.2296619, |
| 137 | + -0.4584928, 1.2623460, 0.1548755, |
| 138 | + 0.0156321, -0.0044090, 1.0377287, |
| 139 | + ]) |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +impl<F> RgbStandard for DciP3Plus<F> { |
| 144 | + type Space = DciP3Plus<F>; |
| 145 | + type TransferFn = F; |
| 146 | +} |
| 147 | + |
| 148 | +impl<F> LumaStandard for DciP3Plus<F> { |
| 149 | + type WhitePoint = DciP3; |
| 150 | + type TransferFn = F; |
| 151 | +} |
| 152 | + |
| 153 | +/// A gamma 2.6 transfer function used by some P3 variants |
| 154 | +#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
| 155 | +pub struct P3Gamma; |
| 156 | + |
| 157 | +impl<T> IntoLinear<T, T> for P3Gamma |
| 158 | +where |
| 159 | + T: Real + Powf, |
| 160 | +{ |
| 161 | + #[inline] |
| 162 | + fn into_linear(encoded: T) -> T { |
| 163 | + encoded.powf(T::from_f64(2.6)) |
| 164 | + } |
| 165 | +} |
| 166 | + |
| 167 | +impl<T> FromLinear<T, T> for P3Gamma |
| 168 | +where |
| 169 | + T: Real + Powf, |
| 170 | +{ |
| 171 | + #[inline] |
| 172 | + fn from_linear(linear: T) -> T { |
| 173 | + linear.powf(T::from_f64(1.0 / 2.6)) |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +/// The Display P3 standard. |
| 178 | +/// |
| 179 | +/// This standard uses the same primaries as [`DciP3`] but with a [`D65`] white point |
| 180 | +/// and the [`Srgb`] transfer function. |
| 181 | +#[derive(Copy, Clone, Debug, PartialEq, Eq)] |
| 182 | +pub struct DisplayP3; |
| 183 | + |
| 184 | +impl<T: Real> Primaries<T> for DisplayP3 { |
| 185 | + fn red() -> Yxy<Any, T> { |
| 186 | + Yxy::new(T::from_f64(0.680), T::from_f64(0.320), T::from_f64(0.22900)) |
| 187 | + } |
| 188 | + fn green() -> Yxy<Any, T> { |
| 189 | + Yxy::new(T::from_f64(0.265), T::from_f64(0.690), T::from_f64(0.69173)) |
| 190 | + } |
| 191 | + fn blue() -> Yxy<Any, T> { |
| 192 | + Yxy::new(T::from_f64(0.150), T::from_f64(0.060), T::from_f64(0.07927)) |
| 193 | + } |
| 194 | +} |
| 195 | +impl RgbSpace for DisplayP3 { |
| 196 | + type Primaries = DisplayP3; |
| 197 | + type WhitePoint = D65; |
| 198 | + |
| 199 | + #[rustfmt::skip] |
| 200 | + #[inline(always)] |
| 201 | + fn rgb_to_xyz_matrix() -> Option<Mat3<f64>> { |
| 202 | + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm |
| 203 | + Some([ |
| 204 | + 0.4866327, 0.2656632, 0.1981742, |
| 205 | + 0.2290036, 0.6917267, 0.0792697, |
| 206 | + 0.0000000, 0.0451126, 1.0437174, |
| 207 | + ]) |
| 208 | + } |
| 209 | + |
| 210 | + #[rustfmt::skip] |
| 211 | + #[inline(always)] |
| 212 | + fn xyz_to_rgb_matrix() -> Option<Mat3<f64>> { |
| 213 | + // Matrix calculated using https://www.russellcottrell.com/photo/matrixCalculator.htm |
| 214 | + Some([ |
| 215 | + 2.4931808, -0.9312655, -0.4026597, |
| 216 | + -0.8295031, 1.7626941, 0.0236251, |
| 217 | + 0.0358536, -0.0761890, 0.9570926, |
| 218 | + ]) |
| 219 | + } |
| 220 | +} |
| 221 | + |
| 222 | +impl RgbStandard for DisplayP3 { |
| 223 | + type Space = DisplayP3; |
| 224 | + type TransferFn = Srgb; |
| 225 | +} |
| 226 | + |
| 227 | +impl LumaStandard for DisplayP3 { |
| 228 | + type WhitePoint = D65; |
| 229 | + type TransferFn = Srgb; |
| 230 | +} |
| 231 | + |
| 232 | +#[cfg(test)] |
| 233 | +mod test { |
| 234 | + #[cfg(feature = "approx")] |
| 235 | + mod conversion { |
| 236 | + use crate::{ |
| 237 | + convert::IntoColorUnclamped, |
| 238 | + encoding::p3::{DciP3, DciP3Plus, DisplayP3, P3Gamma}, |
| 239 | + matrix::{matrix_inverse, rgb_to_xyz_matrix}, |
| 240 | + rgb::{Primaries, RgbSpace}, |
| 241 | + white_point::{Any, WhitePoint, D65}, |
| 242 | + Xyz, |
| 243 | + }; |
| 244 | + |
| 245 | + #[test] |
| 246 | + fn rgb_to_xyz_display_p3() { |
| 247 | + let dynamic = rgb_to_xyz_matrix::<DisplayP3, f64>(); |
| 248 | + let constant = DisplayP3::rgb_to_xyz_matrix().unwrap(); |
| 249 | + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); |
| 250 | + } |
| 251 | + |
| 252 | + #[test] |
| 253 | + fn xyz_to_rgb_display_p3() { |
| 254 | + let dynamic = matrix_inverse(rgb_to_xyz_matrix::<DisplayP3, f64>()); |
| 255 | + let constant = DisplayP3::xyz_to_rgb_matrix().unwrap(); |
| 256 | + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); |
| 257 | + } |
| 258 | + |
| 259 | + #[test] |
| 260 | + fn rgb_to_xyz_dci_p3() { |
| 261 | + let dynamic = rgb_to_xyz_matrix::<DciP3, f64>(); |
| 262 | + let constant = DciP3::rgb_to_xyz_matrix().unwrap(); |
| 263 | + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); |
| 264 | + } |
| 265 | + |
| 266 | + #[test] |
| 267 | + fn xyz_to_rgb_dci_p3() { |
| 268 | + let dynamic = matrix_inverse(rgb_to_xyz_matrix::<DciP3, f64>()); |
| 269 | + let constant = DciP3::xyz_to_rgb_matrix().unwrap(); |
| 270 | + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); |
| 271 | + } |
| 272 | + |
| 273 | + #[test] |
| 274 | + fn rgb_to_xyz_dci_p3_plus() { |
| 275 | + let dynamic = rgb_to_xyz_matrix::<DciP3Plus<P3Gamma>, f64>(); |
| 276 | + let constant = DciP3Plus::<P3Gamma>::rgb_to_xyz_matrix().unwrap(); |
| 277 | + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); |
| 278 | + } |
| 279 | + |
| 280 | + #[test] |
| 281 | + fn xyz_to_rgb_dci_p3_plus() { |
| 282 | + let dynamic = matrix_inverse(rgb_to_xyz_matrix::<DciP3Plus<P3Gamma>, f64>()); |
| 283 | + let constant = DciP3Plus::<P3Gamma>::xyz_to_rgb_matrix().unwrap(); |
| 284 | + assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001); |
| 285 | + } |
| 286 | + |
| 287 | + #[test] |
| 288 | + fn primaries_display_p3() { |
| 289 | + let red: Xyz<Any, f64> = DisplayP3::red().into_color_unclamped(); |
| 290 | + let green: Xyz<Any, f64> = DisplayP3::green().into_color_unclamped(); |
| 291 | + let blue: Xyz<Any, f64> = DisplayP3::blue().into_color_unclamped(); |
| 292 | + // Compare sum of primaries to white point. |
| 293 | + assert_relative_eq!(red + green + blue, D65::get_xyz(), epsilon = 0.00001) |
| 294 | + } |
| 295 | + |
| 296 | + #[test] |
| 297 | + fn primaries_dci_p3() { |
| 298 | + let red: Xyz<Any, f64> = DciP3::red().into_color_unclamped(); |
| 299 | + let green: Xyz<Any, f64> = DciP3::green().into_color_unclamped(); |
| 300 | + let blue: Xyz<Any, f64> = DciP3::blue().into_color_unclamped(); |
| 301 | + // Compare sum of primaries to white point. |
| 302 | + assert_relative_eq!(red + green + blue, DciP3::get_xyz(), epsilon = 0.00001) |
| 303 | + } |
| 304 | + |
| 305 | + #[test] |
| 306 | + fn primaries_dci_p3_plus() { |
| 307 | + let red: Xyz<Any, f64> = DciP3Plus::<P3Gamma>::red().into_color_unclamped(); |
| 308 | + let green: Xyz<Any, f64> = DciP3Plus::<P3Gamma>::green().into_color_unclamped(); |
| 309 | + let blue: Xyz<Any, f64> = DciP3Plus::<P3Gamma>::blue().into_color_unclamped(); |
| 310 | + // Compare sum of primaries to white point. |
| 311 | + assert_relative_eq!(red + green + blue, DciP3::get_xyz(), epsilon = 0.00001) |
| 312 | + } |
| 313 | + } |
| 314 | +} |
0 commit comments