1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
/* This file is part of srgb crate.
 * Copyright 2021 by Michał Nazarewicz <mina86@mina86.com>
 *
 * srgb crate is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option) any
 * later version.
 *
 * srgb crate is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * srgb crate.  If not, see <http://www.gnu.org/licenses/>. */

#![doc = include_str!("../README.md")]
#![allow(clippy::excessive_precision)]

pub mod gamma;
pub mod xyz;

mod maths;


/// Converts a 24-bit sRGB colour (also known as true colour) into normalised
/// representation.
///
/// Returns three components each normalised to the range 0–1.  This does
/// exactly what one might expect: divides each component by 255.
///
/// # Example
/// ```
/// assert_eq!(
///     [0.9137255, 0.9098039, 0.90588236],
///     srgb::normalised_from_u8([233, 232, 231])
/// );
/// assert_eq!(
///     [0.83137256, 0.12941177, 0.23921569],
///     srgb::normalised_from_u8([212, 33, 61])
/// );
/// ```
#[doc(hidden)]
pub fn normalised_from_u8(
    encoded: impl std::convert::Into<[u8; 3]>,
) -> [f32; 3] {
    arr_map(encoded, |v| v as f32 / 255.0)
}

/// Converts an sRGB colour in normalised representation into a 24-bit (also
/// known as true colour).
///
/// That is, converts sRGB representation where each component is a number in
/// the range from zero to one to one where each component is an 8-bit unsigned
/// integer.  Components in source colour are clamped to the valid range.  This
/// is roughly equivalent to multiplying each component by 255.
///
/// # Example
/// ```
/// assert_eq!(
///     [233, 232, 231],
///     srgb::u8_from_normalised([0.9137255, 0.9098039, 0.90588236])
/// );
/// assert_eq!(
///     [212, 33, 61],
///     srgb::u8_from_normalised([0.83137256, 0.12941177, 0.23921569])
/// );
/// ```
#[doc(hidden)]
pub fn u8_from_normalised(
    normalised: impl std::convert::Into<[f32; 3]>,
) -> [u8; 3] {
    // Adding 0.5 is for rounding.
    arr_map(normalised, |v| v.clamp(0.0, 1.0).mul_add(255.0, 0.5) as u8)
}


/// Converts a colour in an XYZ colour space into 24-bit sRGB representation.
///
/// This is just a convenience function which wraps gamma (see [`gamma`] module)
/// and XYZ (see [`xyz`] module) conversions function together.
pub fn u8_from_xyz(xyz: impl std::convert::Into<[f32; 3]>) -> [u8; 3] {
    gamma::u8_from_linear(xyz::linear_from_xyz(xyz))
}

/// Converts a 24-bit sRGB colour into XYZ colour space.
///
/// This is just a convenience function which wraps gamma (see [`gamma`] module)
/// and XYZ (see [`xyz`] module) conversions function together.
pub fn xyz_from_u8(rgb: impl std::convert::Into<[u8; 3]>) -> [f32; 3] {
    xyz::xyz_from_linear(gamma::linear_from_u8(rgb))
}

/// Converts a colour in an XYZ colour space into a normalised sRGB
/// representation.
///
/// This is just a convenience function which wraps gamma (see [`gamma`] module)
/// and XYZ (see [`xyz`] module) conversions function together.
pub fn normalised_from_xyz(xyz: impl std::convert::Into<[f32; 3]>) -> [f32; 3] {
    gamma::normalised_from_linear(xyz::linear_from_xyz(xyz))
}

/// Converts a normalised representation of a sRGB colour into XYZ colour space.
///
/// This is just a convenience function which wraps gamma (see [`gamma`] module)
/// and XYZ (see [`xyz`] module) conversions function together.
pub fn xyz_from_normalised(rgb: impl std::convert::Into<[f32; 3]>) -> [f32; 3] {
    xyz::xyz_from_linear(gamma::linear_from_normalised(rgb))
}


pub(crate) fn arr_map<F: Copy, T: Copy, Fun: Fn(F) -> T>(
    arr: impl std::convert::Into<[F; 3]>,
    f: Fun,
) -> [T; 3] {
    let arr = arr.into();
    [f(arr[0]), f(arr[1]), f(arr[2])]
}


#[cfg(test)]
mod test {
    use kahan::KahanSummator;

    const WHITE_X: f64 = 0.312713;
    const WHITE_Y: f64 = 0.329016;

    fn measure_grey_chromaticity_error(f: impl Fn(u8) -> [f32; 3]) -> f64 {
        // Grey colours should have chromaticity equal white point’s
        // chromaticity.
        let mut error = kahan::KahanSum::new();
        for i in 1..=255 {
            let [x, y, z] = f(i);
            let d: f64 =
                [x as f64, y as f64, z as f64].iter().kahan_sum().sum();
            let x = x as f64 / d - WHITE_X;
            let y = y as f64 / d - WHITE_Y;
            error += x * x;
            error += y * y;
        }
        error.sum() * 1e15
    }

    #[test]
    fn test_grey_chromaticity_error_u8() {
        assert_eq!(
            48.99296021015466,
            measure_grey_chromaticity_error(|i| {
                super::xyz_from_u8([i, i, i])
            })
        );
    }

    #[test]
    fn test_grey_chromaticity_error_normalised() {
        assert_eq!(
            39.81365168327802,
            measure_grey_chromaticity_error(|i| {
                let v = i as f32 / 255.0;
                super::xyz_from_normalised([v, v, v])
            })
        );
    }

    #[test]
    fn test_grey_chromaticity_error_linear() {
        assert_eq!(
            50.927415198412874,
            measure_grey_chromaticity_error(|i| {
                let v = i as f32 / 255.0;
                crate::xyz::xyz_from_linear([v, v, v])
            })
        );
    }
}