farg 0.4.5

A Rust library for colorimetry, color space conversions, and color manipulation.
Documentation
use std::fmt::{Display, Formatter, Result as FmtResult};

#[cfg(feature = "chromaticity-rg")]
use super::Rg;
#[cfg(feature = "chromaticity-upvp")]
use super::Upvp;
#[cfg(feature = "chromaticity-uv")]
use super::Uv;
#[cfg(feature = "chromaticity-rg")]
use crate::space::RgbSpec;
use crate::{component::Component, space::Xyz};

/// CIE 1931 chromaticity coordinates (x, y).
///
/// Derived from XYZ by normalizing: x = X/(X+Y+Z), y = Y/(X+Y+Z).
/// Always available regardless of feature flags.
#[derive(Clone, Copy, Debug)]
pub struct Xy {
  x: Component,
  y: Component,
}

impl Xy {
  /// Creates new chromaticity coordinates from x and y values.
  pub fn new(x: impl Into<Component>, y: impl Into<Component>) -> Self {
    Self {
      x: x.into(),
      y: y.into(),
    }
  }

  /// Creates new chromaticity coordinates in a const context.
  pub const fn new_const(x: f64, y: f64) -> Self {
    Self {
      x: Component::new_const(x),
      y: Component::new_const(y),
    }
  }

  /// Returns the [x, y] components as an array.
  pub fn components(&self) -> [f64; 2] {
    [self.x.0, self.y.0]
  }

  /// Converts to rg chromaticity coordinates in the given RGB space.
  #[cfg(feature = "chromaticity-rg")]
  pub fn to_rg<S>(&self) -> Rg<S>
  where
    S: RgbSpec,
  {
    let xyz = self.to_xyz(1.0);
    let [r, g, b] = *S::inversed_xyz_matrix() * xyz;
    let sum = r + g + b;

    if sum == 0.0 {
      Rg::new(0.0, 0.0)
    } else {
      Rg::new(r / sum, g / sum)
    }
  }

  /// Converts to CIE 1976 u'v' chromaticity coordinates.
  #[cfg(feature = "chromaticity-upvp")]
  pub fn to_upvp(&self) -> Upvp {
    let [x, y] = self.components();
    let denom = -2.0 * x + 12.0 * y + 3.0;

    if denom == 0.0 {
      Upvp::new(0.0, 0.0)
    } else {
      Upvp::new((4.0 * x) / denom, (9.0 * y) / denom)
    }
  }

  /// Converts to CIE 1960 uv chromaticity coordinates.
  #[cfg(feature = "chromaticity-uv")]
  pub fn to_uv(&self) -> Uv {
    let [x, y] = self.components();
    let denom = -2.0 * x + 12.0 * y + 3.0;

    if denom == 0.0 {
      Uv::new(0.0, 0.0)
    } else {
      Uv::new((4.0 * x) / denom, (6.0 * y) / denom)
    }
  }

  /// Reconstructs XYZ tristimulus values from chromaticity and the given luminance (Y).
  pub fn to_xyz(&self, luminance: impl Into<Component>) -> Xyz {
    let luminance = luminance.into().0;
    let [x, y] = self.components();

    if y == 0.0 {
      Xyz::new(0.0, 0.0, 0.0)
    } else {
      Xyz::new((x / y) * luminance, luminance, ((1.0 - x - y) / y) * luminance)
    }
  }

  /// Returns the x chromaticity coordinate.
  pub fn x(&self) -> f64 {
    self.x.0
  }

  /// Returns the y chromaticity coordinate.
  pub fn y(&self) -> f64 {
    self.y.0
  }
}

impl Display for Xy {
  fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
    write!(
      f,
      "({:.precision$}, {:.precision$})",
      self.x,
      self.y,
      precision = f.precision().unwrap_or(4)
    )
  }
}

impl<T> From<[T; 2]> for Xy
where
  T: Into<Component>,
{
  fn from([x, y]: [T; 2]) -> Self {
    Self::new(x, y)
  }
}

#[cfg(feature = "chromaticity-rg")]
impl<S> From<Rg<S>> for Xy
where
  S: RgbSpec,
{
  fn from(rg: Rg<S>) -> Self {
    rg.to_xy()
  }
}

#[cfg(feature = "chromaticity-upvp")]
impl From<Upvp> for Xy {
  fn from(upvp: Upvp) -> Self {
    upvp.to_xy()
  }
}

#[cfg(feature = "chromaticity-uv")]
impl From<Uv> for Xy {
  fn from(uv: Uv) -> Self {
    uv.to_xy()
  }
}

impl From<Xyz> for Xy {
  fn from(xyz: Xyz) -> Self {
    xyz.chromaticity()
  }
}

impl<T> PartialEq<T> for Xy
where
  T: Into<Xy> + Copy,
{
  fn eq(&self, other: &T) -> bool {
    let other = (*other).into();
    self.x == other.x && self.y == other.y
  }
}

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

  mod display {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_formats_with_default_precision() {
      let xy = Xy::new(0.31271, 0.32902);

      assert_eq!(format!("{}", xy), "(0.3127, 0.3290)");
    }

    #[test]
    fn it_formats_with_custom_precision() {
      let xy = Xy::new(0.31271, 0.32902);

      assert_eq!(format!("{:.2}", xy), "(0.31, 0.33)");
      assert_eq!(format!("{:.6}", xy), "(0.312710, 0.329020)");
    }
  }

  mod from_xyz {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_converts_from_xyz() {
      let xyz = Xyz::new(0.95047, 1.0, 1.08883);
      let xy: Xy = xyz.into();

      assert_eq!(xy.x(), 0.95047 / (0.95047 + 1.0 + 1.08883));
      assert_eq!(xy.y(), 1.0 / (0.95047 + 1.0 + 1.08883));
    }
  }

  mod partial_eq {
    use pretty_assertions::{assert_eq, assert_ne};

    use super::*;

    #[test]
    fn it_compares_equal_values() {
      let a = Xy::new(0.31271, 0.32902);
      let b = Xy::new(0.31271, 0.32902);

      assert_eq!(a, b);
    }

    #[test]
    fn it_compares_unequal_values() {
      let a = Xy::new(0.31271, 0.32902);
      let b = Xy::new(0.31271, 0.33);

      assert_ne!(a, b);
    }

    #[test]
    fn it_compares_with_array() {
      let xy = Xy::new(0.31271, 0.32902);

      assert_eq!(xy, [0.31271, 0.32902]);
    }
  }

  #[cfg(feature = "chromaticity-rg")]
  mod to_rg {
    use super::*;
    use crate::space::Srgb;

    #[test]
    fn it_converts_to_rg() {
      let xy = Xy::new(0.31271, 0.32902);
      let rg: Rg<Srgb> = xy.to_rg();
      let xyz = xy.to_xyz(1.0);
      let [r, g, b] = *Srgb::inversed_xyz_matrix() * xyz;
      let sum = r + g + b;

      assert!((rg.r() - r / sum).abs() < 1e-10);
      assert!((rg.g() - g / sum).abs() < 1e-10);
    }

    #[test]
    fn it_handles_zero_sum() {
      let xy = Xy::new(0.0, 0.0);
      let rg: Rg<Srgb> = xy.to_rg();

      assert!(rg.r() == 0.0 || rg.r().is_finite());
      assert!(rg.g() == 0.0 || rg.g().is_finite());
    }
  }

  #[cfg(feature = "chromaticity-upvp")]
  mod to_upvp {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_converts_to_upvp() {
      let xy = Xy::new(0.31271, 0.32902);
      let upvp = xy.to_upvp();
      let denom = -2.0 * 0.31271 + 12.0 * 0.32902 + 3.0;

      assert_eq!(upvp.u(), (4.0 * 0.31271) / denom);
      assert_eq!(upvp.v(), (9.0 * 0.32902) / denom);
    }

    #[test]
    fn it_handles_zero_denominator() {
      let xy = Xy::new(1.5, 0.0);
      let upvp = xy.to_upvp();

      assert_eq!(upvp.u(), 0.0);
      assert_eq!(upvp.v(), 0.0);
    }
  }

  #[cfg(feature = "chromaticity-uv")]
  mod to_uv {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_converts_to_uv() {
      let xy = Xy::new(0.31271, 0.32902);
      let uv = xy.to_uv();
      let denom = -2.0 * 0.31271 + 12.0 * 0.32902 + 3.0;

      assert_eq!(uv.u(), (4.0 * 0.31271) / denom);
      assert_eq!(uv.v(), (6.0 * 0.32902) / denom);
    }

    #[test]
    fn it_handles_zero_denominator() {
      let xy = Xy::new(1.5, 0.0);
      let uv = xy.to_uv();

      assert_eq!(uv.u(), 0.0);
      assert_eq!(uv.v(), 0.0);
    }
  }

  mod to_xyz {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn it_converts_to_xyz_with_luminance() {
      let xy = Xy::new(0.31271, 0.32902);
      let xyz = xy.to_xyz(1.0);

      assert_eq!(xyz.y(), 1.0);
      assert_eq!(xyz.x(), 0.31271 / 0.32902);
      assert_eq!(xyz.z(), (1.0 - 0.31271 - 0.32902) / 0.32902);
    }

    #[test]
    fn it_handles_zero_y() {
      let xy = Xy::new(0.5, 0.0);
      let xyz = xy.to_xyz(1.0);

      assert_eq!(xyz.x(), 0.0);
      assert_eq!(xyz.y(), 0.0);
      assert_eq!(xyz.z(), 0.0);
    }

    #[test]
    fn it_scales_by_luminance() {
      let xy = Xy::new(0.31271, 0.32902);
      let xyz = xy.to_xyz(0.5);

      assert_eq!(xyz.y(), 0.5);
    }
  }
}