1#![allow(clippy::excessive_precision)]
2use crate::ColorPrimaries;
11
12pub type GamutMatrix = [[f32; 3]; 3];
14
15pub(crate) const BT709_TO_XYZ: GamutMatrix = [
21 [0.4123907993, 0.3575843394, 0.1804807884],
22 [0.2126390059, 0.7151686788, 0.0721923154],
23 [0.0193308187, 0.1191947798, 0.9505321522],
24];
25
26pub(crate) const XYZ_TO_BT709: GamutMatrix = [
28 [3.2409699419, -1.5373831776, -0.4986107603],
29 [-0.9692436363, 1.8759675015, 0.0415550574],
30 [0.0556300797, -0.2039769589, 1.0569715142],
31];
32
33pub(crate) const DISPLAY_P3_TO_XYZ: GamutMatrix = [
35 [0.4865709486, 0.2656676932, 0.1982172852],
36 [0.2289745641, 0.6917385218, 0.0792869141],
37 [0.0000000000, 0.0451133819, 1.0439443689],
38];
39
40pub(crate) const XYZ_TO_DISPLAY_P3: GamutMatrix = [
42 [2.4934969119, -0.9313836179, -0.4027107845],
43 [-0.8294889696, 1.7626640603, 0.0236246858],
44 [0.0358458302, -0.0761723893, 0.9568845240],
45];
46
47pub(crate) const BT2020_TO_XYZ: GamutMatrix = [
49 [0.6369580484, 0.1446169036, 0.1688809752],
50 [0.2627002120, 0.6779980715, 0.0593017165],
51 [0.0000000000, 0.0280726930, 1.0609850578],
52];
53
54pub(crate) const XYZ_TO_BT2020: GamutMatrix = [
56 [1.7166511880, -0.3556707838, -0.2533662814],
57 [-0.6666843518, 1.6164812366, 0.0157685458],
58 [0.0176398574, -0.0427706133, 0.9421031212],
59];
60
61pub fn mat3_mul(a: &GamutMatrix, b: &GamutMatrix) -> GamutMatrix {
63 let mut c = [[0.0f32; 3]; 3];
64 for i in 0..3 {
65 for j in 0..3 {
66 c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
67 }
68 }
69 c
70}
71
72#[inline]
74pub fn apply_matrix_f32(rgb: &mut [f32; 3], m: &GamutMatrix) {
75 let [r, g, b] = *rgb;
76 rgb[0] = m[0][0] * r + m[0][1] * g + m[0][2] * b;
77 rgb[1] = m[1][0] * r + m[1][1] * g + m[1][2] * b;
78 rgb[2] = m[2][0] * r + m[2][1] * g + m[2][2] * b;
79}
80
81pub fn apply_matrix_row_f32(data: &mut [f32], width: usize, m: &GamutMatrix) {
86 for i in 0..width {
87 let base = i * 3;
88 let r = data[base];
89 let g = data[base + 1];
90 let b = data[base + 2];
91 data[base] = m[0][0] * r + m[0][1] * g + m[0][2] * b;
92 data[base + 1] = m[1][0] * r + m[1][1] * g + m[1][2] * b;
93 data[base + 2] = m[2][0] * r + m[2][1] * g + m[2][2] * b;
94 }
95}
96
97pub fn apply_matrix_row_rgba_f32(data: &mut [f32], width: usize, m: &GamutMatrix) {
101 for i in 0..width {
102 let base = i * 4;
103 let r = data[base];
104 let g = data[base + 1];
105 let b = data[base + 2];
106 data[base] = m[0][0] * r + m[0][1] * g + m[0][2] * b;
107 data[base + 1] = m[1][0] * r + m[1][1] * g + m[1][2] * b;
108 data[base + 2] = m[2][0] * r + m[2][1] * g + m[2][2] * b;
109 }
111}
112
113pub fn conversion_matrix(from: ColorPrimaries, to: ColorPrimaries) -> Option<GamutMatrix> {
119 if from as u8 == to as u8 {
120 return None;
121 }
122 from.gamut_matrix_to(to)
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 use crate::ColorPrimaries;
130
131 #[test]
133 fn bt709_bt2020_roundtrip() {
134 let fwd = ColorPrimaries::Bt709
135 .gamut_matrix_to(ColorPrimaries::Bt2020)
136 .unwrap();
137 let inv = ColorPrimaries::Bt2020
138 .gamut_matrix_to(ColorPrimaries::Bt709)
139 .unwrap();
140 let test_rgb = [0.5f32, 0.3, 0.8];
141 let mut rgb = test_rgb;
142 apply_matrix_f32(&mut rgb, &fwd);
143 apply_matrix_f32(&mut rgb, &inv);
144 for c in 0..3 {
145 assert!(
146 (rgb[c] - test_rgb[c]).abs() < 1e-4,
147 "BT.709→BT.2020→BT.709 roundtrip error in ch{c}: {:.6} vs {:.6}",
148 rgb[c],
149 test_rgb[c]
150 );
151 }
152 }
153
154 #[test]
156 fn bt709_displayp3_roundtrip() {
157 let fwd = ColorPrimaries::Bt709
158 .gamut_matrix_to(ColorPrimaries::DisplayP3)
159 .unwrap();
160 let inv = ColorPrimaries::DisplayP3
161 .gamut_matrix_to(ColorPrimaries::Bt709)
162 .unwrap();
163 let test_rgb = [0.5f32, 0.3, 0.8];
164 let mut rgb = test_rgb;
165 apply_matrix_f32(&mut rgb, &fwd);
166 apply_matrix_f32(&mut rgb, &inv);
167 for c in 0..3 {
168 assert!(
169 (rgb[c] - test_rgb[c]).abs() < 1e-4,
170 "BT.709→P3→BT.709 roundtrip error in ch{c}: {:.6} vs {:.6}",
171 rgb[c],
172 test_rgb[c]
173 );
174 }
175 }
176
177 #[test]
179 fn white_point_preservation() {
180 let m = ColorPrimaries::Bt709
181 .gamut_matrix_to(ColorPrimaries::Bt2020)
182 .unwrap();
183 let mut rgb = [1.0f32, 1.0, 1.0];
184 apply_matrix_f32(&mut rgb, &m);
185 for (c, &val) in rgb.iter().enumerate() {
186 assert!(
187 (val - 1.0).abs() < 1e-4,
188 "White point not preserved in ch{c}: {val:.6}",
189 );
190 }
191 }
192
193 #[test]
195 fn rgba_alpha_preserved() {
196 let m = ColorPrimaries::Bt709
197 .gamut_matrix_to(ColorPrimaries::Bt2020)
198 .unwrap();
199 let mut row = [0.5f32, 0.3, 0.8, 0.42, 0.1, 0.9, 0.2, 0.99];
200 apply_matrix_row_rgba_f32(&mut row, 2, &m);
201 assert_eq!(row[3], 0.42);
202 assert_eq!(row[7], 0.99);
203 }
204
205 #[test]
207 fn xyz_bt709_roundtrip() {
208 let rgb = [0.5f32, 0.3, 0.8];
209 let mut v = rgb;
210 apply_matrix_f32(&mut v, &BT709_TO_XYZ);
211 apply_matrix_f32(&mut v, &XYZ_TO_BT709);
212 for c in 0..3 {
213 assert!(
214 (v[c] - rgb[c]).abs() < 1e-4,
215 "XYZ BT.709 roundtrip ch{c}: {:.6} vs {:.6}",
216 v[c],
217 rgb[c]
218 );
219 }
220 }
221
222 #[test]
224 fn xyz_displayp3_roundtrip() {
225 let rgb = [0.5f32, 0.3, 0.8];
226 let mut v = rgb;
227 apply_matrix_f32(&mut v, &DISPLAY_P3_TO_XYZ);
228 apply_matrix_f32(&mut v, &XYZ_TO_DISPLAY_P3);
229 for c in 0..3 {
230 assert!(
231 (v[c] - rgb[c]).abs() < 1e-4,
232 "XYZ P3 roundtrip ch{c}: {:.6} vs {:.6}",
233 v[c],
234 rgb[c]
235 );
236 }
237 }
238
239 #[test]
241 fn xyz_bt2020_roundtrip() {
242 let rgb = [0.5f32, 0.3, 0.8];
243 let mut v = rgb;
244 apply_matrix_f32(&mut v, &BT2020_TO_XYZ);
245 apply_matrix_f32(&mut v, &XYZ_TO_BT2020);
246 for c in 0..3 {
247 assert!(
248 (v[c] - rgb[c]).abs() < 1e-4,
249 "XYZ BT.2020 roundtrip ch{c}: {:.6} vs {:.6}",
250 v[c],
251 rgb[c]
252 );
253 }
254 }
255
256 #[test]
258 fn xyz_white_point() {
259 for (name, to, from) in [
260 ("BT.709", &BT709_TO_XYZ, &XYZ_TO_BT709),
261 ("P3", &DISPLAY_P3_TO_XYZ, &XYZ_TO_DISPLAY_P3),
262 ("BT.2020", &BT2020_TO_XYZ, &XYZ_TO_BT2020),
263 ] {
264 let mut rgb = [1.0f32; 3];
265 apply_matrix_f32(&mut rgb, to);
266 apply_matrix_f32(&mut rgb, from);
267 for (c, &val) in rgb.iter().enumerate() {
268 assert!(
269 (val - 1.0).abs() < 1e-3,
270 "{name} XYZ white point ch{c}: {val:.6}",
271 );
272 }
273 }
274 }
275
276 #[test]
278 fn mat3_mul_inverse() {
279 let identity = mat3_mul(&BT709_TO_XYZ, &XYZ_TO_BT709);
280 for (i, row) in identity.iter().enumerate() {
281 for (j, &val) in row.iter().enumerate() {
282 let expected = if i == j { 1.0 } else { 0.0 };
283 assert!(
284 (val - expected).abs() < 1e-4,
285 "mat3_mul identity [{i}][{j}] = {val:.6}, expected {expected:.1}",
286 );
287 }
288 }
289 }
290
291 #[test]
293 fn xyz_cross_gamut_consistency() {
294 let via_xyz = mat3_mul(&XYZ_TO_BT2020, &BT709_TO_XYZ);
295 let direct = ColorPrimaries::Bt709
296 .gamut_matrix_to(ColorPrimaries::Bt2020)
297 .unwrap();
298 let rgb = [0.5f32, 0.3, 0.8];
299 let mut v1 = rgb;
300 apply_matrix_f32(&mut v1, &via_xyz);
301 let mut v2 = rgb;
302 apply_matrix_f32(&mut v2, &direct);
303 for c in 0..3 {
304 assert!(
305 (v1[c] - v2[c]).abs() < 1e-3,
306 "cross-gamut ch{c}: via_xyz={:.6} vs direct={:.6}",
307 v1[c],
308 v2[c]
309 );
310 }
311 }
312}