use core::marker::PhantomData;
use crate::color::Color;
use crate::encoding::Linear;
use crate::fixed::Q4_28;
use crate::space::ColorSpace;
pub struct Gain<S: ColorSpace> {
coefs: [Q4_28; 3],
_pd: PhantomData<fn() -> S>,
}
impl<S: ColorSpace> Copy for Gain<S> {}
impl<S: ColorSpace> Clone for Gain<S> {
fn clone(&self) -> Self {
*self
}
}
impl<S: ColorSpace> PartialEq for Gain<S> {
fn eq(&self, other: &Self) -> bool {
self.coefs == other.coefs
}
}
impl<S: ColorSpace> Eq for Gain<S> {}
impl<S: ColorSpace> core::fmt::Debug for Gain<S> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("Gain").field("coefs", &self.coefs).finish()
}
}
impl<S: ColorSpace> Gain<S> {
#[must_use]
pub const fn from_q428(coefs: [Q4_28; 3]) -> Self {
Self {
coefs,
_pd: PhantomData,
}
}
#[must_use]
pub const fn apply(&self, color: Color<S, Linear>) -> Color<S, Linear> {
let [g0, g1, g2] = self.coefs;
let [c0, c1, c2] = color.ch;
Color::new([
crate::arith::shift_round_sat_q0_16(crate::arith::mul_acc(0, g0, c0)),
crate::arith::shift_round_sat_q0_16(crate::arith::mul_acc(0, g1, c1)),
crate::arith::shift_round_sat_q0_16(crate::arith::mul_acc(0, g2, c2)),
])
}
#[cfg(feature = "f32")]
#[must_use]
pub fn apply_f32(&self, color: crate::ColorF32<S, Linear>) -> crate::ColorF32<S, Linear> {
let [g0, g1, g2] = self.coefs;
let [c0, c1, c2] = color.ch;
crate::ColorF32::new([
crate::color_f32::sat_unit(g0.to_f32() * crate::color_f32::sat_unit(c0)),
crate::color_f32::sat_unit(g1.to_f32() * crate::color_f32::sat_unit(c1)),
crate::color_f32::sat_unit(g2.to_f32() * crate::color_f32::sat_unit(c2)),
])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encoding::Linear;
use crate::fixed::Q0_16;
use crate::space::Srgb;
#[test]
fn identity_gain_is_unchanged() {
let one = Q4_28::ONE;
let g = Gain::<Srgb>::from_q428([one, one, one]);
let c = Color::<Srgb, Linear>::new(Q0_16::array_from_raw([10, 20, 30]));
assert_eq!(g.apply(c), c);
}
#[test]
fn gain_two_saturates_full_scale() {
let two = Q4_28::ONE.saturating_add(Q4_28::ONE);
let g = Gain::<Srgb>::from_q428([two, two, two]);
let c = Color::<Srgb, Linear>::new(Q0_16::array_from_raw([40_000, 65535, 1]));
let out = g.apply(c);
assert_eq!(out.ch, Q0_16::array_from_raw([65535, 65535, 2]));
}
#[cfg(feature = "f32")]
#[test]
fn apply_f32_clamps_negative_channels_before_signed_gain() {
let neg_one = Q4_28::ONE.saturating_neg();
let g = Gain::<Srgb>::from_q428([neg_one, neg_one, neg_one]);
let c = crate::ColorF32::<Srgb, Linear>::new([-1.0, f32::NEG_INFINITY, 0.25]);
assert_eq!(g.apply_f32(c).ch, [0.0, 0.0, 0.0]);
}
}