Skip to main content

ph_color/
matrix.rs

1//! Typed 3×3 matrix from linear `Src` to linear `Dst`.
2//!
3//! Coefficients are baked Q4.28. This crate applies them; it does not invert,
4//! adapt, or solve.
5
6use core::marker::PhantomData;
7
8use crate::color::Color;
9use crate::encoding::Linear;
10use crate::fixed::Q4_28;
11use crate::space::ColorSpace;
12
13/// [`Q4_28`] coefficients that convert `Color<Src, Linear>` to `Color<Dst, Linear>`.
14pub struct Matrix3<Src: ColorSpace, Dst: ColorSpace> {
15    coefs: [[Q4_28; 3]; 3],
16    _pd: PhantomData<fn() -> (Src, Dst)>,
17}
18
19impl<Src: ColorSpace, Dst: ColorSpace> Copy for Matrix3<Src, Dst> {}
20
21impl<Src: ColorSpace, Dst: ColorSpace> Clone for Matrix3<Src, Dst> {
22    fn clone(&self) -> Self {
23        *self
24    }
25}
26
27impl<Src: ColorSpace, Dst: ColorSpace> PartialEq for Matrix3<Src, Dst> {
28    fn eq(&self, other: &Self) -> bool {
29        self.coefs == other.coefs
30    }
31}
32
33impl<Src: ColorSpace, Dst: ColorSpace> Eq for Matrix3<Src, Dst> {}
34
35impl<Src: ColorSpace, Dst: ColorSpace> core::fmt::Debug for Matrix3<Src, Dst> {
36    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
37        f.debug_struct("Matrix3")
38            .field("coefs", &self.coefs)
39            .finish()
40    }
41}
42
43impl<Src: ColorSpace, Dst: ColorSpace> Matrix3<Src, Dst> {
44    /// Wrap baked [`Q4_28`] coefficients. Target code does not derive them.
45    #[must_use]
46    pub const fn from_q428(coefs: [[Q4_28; 3]; 3]) -> Self {
47        Self {
48            coefs,
49            _pd: PhantomData,
50        }
51    }
52
53    /// Apply to a linear source: one `i64` accumulate per row, then one
54    /// shift-round-saturate. There is no inverse or chromatic adaptation.
55    #[must_use]
56    pub const fn apply(&self, color: Color<Src, Linear>) -> Color<Dst, Linear> {
57        let [row0, row1, row2] = self.coefs;
58        Color::new([
59            crate::arith::mul_acc3(row0, color.ch),
60            crate::arith::mul_acc3(row1, color.ch),
61            crate::arith::mul_acc3(row2, color.ch),
62        ])
63    }
64
65    /// Apply with `f32` mul/add of Q4.28 coefficients decoded as
66    /// `coef as f32 / 2^28`. Saturates each channel to `0.0..=1.0`.
67    ///
68    /// Additive: does not change [`Self::apply`]. Error versus W7 goldens is
69    /// at most [`crate::F32_MAX_ERR_LSB`].
70    #[cfg(feature = "f32")]
71    #[must_use]
72    pub fn apply_f32(&self, color: crate::ColorF32<Src, Linear>) -> crate::ColorF32<Dst, Linear> {
73        let [row0, row1, row2] = self.coefs;
74        crate::ColorF32::new([
75            crate::color_f32::apply_row_f32(row0, color.ch),
76            crate::color_f32::apply_row_f32(row1, color.ch),
77            crate::color_f32::apply_row_f32(row2, color.ch),
78        ])
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use crate::encoding::Linear;
86    use crate::fixed::Q0_16;
87    use crate::space::Srgb;
88
89    #[test]
90    fn identity_leaves_linear_color_unchanged() {
91        let one = Q4_28::ONE;
92        let zero = Q4_28::ZERO;
93        let m = Matrix3::<Srgb, Srgb>::from_q428([
94            [one, zero, zero],
95            [zero, one, zero],
96            [zero, zero, one],
97        ]);
98        let c = Color::<Srgb, Linear>::new(Q0_16::array_from_raw([12, 34, 56]));
99        assert_eq!(m.apply(c), c);
100    }
101}