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> {}