palette/encoding/
srgb.rs

1//! The sRGB standard.
2
3use crate::{
4    bool_mask::LazySelect,
5    encoding::{FromLinear, IntoLinear},
6    luma::LumaStandard,
7    num::{Arithmetics, MulAdd, MulSub, PartialCmp, Powf, Real},
8    rgb::{Primaries, RgbSpace, RgbStandard},
9    white_point::{Any, D65},
10    Mat3, Yxy,
11};
12
13use lookup_tables::*;
14
15mod lookup_tables;
16
17/// The sRGB standard, color space, and transfer function.
18///
19/// # As transfer function
20///
21/// `Srgb` will not use any kind of approximation when converting from `T` to
22/// `T`. This involves calls to `powf`, which may make it too slow for certain
23/// applications.
24///
25/// There are some specialized cases where it has been optimized:
26///
27/// * When converting from `u8` to `f32` or `f64`, while converting to linear
28///   space. This uses lookup tables with precomputed values. `f32` will use the
29///   table provided by [fast_srgb8::srgb8_to_f32].
30/// * When converting from `f32` or `f64` to `u8`, while converting from linear
31///   space. This uses [fast_srgb8::f32_to_srgb8].
32#[derive(Copy, Clone, Debug, PartialEq, Eq)]
33pub struct Srgb;
34
35impl<T: Real> Primaries<T> for Srgb {
36    fn red() -> Yxy<Any, T> {
37        Yxy::new(
38            T::from_f64(0.6400),
39            T::from_f64(0.3300),
40            T::from_f64(0.212656),
41        )
42    }
43    fn green() -> Yxy<Any, T> {
44        Yxy::new(
45            T::from_f64(0.3000),
46            T::from_f64(0.6000),
47            T::from_f64(0.715158),
48        )
49    }
50    fn blue() -> Yxy<Any, T> {
51        Yxy::new(
52            T::from_f64(0.1500),
53            T::from_f64(0.0600),
54            T::from_f64(0.072186),
55        )
56    }
57}
58
59impl RgbSpace for Srgb {
60    type Primaries = Srgb;
61    type WhitePoint = D65;
62
63    #[rustfmt::skip]
64    #[inline(always)]
65    fn rgb_to_xyz_matrix() -> Option<Mat3<f64>> {
66        // Matrix from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
67        Some([
68            0.4124564, 0.3575761, 0.1804375,
69            0.2126729, 0.7151522, 0.0721750,
70            0.0193339, 0.1191920, 0.9503041,
71        ])
72    }
73
74    #[rustfmt::skip]
75    #[inline(always)]
76    fn xyz_to_rgb_matrix() -> Option<Mat3<f64>> {
77        // Matrix from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
78        Some([
79             3.2404542, -1.5371385, -0.4985314,
80            -0.9692660,  1.8760108,  0.0415560,
81             0.0556434, -0.2040259,  1.0572252,
82        ])
83    }
84}
85
86impl RgbStandard for Srgb {
87    type Space = Srgb;
88    type TransferFn = Srgb;
89}
90
91impl LumaStandard for Srgb {
92    type WhitePoint = D65;
93    type TransferFn = Srgb;
94}
95
96impl<T> IntoLinear<T, T> for Srgb
97where
98    T: Real + Powf + MulAdd + Arithmetics + PartialCmp + Clone,
99    T::Mask: LazySelect<T>,
100{
101    #[inline]
102    fn into_linear(x: T) -> T {
103        // Dividing the constants directly shows performance benefits in benchmarks for this function
104        lazy_select! {
105            if x.lt_eq(&T::from_f64(0.04045)) => T::from_f64(1.0 / 12.92) * &x,
106            else => x.clone().mul_add(T::from_f64(1.0 / 1.055), T::from_f64(0.055 / 1.055)).powf(T::from_f64(2.4)),
107        }
108    }
109}
110
111impl<T> FromLinear<T, T> for Srgb
112where
113    T: Real + Powf + MulSub + Arithmetics + PartialCmp + Clone,
114    T::Mask: LazySelect<T>,
115{
116    #[inline]
117    fn from_linear(x: T) -> T {
118        lazy_select! {
119            if x.lt_eq(&T::from_f64(0.0031308)) => T::from_f64(12.92) * &x,
120            else => x.clone().powf(T::from_f64(1.0 / 2.4)).mul_sub(T::from_f64(1.055), T::from_f64(0.055)),
121        }
122    }
123}
124
125impl IntoLinear<f32, u8> for Srgb {
126    #[inline]
127    fn into_linear(encoded: u8) -> f32 {
128        fast_srgb8::srgb8_to_f32(encoded)
129    }
130}
131
132impl FromLinear<f32, u8> for Srgb {
133    #[inline]
134    fn from_linear(linear: f32) -> u8 {
135        fast_srgb8::f32_to_srgb8(linear)
136    }
137}
138
139impl IntoLinear<f64, u8> for Srgb {
140    #[inline]
141    fn into_linear(encoded: u8) -> f64 {
142        SRGB_U8_TO_F64[encoded as usize]
143    }
144}
145
146impl FromLinear<f64, u8> for Srgb {
147    #[inline]
148    fn from_linear(linear: f64) -> u8 {
149        Srgb::from_linear(linear as f32)
150    }
151}
152
153#[cfg(test)]
154mod test {
155    use crate::encoding::{FromLinear, IntoLinear, Srgb};
156
157    #[cfg(feature = "approx")]
158    mod conversion {
159        use crate::{
160            encoding::Srgb,
161            matrix::{matrix_inverse, rgb_to_xyz_matrix},
162            rgb::RgbSpace,
163        };
164
165        #[test]
166        fn rgb_to_xyz() {
167            let dynamic = rgb_to_xyz_matrix::<Srgb, f64>();
168            let constant = Srgb::rgb_to_xyz_matrix().unwrap();
169            assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001);
170        }
171
172        #[test]
173        fn xyz_to_rgb() {
174            let dynamic = matrix_inverse(rgb_to_xyz_matrix::<Srgb, f64>());
175            let constant = Srgb::xyz_to_rgb_matrix().unwrap();
176            assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001);
177        }
178    }
179
180    #[test]
181    fn u8_to_f32_to_u8() {
182        for expected in 0u8..=255u8 {
183            let linear: f32 = Srgb::into_linear(expected);
184            let result: u8 = Srgb::from_linear(linear);
185            assert_eq!(result, expected);
186        }
187    }
188
189    #[test]
190    fn u8_to_f64_to_u8() {
191        for expected in 0u8..=255u8 {
192            let linear: f64 = Srgb::into_linear(expected);
193            let result: u8 = Srgb::from_linear(linear);
194            assert_eq!(result, expected);
195        }
196    }
197}