colr 0.1.1

A general purpose, extensible color type unifying storage, channel layouts, and color spaces at the type level.
Documentation
//! RGB primary sets and the PrimariesToXyz routing trait.
//!
//! Primaries defines the chromaticity coordinates and native RGB-to-XYZ
//! matrices for one primary set. PrimariesToXyz extends this with a
//! hub-adapted matrix for a specific illuminant, inserting chromatic
//! adaptation at compile time when the native illuminant differs.
//!
//! All matrix values are derived via const fn at compile time.
//!
//! Adding a new primary set:
//!
//! 1. Define the chromaticity constants as [f32; 2] xy pairs.
//! 2. impl Primaries for MyPrimaries.
//! 3. impl_native!(MyPrimaries, W) for the hub sharing the native illuminant.
//! 4. impl_adapted!(MyPrimaries, NativeIlluminant, TargetIlluminant) for any cross-illuminant hubs
//!    required.

use crate::adaptation::{Bradford, ChromaticAdaptation, adapt};
use crate::illuminant::{AcesWhitePoint, D50, D65, DciWhite, Illuminant};
use crate::math::Mat3;

/// Derive the linear RGB to CIE XYZ matrix from chromaticity coordinates
/// and an exact XYZ white point. const fn.
///
/// Takes WHITE_POINT_XYZ directly rather than xy chromaticities to avoid
/// the floating-point drift that occurs when deriving XYZ from rounded xy
/// coordinates. The Y row of the result gives luminance weights directly.
pub const fn derive_rgb_to_xyz(r: [f32; 2], g: [f32; 2], b: [f32; 2], w_xyz: [f32; 3]) -> Mat3 {
    let [xr, yr] = r;
    let zr = 1.0 - xr - yr;
    let [xg, yg] = g;
    let zg = 1.0 - xg - yg;
    let [xb, yb] = b;
    let zb = 1.0 - xb - yb;

    let det = xr * (yg * zb - yb * zg) - xg * (yr * zb - yb * zr) + xb * (yr * zg - yg * zr);

    let inv00 = (yg * zb - yb * zg) / det;
    let inv01 = (xb * zg - xg * zb) / det;
    let inv02 = (xg * yb - xb * yg) / det;
    let inv10 = (yb * zr - yr * zb) / det;
    let inv11 = (xr * zb - xb * zr) / det;
    let inv12 = (xb * yr - xr * yb) / det;
    let inv20 = (yr * zg - yg * zr) / det;
    let inv21 = (xg * zr - xr * zg) / det;
    let inv22 = (xr * yg - xg * yr) / det;

    let [xw, yw, zw] = w_xyz;

    let sr = inv00 * xw + inv01 * yw + inv02 * zw;
    let sg = inv10 * xw + inv11 * yw + inv12 * zw;
    let sb = inv20 * xw + inv21 * yw + inv22 * zw;

    Mat3 {
        col0: [sr * xr, sr * yr, sr * zr, 0.0],
        col1: [sg * xg, sg * yg, sg * zg, 0.0],
        col2: [sb * xb, sb * yb, sb * zb, 0.0],
    }
}

/// The irreducible specification of an RGB primary set.
pub trait Primaries: 'static {
    /// The illuminant these chromaticities are specified relative to.
    type Native: Illuminant;

    /// Red primary CIE 1931 xy chromaticity.
    const R: [f32; 2];
    /// Green primary CIE 1931 xy chromaticity.
    const G: [f32; 2];
    /// Blue primary CIE 1931 xy chromaticity.
    const B: [f32; 2];

    /// Linear RGB to CIE XYZ under Self::Native.
    const TO_XYZ_NATIVE: Mat3;
    /// CIE XYZ under Self::Native to linear RGB.
    const FROM_XYZ_NATIVE: Mat3;
}

/// Routing from a primary set to a CIE XYZ connection space under illuminant W.
///
/// TO_XYZ and FROM_XYZ incorporate chromatic adaptation when the native
/// illuminant differs from W. Luminance weights are derived from TO_XYZ
/// rather than the native matrix because adaptation shifts the Y coefficients.
///
/// The adaptation method A defaults to Bradford, which is mandated by ICC,
/// ACES, and CSS Color Level 4 for all standard RGB spaces. Specify an
/// alternative only for appearance model or research contexts.
pub trait PrimariesToXyz<W: Illuminant, A: ChromaticAdaptation = Bradford>: Primaries {
    /// Hub-adapted forward matrix: linear RGB to CIE XYZ under W.
    const TO_XYZ: Mat3;
    /// Hub-adapted inverse matrix: CIE XYZ under W to linear RGB.
    const FROM_XYZ: Mat3;
    /// Y row of TO_XYZ as CIE luminance weights [w_r, w_g, w_b].
    const LUMINANCE_WEIGHTS: [f32; 3];
}

/// Implements PrimariesToXyz<W> when the native illuminant matches W.
/// No chromatic adaptation applied.
macro_rules! impl_native {
    ($P:ty, $W:ty) => {
        impl PrimariesToXyz<$W> for $P {
            const TO_XYZ: Mat3 = <$P as Primaries>::TO_XYZ_NATIVE;
            const FROM_XYZ: Mat3 = <$P as Primaries>::FROM_XYZ_NATIVE;
            const LUMINANCE_WEIGHTS: [f32; 3] = <$P as Primaries>::TO_XYZ_NATIVE.luminance_weights();
        }
    };
}

/// Implements PrimariesToXyz<$to, $A> with chromatic adaptation from $from to $to.
///
/// Uses WHITE_POINT_XYZ from each illuminant for numerical precision.
/// Forward: RGB to XYZ_from, then adapt(from->to), yielding XYZ_to.
/// Inverse: XYZ_to, then adapt(to->from), then XYZ_from to RGB.
macro_rules! impl_adapted {
    ($P:ty, $from:ty, $to:ty) => {
        impl_adapted!($P, $from, $to, Bradford);
    };
    ($P:ty, $from:ty, $to:ty, $A:ty) => {
        impl PrimariesToXyz<$to, $A> for $P {
            const TO_XYZ: Mat3 = {
                const ADAPT: Mat3 = adapt::<$A>(
                    <$from as Illuminant>::WHITE_POINT_XYZ,
                    <$to as Illuminant>::WHITE_POINT_XYZ,
                );
                Mat3::mul(&ADAPT, &<$P as Primaries>::TO_XYZ_NATIVE)
            };
            const FROM_XYZ: Mat3 = {
                const ADAPT: Mat3 = adapt::<$A>(
                    <$to as Illuminant>::WHITE_POINT_XYZ,
                    <$from as Illuminant>::WHITE_POINT_XYZ,
                );
                Mat3::mul(&<$P as Primaries>::FROM_XYZ_NATIVE, &ADAPT)
            };
            const LUMINANCE_WEIGHTS: [f32; 3] = <$P as PrimariesToXyz<$to, $A>>::TO_XYZ.luminance_weights();
        }
    };
}

/// sRGB and Rec. 709 primaries (D65).
///
/// Both standards share identical chromaticities and differ only in their
/// transfer functions.
///
/// Reference: IEC 61966-2-1:1999, ITU-R BT.709-6.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SrgbPrimaries;

const SRGB_TO_XYZ: Mat3 = derive_rgb_to_xyz(
    [0.6400, 0.3300],
    [0.3000, 0.6000],
    [0.1500, 0.0600],
    D65::WHITE_POINT_XYZ,
);
const SRGB_FROM_XYZ: Mat3 = Mat3::invert(&SRGB_TO_XYZ);

impl Primaries for SrgbPrimaries {
    type Native = D65;
    const R: [f32; 2] = [0.6400, 0.3300];
    const G: [f32; 2] = [0.3000, 0.6000];
    const B: [f32; 2] = [0.1500, 0.0600];
    const TO_XYZ_NATIVE: Mat3 = SRGB_TO_XYZ;
    const FROM_XYZ_NATIVE: Mat3 = SRGB_FROM_XYZ;
}

/// Display P3 primaries (D65). Consumer D65 variant; DCI-P3 theatre uses D60.
///
/// Reference: SMPTE EG 432-1:2010.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct P3Primaries;

const P3_TO_XYZ: Mat3 = derive_rgb_to_xyz(
    [0.6800, 0.3200],
    [0.2650, 0.6900],
    [0.1500, 0.0600],
    D65::WHITE_POINT_XYZ,
);
const P3_FROM_XYZ: Mat3 = Mat3::invert(&P3_TO_XYZ);

impl Primaries for P3Primaries {
    type Native = D65;
    const R: [f32; 2] = [0.6800, 0.3200];
    const G: [f32; 2] = [0.2650, 0.6900];
    const B: [f32; 2] = [0.1500, 0.0600];
    const TO_XYZ_NATIVE: Mat3 = P3_TO_XYZ;
    const FROM_XYZ_NATIVE: Mat3 = P3_FROM_XYZ;
}

/// Rec. 2020 primaries (D65). Ultra-wide gamut; primaries lie on the spectral
/// locus and are not physically realisable by current displays.
///
/// Reference: ITU-R BT.2020-2.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Rec2020Primaries;

const REC2020_TO_XYZ: Mat3 = derive_rgb_to_xyz(
    [0.7080, 0.2920],
    [0.1700, 0.7970],
    [0.1310, 0.0460],
    D65::WHITE_POINT_XYZ,
);
const REC2020_FROM_XYZ: Mat3 = Mat3::invert(&REC2020_TO_XYZ);

impl Primaries for Rec2020Primaries {
    type Native = D65;
    const R: [f32; 2] = [0.7080, 0.2920];
    const G: [f32; 2] = [0.1700, 0.7970];
    const B: [f32; 2] = [0.1310, 0.0460];
    const TO_XYZ_NATIVE: Mat3 = REC2020_TO_XYZ;
    const FROM_XYZ_NATIVE: Mat3 = REC2020_FROM_XYZ;
}

/// ACES AP1 primaries (ACES white point). VFX rendering working space (ACEScg).
/// All real-world colors have positive values.
///
/// Reference: Academy S-2014-004.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AcesAp1Primaries;

const AP1_TO_XYZ: Mat3 = derive_rgb_to_xyz(
    [0.71300, 0.29300],
    [0.16500, 0.83000],
    [0.12800, 0.04400],
    AcesWhitePoint::WHITE_POINT_XYZ,
);
const AP1_FROM_XYZ: Mat3 = Mat3::invert(&AP1_TO_XYZ);

impl Primaries for AcesAp1Primaries {
    type Native = AcesWhitePoint;
    const R: [f32; 2] = [0.71300, 0.29300];
    const G: [f32; 2] = [0.16500, 0.83000];
    const B: [f32; 2] = [0.12800, 0.04400];
    const TO_XYZ_NATIVE: Mat3 = AP1_TO_XYZ;
    const FROM_XYZ_NATIVE: Mat3 = AP1_FROM_XYZ;
}

/// ACES AP0 primaries (ACES white point). Full-gamut archival space (ACES 2065-1).
/// The blue primary y-coordinate is negative (an imaginary color), correct per spec.
///
/// Reference: SMPTE ST 2065-1:2021.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AcesAp0Primaries;

const AP0_TO_XYZ: Mat3 = derive_rgb_to_xyz(
    [0.73470, 0.26530],
    [0.00000, 1.00000],
    [0.00010, -0.07700],
    AcesWhitePoint::WHITE_POINT_XYZ,
);
const AP0_FROM_XYZ: Mat3 = Mat3::invert(&AP0_TO_XYZ);

impl Primaries for AcesAp0Primaries {
    type Native = AcesWhitePoint;
    const R: [f32; 2] = [0.73470, 0.26530];
    const G: [f32; 2] = [0.00000, 1.00000];
    const B: [f32; 2] = [0.00010, -0.07700];
    const TO_XYZ_NATIVE: Mat3 = AP0_TO_XYZ;
    const FROM_XYZ_NATIVE: Mat3 = AP0_FROM_XYZ;
}

/// ProPhoto (ROMM RGB) primaries (D50). Roughly 13% of the gamut is imaginary.
///
/// Reference: ISO 22028-2:2013.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ProPhotoPrimaries;

const PRO_PHOTO_TO_XYZ: Mat3 = derive_rgb_to_xyz(
    [0.7347, 0.2653],
    [0.1596, 0.8404],
    [0.0366, 0.0001],
    D50::WHITE_POINT_XYZ,
);
const PRO_PHOTO_FROM_XYZ: Mat3 = Mat3::invert(&PRO_PHOTO_TO_XYZ);

impl Primaries for ProPhotoPrimaries {
    type Native = D50;
    const R: [f32; 2] = [0.7347, 0.2653];
    const G: [f32; 2] = [0.1596, 0.8404];
    const B: [f32; 2] = [0.0366, 0.0001];
    const TO_XYZ_NATIVE: Mat3 = PRO_PHOTO_TO_XYZ;
    const FROM_XYZ_NATIVE: Mat3 = PRO_PHOTO_FROM_XYZ;
}

/// DCI-P3 primaries (DCI white point) for theatrical projection.
///
/// Same chromaticities as P3Primaries but with the DCI white point
/// instead of D65. Use this for true theatrical DCI-P3. For consumer
/// Display P3 (Apple, Android), use P3Primaries with D65.
///
/// Reference: SMPTE EG 432-1:2010.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct DciP3Primaries;

const DCI_P3_TO_XYZ: Mat3 = derive_rgb_to_xyz(
    [0.6800, 0.3200],
    [0.2650, 0.6900],
    [0.1500, 0.0600],
    DciWhite::WHITE_POINT_XYZ,
);
const DCI_P3_FROM_XYZ: Mat3 = Mat3::invert(&DCI_P3_TO_XYZ);

impl Primaries for DciP3Primaries {
    type Native = DciWhite;
    const R: [f32; 2] = [0.6800, 0.3200];
    const G: [f32; 2] = [0.2650, 0.6900];
    const B: [f32; 2] = [0.1500, 0.0600];
    const TO_XYZ_NATIVE: Mat3 = DCI_P3_TO_XYZ;
    const FROM_XYZ_NATIVE: Mat3 = DCI_P3_FROM_XYZ;
}

// Native impls, no adaptation needed.
impl_native!(SrgbPrimaries, D65);
impl_native!(P3Primaries, D65);
impl_native!(Rec2020Primaries, D65);
impl_native!(AcesAp1Primaries, AcesWhitePoint);
impl_native!(AcesAp0Primaries, AcesWhitePoint);
impl_native!(ProPhotoPrimaries, D50);
impl_native!(DciP3Primaries, DciWhite);

// Cross-illuminant impls via Bradford, computed at compile time.
impl_adapted!(AcesAp1Primaries, AcesWhitePoint, D65);
impl_adapted!(AcesAp0Primaries, AcesWhitePoint, D65);
impl_adapted!(AcesAp1Primaries, AcesWhitePoint, D50);
impl_adapted!(AcesAp0Primaries, AcesWhitePoint, D50);
impl_adapted!(ProPhotoPrimaries, D50, D65);
impl_adapted!(SrgbPrimaries, D65, D50);
impl_adapted!(P3Primaries, D65, D50);
impl_adapted!(Rec2020Primaries, D65, D50);
impl_adapted!(DciP3Primaries, DciWhite, D65);