farg 0.4.5

A Rust library for colorimetry, color space conversions, and color manipulation.
Documentation
use super::{Cmf, ConeResponse, Spd, Table};
use crate::space::{Lms, Xyz};

/// Spectral cone sensitivity functions mapping wavelengths to LMS cone responses.
#[derive(Clone, Copy, Debug)]
pub struct ConeFundamentals(&'static [(u32, ConeResponse)]);

impl ConeFundamentals {
  /// Creates new cone fundamentals from static wavelength-response pairs.
  pub const fn new(table: &'static [(u32, ConeResponse)]) -> Self {
    Self(table)
  }

  /// Alias for [`Self::spectral_power_distribution_to_lms`].
  pub fn spd_to_lms(&self, spd: &Spd) -> Lms {
    self.spectral_power_distribution_to_lms(spd)
  }

  /// Integrates a spectral power distribution with cone fundamentals to produce LMS values.
  pub fn spectral_power_distribution_to_lms(&self, spd: &Spd) -> Lms {
    let step = self.step() as f64;
    let mut components = [0.0_f64; 3];

    for (wavelength, response) in self.table().iter() {
      let Some(&spd_response) = spd.at(*wavelength) else {
        continue;
      };

      let lms = response.components();
      components[0] += spd_response * lms[0] * step;
      components[1] += spd_response * lms[1] * step;
      components[2] += spd_response * lms[2] * step;
    }

    Lms::new(components[0], components[1], components[2])
  }
}

impl From<Cmf> for ConeFundamentals {
  fn from(cmf: Cmf) -> Self {
    let data: Box<[(u32, ConeResponse)]> = cmf
      .table()
      .iter()
      .map(|(wavelength, xyz)| {
        let [l, m, s] = Xyz::from(xyz.components()).to_lms().components();
        (*wavelength, ConeResponse::new(l, m, s))
      })
      .collect();
    Self::new(Box::leak(data))
  }
}

impl Table for ConeFundamentals {
  type Value = ConeResponse;

  fn table(&self) -> &[(u32, Self::Value)] {
    self.0
  }
}

#[cfg(test)]
mod test {
  use super::*;
  use crate::spectral::TristimulusResponse;

  static TEST_CONE_FUNDAMENTALS: &[(u32, ConeResponse)] = &[
    (380, ConeResponse::new(0.001, 0.0001, 0.006)),
    (400, ConeResponse::new(0.014, 0.0004, 0.068)),
    (420, ConeResponse::new(0.134, 0.004, 0.646)),
    (440, ConeResponse::new(0.348, 0.023, 1.747)),
  ];

  static TEST_SPD: &[(u32, f64)] = &[(380, 0.5), (400, 0.8), (420, 1.0), (440, 0.9)];

  mod from_cmf {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_converts_cmf_to_cone_fundamentals() {
      static CMF: &[(u32, TristimulusResponse)] = &[
        (380, TristimulusResponse::new(0.001, 0.0001, 0.006)),
        (400, TristimulusResponse::new(0.014, 0.0004, 0.068)),
      ];
      let cmf = Cmf::new(CMF);
      let fundamentals = ConeFundamentals::from(cmf);

      assert_eq!(fundamentals.len(), 2);
      assert_eq!(fundamentals.min_wavelength(), Some(380));
      assert_eq!(fundamentals.max_wavelength(), Some(400));
    }

    #[test]
    fn it_preserves_wavelengths() {
      static CMF: &[(u32, TristimulusResponse)] = &[
        (380, TristimulusResponse::new(0.1, 0.2, 0.3)),
        (420, TristimulusResponse::new(0.4, 0.5, 0.6)),
      ];
      let cmf = Cmf::new(CMF);
      let fundamentals = ConeFundamentals::from(cmf);
      let wavelengths: Vec<u32> = fundamentals.wavelengths().collect();

      assert_eq!(wavelengths, vec![380, 420]);
    }
  }

  mod spectral_power_distribution_to_lms {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_integrates_spd_with_cone_fundamentals() {
      let fundamentals = ConeFundamentals::new(TEST_CONE_FUNDAMENTALS);
      let spd = Spd::new(TEST_SPD);
      let lms = fundamentals.spectral_power_distribution_to_lms(&spd);

      assert!(lms.l() > 0.0);
      assert!(lms.m() > 0.0);
      assert!(lms.s() > 0.0);
    }

    #[test]
    fn it_weights_by_step_size() {
      let fundamentals = ConeFundamentals::new(TEST_CONE_FUNDAMENTALS);
      let spd = Spd::new(TEST_SPD);
      let lms = fundamentals.spectral_power_distribution_to_lms(&spd);

      let step = fundamentals.step() as f64;
      let mut expected = [0.0_f64; 3];
      for (wavelength, response) in fundamentals.table().iter() {
        if let Some(&power) = spd.at(*wavelength) {
          let [l, m, s] = response.components();
          expected[0] += power * l * step;
          expected[1] += power * m * step;
          expected[2] += power * s * step;
        }
      }

      assert_eq!(lms.l(), expected[0]);
      assert_eq!(lms.m(), expected[1]);
      assert_eq!(lms.s(), expected[2]);
    }

    #[test]
    fn it_returns_zero_for_non_overlapping_wavelengths() {
      static NON_OVERLAPPING_SPD: &[(u32, f64)] = &[(500, 1.0), (520, 1.0)];
      let fundamentals = ConeFundamentals::new(TEST_CONE_FUNDAMENTALS);
      let spd = Spd::new(NON_OVERLAPPING_SPD);
      let lms = fundamentals.spectral_power_distribution_to_lms(&spd);

      assert_eq!(lms.l(), 0.0);
      assert_eq!(lms.m(), 0.0);
      assert_eq!(lms.s(), 0.0);
    }
  }

  mod spd_to_lms {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_delegates_to_spectral_power_distribution_to_lms() {
      let fundamentals = ConeFundamentals::new(TEST_CONE_FUNDAMENTALS);
      let spd = Spd::new(TEST_SPD);

      assert_eq!(
        fundamentals.spd_to_lms(&spd),
        fundamentals.spectral_power_distribution_to_lms(&spd)
      );
    }
  }
}