colr
A general purpose, extensible color type unifying storage, channel layouts, and color spaces at the type level.
Color spaces, transfer functions, chromatic adaptation, and channel layouts are encoded in the type system. Mixing an sRGB color with a Display P3 color is a compile error unless you convert explicitly. All colorimetric matrices are derived at compile time. Colors are generic over their storage representation, which is itself generic over its channel layout.
Usage
use ;
// Construct an sRGB color. Defaults to RGBA layout.
let srgb: = new;
// Decode to linear light before converting. The transfer function must be
// removed before the RGB-to-XYZ matrix is applied.
let linear: = srgb.decode;
// Convert to Oklab for perceptual work. Each step is an explicit From.
// All matrices are compile-time constants. Xyz defaults to D65.
// Alpha is carried through each conversion unchanged.
let xyz: = linear.into;
let lab: = xyz.into;
// Pull out chroma and hue, reduce chroma, put it back.
let lch: = lab.into;
let = lch.inner;
let muted: = new;
// Convert back. XYZ-to-RGB may produce out-of-range values for wide-gamut
// colors; clamp if targeting a display, or pass through for tone mapping.
let oklab_out: = muted.into;
let xyz_out: = oklab_out.into;
let linear_out: = xyz_out.into;
let final_color: = linear_out.encode;
Color models and spaces
Perceptual spaces
| Type | Description |
|---|---|
Lab |
CIE 1976 L*a*b* parameterized by reference white W |
LCh |
Polar form of L*a*b* |
Oklab |
Oklab (Ottosson 2020); improved hue linearity over Lab |
Oklch |
Polar form of Oklab |
Lab and LCh are parameterized by illuminant: Lab<D65>, Lab<D50>, and so on, with D65 as the default. Oklab and Oklch have D65 baked into the specification.
All four types accept a const OFFSET parameter that controls where the color channels begin within a four-channel array. The default OFFSET = 0 places color channels at indices 0, 1, 2 and alpha at 3. OFFSET = 1 places alpha at 0 and color channels at 1, 2, 3. This allows in-place processing of ARGB-layout buffers without swizzling: reinterpret the buffer as Oklab<1>, operate, reinterpret back.
RGB spaces
| Alias | Primaries | Transfer function | White |
|---|---|---|---|
Srgb |
sRGB/Rec709 | sRGB | D65 |
LinearSrgb |
sRGB/Rec709 | Linear | D65 |
Rec709 |
sRGB/Rec709 | Rec. 709 | D65 |
DisplayP3 |
P3 | sRGB | D65 |
LinearP3 |
P3 | Linear | D65 |
Hdr10 |
Rec. 2020 | PQ (ST 2084) | D65 |
Hlg |
Rec. 2020 | HLG (BT.2100) | D65 |
LinearRec2020 |
Rec. 2020 | Linear | D65 |
AcesCg |
AP1 | Linear | ACES |
Aces2065 |
AP0 | Linear | ACES |
AcesCc |
AP1 | ACEScc log | ACES |
AcesCct |
AP1 | ACEScct log+toe | ACES |
ProPhoto |
ProPhoto | gamma 1.8 | D50 |
LinearProPhoto |
ProPhoto | Linear | D50 |
DciP3 |
DCI-P3 | gamma 2.6 | DCI |
Each alias defaults to RGBA layout. Alternate layouts are selected with the type parameter: Srgb<Rgb>, Srgb<Bgra>, and so on.
XYZ
Xyz<W> is the CIE XYZ tristimulus space parameterized by illuminant W, defaulting to Xyz<D65>. It serves as the connection space for all RGB and perceptual conversions.
Conversions between Xyz<W1> and Xyz<W2> apply Bradford chromatic adaptation by default, computed at compile time. The adaptation method can be overridden with .adapt::<W2, CAT>().
Luma
Luma<P, TF> is a single-channel luminance model derived from primary set P with transfer function TF. LumaAlpha<P, TF, A> adds an alpha channel with alpha state A (defaults to Straight). The luma weights are the Y row of the primaries' RGB-to-XYZ matrix, so they vary by primary set.
YCbCr
YCbCr<P, TF, L> is Y'CbCr derived from RGB primaries P and transfer function TF. The conversion matrix is derived from the primaries' luma weights. Used in JPEG, H.264, H.265, and broadcast video. Layout defaults to Ycbcr.
Spectral
Spectral<N, G, K> carries N spectral samples over wavelength grid G with physical kind K. Three kinds are defined:
| Kind marker | Physical quantity | Conversion to XYZ |
|---|---|---|
IsRadiance |
Spectral radiance | SPD dot CMF |
IsReflectance |
Spectral reflectance [0,1] | (illuminant * reflectance) dot CMF |
IsTransmittance |
Spectral transmittance [0,1] | same as reflectance |
IsBispectral is defined as a kind marker to close the taxonomy but does not implement BackingStore; bispectral (fluorescent) storage is reserved for a future explicit design.
Standard grids include Grid380_780_10nm (41 bands), Grid400_700_10nm (31 band ICC standard), and Grid380_780_5nm (81 bands), all evaluated against the CIE 1931 observer.
Blanket alias traits Radiance, Reflectance, Transmittance, and Bispectral are provided for use in bounds.
Device-dependent
| Type | Description |
|---|---|
Cmyk |
Device CMYK. Profile required for XYZ conversion |
DeviceRgb |
Device RGB. Profile required for XYZ conversion |
Conversions for these types use FromDevice rather than From. See the Device-dependent colors section.
If a color space or model from an open standard is not listed above, please open an issue.
Conversions
All conversions use From/Into. XYZ-to-RGB is infallible and may return out-of-range channel values for wide-gamut colors. Clamp with .clamp(0.0, 1.0) for display output, or pass the values directly to a tone mapper.
# use ;
# let src: = new;
let linear: = src.into;
let display_ready: = linear.clamp;
Encode and decode transfer functions with .encode() and .decode(). Adapt a color between XYZ white points with .adapt::<W2, CAT>() on a Color<_, Xyz<W1>>.
Device-dependent colors
Cmyk and DeviceRgb<L> are device-dependent color models whose relationship to XYZ is defined by a runtime ICC profile. Because no compile-time matrix describes them, they do not implement From. Conversions use FromDevice from colr::device:
The Profile associated type is set by each impl. A profile type defined externally can add FromDevice impls for Cmyk or DeviceRgb without changes to this crate.
Channel layouts
Layout is a type parameter, not a runtime tag. The compiler distinguishes RGBA from BGRA and will not accept one where the other is expected.
| Type | Order |
|---|---|
Rgba |
R G B A |
Bgra |
B G R A |
Argb |
A R G B |
Abgr |
A B G R |
Rgb |
R G B |
Bgr |
B G R |
Layout reorders are infallible and compile to a shuffle with no arithmetic:
# use ;
# use ;
# let rgba: = new;
let bgra: = rgba.swizzle;
Chromatic adaptation
Four methods are provided. Bradford is used automatically by all built-in conversions and is the correct default for standard RGB work.
| Type | Standard |
|---|---|
Bradford |
ICC, ACES, CSS Color Level 4 |
Cat02 |
CIECAM02, ICC v4 appearance models |
Cat16 |
CAM16 |
VonKries |
Legacy and reference |
To adapt a color explicitly:
# use ;
# use Bradford;
# use ;
# let xyz_d65: = new;
let xyz_d50: = xyz_d65.;
Custom adaptation matrices can be computed at compile time with operations::adapt::adapt.
Color difference
| Method | Space | Formula |
|---|---|---|
delta_e76 |
Lab |
CIE 1976 (Euclidean) |
delta_e2000 |
Lab |
CIEDE2000 (CIE 142-2001) |
delta_e_ok |
Oklab |
Euclidean in Oklab |
# use ;
# let a: = new;
# let b: = new;
let delta: f32 = a.delta_e_ok;
Tone mapping
Tone mapping compresses unbounded scene-linear light [0, inf) down to bounded display-linear light [0, 1].
| Type | Description |
|---|---|
AcesNarkowicz |
ACES filmic curve approximation (per-channel) |
KhronosPbrNeutral |
glTF 3D Commerce standard, hue-preserving (RGB-coupled) |
Reinhard |
Classic x / (x + 1) curve (per-channel) |
# use ;
# use KhronosPbrNeutral;
# let scene_linear: = new;
let display_linear = scene_linear.;
Quantization and dithering
Converting continuous f32 signals to 8-bit u8 integers without dithering causes visible banding. colr provides a way to convert with a dithering offset as well as straight. The to_u8 method performs naive quantization while to_u8_dithered method dithers the color channels leaving the alpha channel untouched.
# use ;
# let color: = new;
// Generate two independent uniform random floats in [0, 1)
# let u1 = 0.2; let u2 = 0.7;
let dither = / 255.0;
let pixel: = color.to_u8_dithered;
Alpha compositing
Blend operations are defined on Color<[f32; 4], Rgb<P, Linear, L>> and require premultiplied alpha. Compositing in non-linear space is incorrect.
# use ;
# let src: = new;
# let dst: = new;
let result = src.premultiply.over.unpremultiply;
| Method | Description |
|---|---|
premultiply |
Multiply color channels by alpha |
unpremultiply |
Divide color channels by alpha |
over |
Porter-Duff over (both premultiplied) |
no_std support
no_std is available with the libm feature. Either std or libm must be enabled.
[]
= { = "0.2", = false, = ["libm"] }
To support both std and no_std builds in a library crate:
[]
= ["std"]
= ["colr/std"]
= ["colr/libm"]
[]
= { = "0.2", = false }
Optional features
std is enabled by default and selects standard library math functions. The remaining features are opt-in.
| Feature | Description |
|---|---|
glam |
Adds VecN types as valid color storage |
libm |
Uses libm math functions for no_std targets |
Adding a custom primary set
Implement Primaries for your type. All matrices are derived at compile time from the chromaticity coordinates and white point you supply.
use ;
use ;
use Mat3;
;
const CUSTOM_TO_XYZ: Mat3 = derive_rgb_to_xyz;
colr-types
The colr-types crate provides the color model ZSTs and marker traits without Color<S, M> or its operations. Depend on it directly when you need to parameterize on color model at the type level without pulling in the full operation set, for example in a GPU backend that manages its own storage.
Design notes
All colorimetric matrices are derived in const fn at compile time, including RGB-to-XYZ, chromatic adaptation, and composed primaries-to-primaries transforms. There is no matrix inversion or multiplication at runtime.
Color<S, M> is #[repr(transparent)] over its storage type. The color model is carried only as PhantomData. There is no runtime memory overhead from the type-level bookkeeping.
Minimum Supported Rust Version
The minimum supported version of Rust is 1.85. Edition 2024 requires Rust 1.85. Inline const {} blocks, used to evaluate transform matrices at compile time in generic contexts, stabilized in Rust 1.82.
Contributing
This crate is early in development. Consider raising an issue if it doesn't fit your needs, is missing functionality, or is unergonomic in any way.
Contributions are welcome for any color space or transfer function defined in an open standard. If a standard space is missing, please open an issue before implementing it so the approach can be agreed on first. Spaces from proprietary or closed formats belong in a separate crate.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work, as defined in the Apache-2.0 license, shall be dual licensed as below, without any additional terms or conditions.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE.APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE.MIT or http://opensource.org/licenses/MIT)
at your option.