colr 0.1.1

A general purpose, extensible color type unifying storage, channel layouts, and color spaces at the type level.
Documentation
#![no_std]
#![warn(missing_docs)]
#![doc = include_str!("../README.md")]

//! Universal color library.
//!
//! Provides compile-time-safe color space types, conversions, and storage
//! formats. All colorimetric math is computed at compile time with zero
//! runtime cost.
//!
//! A color value is `Color<S, Sp>` where:
//! - `S` is the storage type (`[f32; 4]`, `glam::Vec4`)
//! - `Sp` is the color space (`Srgb`, `AcesCg`, `Xyz<D65>`)
//!
//! For RGB spaces the layout is a parameter of the space itself:
//! `RgbSpace<P, TF, L>` where `L` defaults to `Rgba`.

#[cfg(feature = "std")]
extern crate std;

pub mod adaptation;
pub mod illuminant;
pub mod math;
pub mod perceptual;
pub mod primaries;
pub mod rgb;
pub mod spectral;
pub mod transfer;
pub mod xyz;

use core::marker::PhantomData;

pub use adaptation::{Bradford, Cat02, Cat16, ChromaticAdaptation, VonKries};
pub use illuminant::{AcesWhitePoint, D50, D60, D65, DciWhite, Illuminant};
pub use perceptual::{LCh, LChD65, Lab, LabD65, Oklab, Oklch};
pub use rgb::{
    Abgr, Aces2065, AcesCc, AcesCct, AcesCg, Argb, Bgr, Bgra, ChannelMap, DciP3, DisplayP3, Hdr10, Hlg, LinearP3,
    LinearProPhoto, LinearRec2020, LinearSrgb, P3D65Gamma26, ProPhoto, Rec709, Rgb, RgbColorSpace, RgbSpace, Rgba,
    Srgb,
};
pub use spectral::{
    Bispectral, IsBispectral, IsRadiance, IsReflectance, IsTransmittance, Radiance, Reflectance, SpectralKind,
    SpectralSpace, Transmittance, WavelengthGrid,
};
pub use transfer::{
    AcesCcTf, AcesCctTf, DciP3Tf, HlgTf, IsDisplayReferred, IsLinearEncoding, IsSceneReferred, LinearTf, PqTf,
    ProPhotoTf, Rec709Tf, SrgbTf, TransferFunction,
};
pub use xyz::{Xyz, XyzAces, XyzD50, XyzD65};

/// Compatibility marker between two types.
///
/// Layout implements `Asserts<S>` for each storage type it accepts.
/// Space implements `Asserts<S>` for each storage type it can be carried by.
pub trait Asserts<T: 'static> {}

/// A color domain defined by a fixed set of channels.
pub trait ColorSpace: 'static {
    /// Number of channels this space defines.
    const CHANNELS: usize;

    /// Per-primary contribution to CIE relative luminance Y (CIE 015:2018).
    /// None for non-RGB spaces.
    const LUMINANCE_WEIGHTS: Option<[f32; 3]>;
}

/// A color value in color space `Sp` carried by storage type `S`.
///
/// `Sp: Asserts<S>` is the only construction gate. For RGB spaces the
/// layout is encoded in `Sp` as `RgbSpace<P, TF, L>`.
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color<S, Sp>
where
    Sp: Asserts<S>,
    S: Copy + 'static,
    Sp: 'static,
{
    storage: S,
    _space: PhantomData<Sp>,
}

impl<S, Sp> Color<S, Sp>
where
    S: Copy + 'static,
    Sp: Asserts<S> + 'static,
{
    /// Construct without bounds checking.
    #[inline(always)]
    pub fn new_unchecked(storage: S) -> Self {
        Self {
            storage,
            _space: PhantomData,
        }
    }

    /// Returns the underlying storage value.
    #[inline(always)]
    pub fn inner(&self) -> S {
        self.storage
    }

    /// Transform to `Dst` with no runtime context. Convenience for the
    /// common case where `Ctx = ()`.
    #[inline(always)]
    pub fn transform<Dst>(self) -> Dst
    where
        Dst: Transform<Self>,
    {
        Dst::transform_from(self, &())
    }

    /// Transform to `Dst` infallibly. Pass `&()` for static transforms.
    #[inline(always)]
    pub fn transform_to<Dst, Ctx>(self, ctx: &Ctx) -> Dst
    where
        Ctx: ?Sized,
        Dst: Transform<Self, Ctx>,
    {
        Dst::transform_from(self, ctx)
    }

    /// Transform to `Dst` with no runtime context, returning `OutOfGamut`
    /// if any channel clips. Convenience for the common case where `Ctx = ()`.
    #[inline(always)]
    pub fn try_transform<Dst>(self) -> Result<Dst, OutOfGamut<Dst>>
    where
        Dst: TryTransform<Self>,
    {
        Dst::try_transform_from(self, &())
    }

    /// Transform to `Dst`, returning `OutOfGamut` if any channel clips.
    #[inline(always)]
    pub fn try_transform_to<Dst, Ctx>(self, ctx: &Ctx) -> Result<Dst, OutOfGamut<Dst>>
    where
        Ctx: ?Sized,
        Dst: TryTransform<Self, Ctx>,
    {
        Dst::try_transform_from(self, ctx)
    }

    /// Transform through intermediate `Via` to `Dst`. Both legs static.
    #[inline(always)]
    pub fn transform_via<Via, Dst>(self) -> Dst
    where
        Via: Transform<Self>,
        Dst: Transform<Via>,
    {
        Dst::transform_from(Via::transform_from(self, &()), &())
    }

    /// Transform through `Via` to `Dst`. Returns `OutOfGamut` if either
    /// leg clips. The clamped value from the first leg is forwarded to
    /// the second so a result is always produced.
    #[inline(always)]
    pub fn try_transform_via<Via, Dst>(self) -> Result<Dst, OutOfGamut<Dst>>
    where
        Via: TryTransform<Self>,
        Dst: TryTransform<Via>,
    {
        let (via, first_clipped) = match Via::try_transform_from(self, &()) {
            Ok(v) => (v, false),
            Err(e) => (e.clamped, true),
        };
        match Dst::try_transform_from(via, &()) {
            Ok(dst) if !first_clipped => Ok(dst),
            Ok(dst) => Err(OutOfGamut { clamped: dst }),
            Err(e) => Err(e),
        }
    }
}

/// Produce `Self` from a source color `Src` infallibly.
///
/// Implement for transforms that cannot clip: RGB to XYZ, layout reorders,
/// and transforms between spaces whose gamuts are compatible.
/// A blanket impl provides `TryTransform` for free.
///
/// `Ctx` defaults to `()` for static transforms:
///
/// | Transform                    | Ctx               |
/// |------------------------------|-------------------|
/// | sRGB to XYZ                  | ()                |
/// | Device CMYK to XYZ           | IccProfile        |
/// | XYZ to CAM16-UCS             | ViewingConditions |
/// | Relative XYZ to Absolute XYZ | f32 (cd/m2)       |
pub trait Transform<Src, Ctx: ?Sized = ()>: Sized + 'static {
    /// Perform the transform.
    fn transform_from(src: Src, ctx: &Ctx) -> Self;
}

/// Produce `Self` from a source color `Src`, returning `OutOfGamut` if
/// any channel clips.
///
/// Implement directly for lossy transforms such as XYZ to bounded RGB.
/// Infallible impls via `Transform` receive this for free.
pub trait TryTransform<Src, Ctx: ?Sized = ()>: Sized + 'static {
    /// Perform the transform with bounds checking.
    fn try_transform_from(src: Src, ctx: &Ctx) -> Result<Self, OutOfGamut<Self>>;
}

/// Blanket: anything infallibly transformable also satisfies `TryTransform`.
impl<Src, Ctx, Dst> TryTransform<Src, Ctx> for Dst
where
    Ctx: ?Sized,
    Dst: Transform<Src, Ctx>,
{
    fn try_transform_from(src: Src, ctx: &Ctx) -> Result<Self, OutOfGamut<Self>> {
        Ok(Self::transform_from(src, ctx))
    }
}

/// Marks a `ColorSpace` as linear-light.
///
/// Values are proportional to physical light intensity. Required for
/// premultiplied alpha compositing, addition of light sources, and
/// texture filtering.
pub trait LinearLight: ColorSpace {}

/// Marks a `ColorSpace` as scene-referred.
///
/// Encodes physical scene radiance. Unbounded above. Requires tone
/// mapping before display.
pub trait SceneReferred: ColorSpace {}

/// Marks a `ColorSpace` as display-referred.
///
/// Encodes a device's bounded output range, already tone mapped.
pub trait DisplayReferred: ColorSpace {}

/// Marks a `ColorSpace` where luminance has photometric units (cd/m2).
pub trait Absolute: ColorSpace {}

/// Distance between two colors in `ColorSpace` `Sp`.
pub trait Delta<Sp: ColorSpace> {
    /// The scalar type returned by this metric.
    type Output;

    /// Compute the distance between `a` and `b`.
    fn delta<S>(a: Color<S, Sp>, b: Color<S, Sp>) -> Self::Output
    where
        S: Copy + 'static,
        Sp: Asserts<S>;
}

/// Returned when a transform produces values outside the target's valid bounds.
///
/// `clamped` is always present; callers never lose data:
///
/// ```rust,ignore
/// // Strict: propagate the error.
/// let exact = color.try_transform_to(&()).unwrap();
///
/// // Permissive: always produce a value.
/// let value = color.try_transform_to(&()).unwrap_or_else(|e| e.clamped);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OutOfGamut<T> {
    /// Closest in-bounds approximation with all clipped channels clamped
    /// to the boundary of the target space's valid range.
    pub clamped: T,
}

impl<T> core::fmt::Display for OutOfGamut<T> {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(f, "color out of gamut")
    }
}

impl<T: core::fmt::Debug> core::error::Error for OutOfGamut<T> {}