jay-compositor 1.15.0

The Jay compositor
use crate::cmm::cmm_render_intent::RenderIntent;
use crate::cmm::cmm_transform::ColorMatrix;
use crate::cmm::cmm_transform::Xyz;
use crate::utils::ordered_float::F64;
use jay_proc::StrFmt;
use jay_proc::jay_hash;

#[jay_hash]
#[derive(Copy, Clone, Debug, Eq, StrFmt)]
pub struct Luminance {
    pub min: F64,
    pub max: F64,
    pub white: F64,
}

#[jay_hash]
#[derive(Copy, Clone, Debug, Eq, StrFmt)]
pub struct TargetLuminance {
    pub min: F64,
    pub max: F64,
}

impl Luminance {
    pub const SRGB: Self = Self {
        min: F64(0.2),
        max: F64(80.0),
        white: F64(80.0),
    };

    pub const BT1886: Self = Self {
        min: F64(0.01),
        max: F64(100.0),
        white: F64(100.0),
    };

    pub const ST2084_PQ: Self = Self {
        min: F64(0.0),
        max: F64(10000.0),
        white: F64(203.0),
    };

    #[expect(unused)]
    pub const HLG: Self = Self {
        min: F64(0.005),
        max: F64(1000.0),
        white: F64(203.0),
    };

    pub const WINDOWS_SCRGB: Self = Self {
        min: F64(0.0),
        max: F64(80.0),
        // This causes the white balance formula (with target ST2084_PQ) to simplify to
        // `Y * 80 / 10000`, meaning that sRGB pure white maps to a luminance of
        // 80 cd/m^2.
        white: F64(203.0),
    };
}

impl Luminance {
    pub fn to_target(&self) -> TargetLuminance {
        TargetLuminance {
            min: self.min,
            max: self.max,
        }
    }

    pub fn embeds_into(&self, other: &Self, intent: RenderIntent) -> bool {
        const EPSILON: f64 = 0.001;
        let scaler = scaler(self, other);
        if (scaler - 1.0).abs() > EPSILON {
            return false;
        }
        let offset = offset(self, other, intent);
        if offset.abs() > EPSILON {
            return false;
        }
        true
    }
}

impl Default for Luminance {
    fn default() -> Self {
        Self::SRGB
    }
}

#[allow(non_snake_case)]
pub fn white_balance(
    from: &Luminance,
    to: &Luminance,
    w_to: (F64, F64),
    intent: RenderIntent,
) -> ColorMatrix<Xyz, Xyz> {
    let a = scaler(from, to);
    let d = offset(from, to, intent);
    let s = a - d;
    let (F64(x_to), F64(y_to)) = w_to;
    let X_to = x_to / y_to;
    let Y_to = 1.0;
    let Z_to = (1.0 - x_to - y_to) / y_to;
    ColorMatrix::new([
        [s, 0.0, 0.0, d * X_to],
        [0.0, s, 0.0, d * Y_to],
        [0.0, 0.0, s, d * Z_to],
    ])
}

fn scaler(from: &Luminance, to: &Luminance) -> f64 {
    let a =
        (from.max - from.min) / (to.max - to.min) * (to.white - to.min) / (from.white - from.min);
    a.0
}

fn offset(from: &Luminance, to: &Luminance, intent: RenderIntent) -> f64 {
    match intent.black_point_compensation() {
        true => 0.0,
        false => ((from.min - to.min) / (to.max - to.min)).0,
    }
}