Skip to main content

colr/
lib.rs

1#![no_std]
2#![warn(missing_docs)]
3#![doc = include_str!("../README.md")]
4
5//! Universal color library.
6//!
7//! Provides compile-time-safe color space types, conversions, and storage
8//! formats. All colorimetric math is computed at compile time with zero
9//! runtime cost.
10//!
11//! A color value is `Color<S, Sp>` where:
12//! - `S` is the storage type (`[f32; 4]`, `glam::Vec4`)
13//! - `Sp` is the color space (`Srgb`, `AcesCg`, `Xyz<D65>`)
14//!
15//! For RGB spaces the layout is a parameter of the space itself:
16//! `RgbSpace<P, TF, L>` where `L` defaults to `Rgba`.
17
18#[cfg(feature = "std")]
19extern crate std;
20
21pub mod adaptation;
22pub mod illuminant;
23pub mod math;
24pub mod perceptual;
25pub mod primaries;
26pub mod rgb;
27pub mod spectral;
28pub mod transfer;
29pub mod xyz;
30
31use core::marker::PhantomData;
32
33pub use adaptation::{Bradford, Cat02, Cat16, ChromaticAdaptation, VonKries};
34pub use illuminant::{AcesWhitePoint, D50, D60, D65, DciWhite, Illuminant};
35pub use perceptual::{LCh, LChD65, Lab, LabD65, Oklab, Oklch};
36pub use rgb::{
37    Abgr, Aces2065, AcesCc, AcesCct, AcesCg, Argb, Bgr, Bgra, ChannelMap, DciP3, DisplayP3, Hdr10, Hlg, LinearP3,
38    LinearProPhoto, LinearRec2020, LinearSrgb, P3D65Gamma26, ProPhoto, Rec709, Rgb, RgbColorSpace, RgbSpace, Rgba,
39    Srgb,
40};
41pub use spectral::{
42    Bispectral, IsBispectral, IsRadiance, IsReflectance, IsTransmittance, Radiance, Reflectance, SpectralKind,
43    SpectralSpace, Transmittance, WavelengthGrid,
44};
45pub use transfer::{
46    AcesCcTf, AcesCctTf, DciP3Tf, HlgTf, IsDisplayReferred, IsLinearEncoding, IsSceneReferred, LinearTf, PqTf,
47    ProPhotoTf, Rec709Tf, SrgbTf, TransferFunction,
48};
49pub use xyz::{Xyz, XyzAces, XyzD50, XyzD65};
50
51/// Compatibility marker between two types.
52///
53/// Layout implements `Asserts<S>` for each storage type it accepts.
54/// Space implements `Asserts<S>` for each storage type it can be carried by.
55pub trait Asserts<T: 'static> {}
56
57/// A color domain defined by a fixed set of channels.
58pub trait ColorSpace: 'static {
59    /// Number of channels this space defines.
60    const CHANNELS: usize;
61
62    /// Per-primary contribution to CIE relative luminance Y (CIE 015:2018).
63    /// None for non-RGB spaces.
64    const LUMINANCE_WEIGHTS: Option<[f32; 3]>;
65}
66
67/// A color value in color space `Sp` carried by storage type `S`.
68///
69/// `Sp: Asserts<S>` is the only construction gate. For RGB spaces the
70/// layout is encoded in `Sp` as `RgbSpace<P, TF, L>`.
71#[repr(transparent)]
72#[derive(Debug, Clone, Copy, PartialEq)]
73pub struct Color<S, Sp>
74where
75    Sp: Asserts<S>,
76    S: Copy + 'static,
77    Sp: 'static,
78{
79    storage: S,
80    _space: PhantomData<Sp>,
81}
82
83impl<S, Sp> Color<S, Sp>
84where
85    S: Copy + 'static,
86    Sp: Asserts<S> + 'static,
87{
88    /// Construct without bounds checking.
89    #[inline(always)]
90    pub fn new_unchecked(storage: S) -> Self {
91        Self {
92            storage,
93            _space: PhantomData,
94        }
95    }
96
97    /// Returns the underlying storage value.
98    #[inline(always)]
99    pub fn inner(&self) -> S {
100        self.storage
101    }
102
103    /// Transform to `Dst` with no runtime context. Convenience for the
104    /// common case where `Ctx = ()`.
105    #[inline(always)]
106    pub fn transform<Dst>(self) -> Dst
107    where
108        Dst: Transform<Self>,
109    {
110        Dst::transform_from(self, &())
111    }
112
113    /// Transform to `Dst` infallibly. Pass `&()` for static transforms.
114    #[inline(always)]
115    pub fn transform_to<Dst, Ctx>(self, ctx: &Ctx) -> Dst
116    where
117        Ctx: ?Sized,
118        Dst: Transform<Self, Ctx>,
119    {
120        Dst::transform_from(self, ctx)
121    }
122
123    /// Transform to `Dst` with no runtime context, returning `OutOfGamut`
124    /// if any channel clips. Convenience for the common case where `Ctx = ()`.
125    #[inline(always)]
126    pub fn try_transform<Dst>(self) -> Result<Dst, OutOfGamut<Dst>>
127    where
128        Dst: TryTransform<Self>,
129    {
130        Dst::try_transform_from(self, &())
131    }
132
133    /// Transform to `Dst`, returning `OutOfGamut` if any channel clips.
134    #[inline(always)]
135    pub fn try_transform_to<Dst, Ctx>(self, ctx: &Ctx) -> Result<Dst, OutOfGamut<Dst>>
136    where
137        Ctx: ?Sized,
138        Dst: TryTransform<Self, Ctx>,
139    {
140        Dst::try_transform_from(self, ctx)
141    }
142
143    /// Transform through intermediate `Via` to `Dst`. Both legs static.
144    #[inline(always)]
145    pub fn transform_via<Via, Dst>(self) -> Dst
146    where
147        Via: Transform<Self>,
148        Dst: Transform<Via>,
149    {
150        Dst::transform_from(Via::transform_from(self, &()), &())
151    }
152
153    /// Transform through `Via` to `Dst`. Returns `OutOfGamut` if either
154    /// leg clips. The clamped value from the first leg is forwarded to
155    /// the second so a result is always produced.
156    #[inline(always)]
157    pub fn try_transform_via<Via, Dst>(self) -> Result<Dst, OutOfGamut<Dst>>
158    where
159        Via: TryTransform<Self>,
160        Dst: TryTransform<Via>,
161    {
162        let (via, first_clipped) = match Via::try_transform_from(self, &()) {
163            Ok(v) => (v, false),
164            Err(e) => (e.clamped, true),
165        };
166        match Dst::try_transform_from(via, &()) {
167            Ok(dst) if !first_clipped => Ok(dst),
168            Ok(dst) => Err(OutOfGamut { clamped: dst }),
169            Err(e) => Err(e),
170        }
171    }
172}
173
174/// Produce `Self` from a source color `Src` infallibly.
175///
176/// Implement for transforms that cannot clip: RGB to XYZ, layout reorders,
177/// and transforms between spaces whose gamuts are compatible.
178/// A blanket impl provides `TryTransform` for free.
179///
180/// `Ctx` defaults to `()` for static transforms:
181///
182/// | Transform                    | Ctx               |
183/// |------------------------------|-------------------|
184/// | sRGB to XYZ                  | ()                |
185/// | Device CMYK to XYZ           | IccProfile        |
186/// | XYZ to CAM16-UCS             | ViewingConditions |
187/// | Relative XYZ to Absolute XYZ | f32 (cd/m2)       |
188pub trait Transform<Src, Ctx: ?Sized = ()>: Sized + 'static {
189    /// Perform the transform.
190    fn transform_from(src: Src, ctx: &Ctx) -> Self;
191}
192
193/// Produce `Self` from a source color `Src`, returning `OutOfGamut` if
194/// any channel clips.
195///
196/// Implement directly for lossy transforms such as XYZ to bounded RGB.
197/// Infallible impls via `Transform` receive this for free.
198pub trait TryTransform<Src, Ctx: ?Sized = ()>: Sized + 'static {
199    /// Perform the transform with bounds checking.
200    fn try_transform_from(src: Src, ctx: &Ctx) -> Result<Self, OutOfGamut<Self>>;
201}
202
203/// Blanket: anything infallibly transformable also satisfies `TryTransform`.
204impl<Src, Ctx, Dst> TryTransform<Src, Ctx> for Dst
205where
206    Ctx: ?Sized,
207    Dst: Transform<Src, Ctx>,
208{
209    fn try_transform_from(src: Src, ctx: &Ctx) -> Result<Self, OutOfGamut<Self>> {
210        Ok(Self::transform_from(src, ctx))
211    }
212}
213
214/// Marks a `ColorSpace` as linear-light.
215///
216/// Values are proportional to physical light intensity. Required for
217/// premultiplied alpha compositing, addition of light sources, and
218/// texture filtering.
219pub trait LinearLight: ColorSpace {}
220
221/// Marks a `ColorSpace` as scene-referred.
222///
223/// Encodes physical scene radiance. Unbounded above. Requires tone
224/// mapping before display.
225pub trait SceneReferred: ColorSpace {}
226
227/// Marks a `ColorSpace` as display-referred.
228///
229/// Encodes a device's bounded output range, already tone mapped.
230pub trait DisplayReferred: ColorSpace {}
231
232/// Marks a `ColorSpace` where luminance has photometric units (cd/m2).
233pub trait Absolute: ColorSpace {}
234
235/// Distance between two colors in `ColorSpace` `Sp`.
236pub trait Delta<Sp: ColorSpace> {
237    /// The scalar type returned by this metric.
238    type Output;
239
240    /// Compute the distance between `a` and `b`.
241    fn delta<S>(a: Color<S, Sp>, b: Color<S, Sp>) -> Self::Output
242    where
243        S: Copy + 'static,
244        Sp: Asserts<S>;
245}
246
247/// Returned when a transform produces values outside the target's valid bounds.
248///
249/// `clamped` is always present; callers never lose data:
250///
251/// ```rust,ignore
252/// // Strict: propagate the error.
253/// let exact = color.try_transform_to(&()).unwrap();
254///
255/// // Permissive: always produce a value.
256/// let value = color.try_transform_to(&()).unwrap_or_else(|e| e.clamped);
257/// ```
258#[derive(Debug, Clone, Copy, PartialEq)]
259pub struct OutOfGamut<T> {
260    /// Closest in-bounds approximation with all clipped channels clamped
261    /// to the boundary of the target space's valid range.
262    pub clamped: T,
263}
264
265impl<T> core::fmt::Display for OutOfGamut<T> {
266    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
267        write!(f, "color out of gamut")
268    }
269}
270
271impl<T: core::fmt::Debug> core::error::Error for OutOfGamut<T> {}