colr 0.3.0

A general purpose, extensible color type unifying color models and their operations at the type level.
docs.rs failed to build colr-0.3.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: colr-0.3.1

colr

Latest Version docs Minimum Supported Rust Version

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 colr::{Color, LinearSrgb, Oklab, Oklch, Srgb, Xyz};

// Construct an sRGB color. Defaults to RGBA layout.
let srgb: Color<[f32; 4], Srgb> = Color::new([0.8, 0.4, 0.2, 1.0]);

// Decode to linear light before converting. The transfer function must be
// removed before the RGB-to-XYZ matrix is applied.
let linear: Color<[f32; 4], LinearSrgb> = 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: Color<[f32; 4], Xyz> = linear.into();
let lab: Color<[f32; 4], Oklab> = xyz.into();

// Pull out chroma and hue, reduce chroma, put it back.
let lch: Color<[f32; 4], Oklch> = lab.into();
let [l, c, h, a] = lch.inner();
let muted: Color<[f32; 4], Oklch> = Color::new([l, c * 0.5, h, a]);

// 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: Color<[f32; 4], Oklab> = muted.into();
let xyz_out: Color<[f32; 4], Xyz> = oklab_out.into();
let linear_out: Color<[f32; 4], LinearSrgb> = xyz_out.into();
let final_color: Color<[f32; 4], Srgb> = 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 colr::{Color, LinearSrgb, Xyz};
# let src: Color<[f32; 4], Xyz> = Color::new([2.0, 1.0, 1.0, 1.0]);
let linear: Color<[f32; 4], LinearSrgb> = src.into();
let display_ready: Color<[f32; 4], LinearSrgb> = linear.clamp(0.0, 1.0);

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:

pub trait FromDevice<Src>: Sized {
    type Profile;
    fn from_device(src: Src, profile: &Self::Profile) -> Self;
}

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 colr::{Color, Srgb};
# use colr::layout::{Bgra, Rgba};
# let rgba: Color<[f32; 4], Srgb<Rgba>> = Color::new([1.0, 0.0, 0.0, 1.0]);
let bgra: Color<[f32; 4], Srgb<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 colr::{Color, Xyz};
# use colr::chromatic_adaptation::Bradford;
# use colr::illuminant::{D50, D65};
# let xyz_d65: Color<[f32; 3], Xyz<D65>> = Color::new([0.95, 1.0, 1.09]);
let xyz_d50: Color<[f32; 3], Xyz<D50>> = xyz_d65.adapt::<D50, Bradford>();

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 colr::{Color, Oklab};
# let a: Color<[f32; 3], Oklab> = Color::new([0.5, 0.1, 0.0]);
# let b: Color<[f32; 3], Oklab> = Color::new([0.5, 0.0, 0.1]);
let delta: f32 = a.delta_e_ok(b);

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 colr::{Color, LinearSrgb};
# use colr::tonemap::KhronosPbrNeutral;
# let scene_linear: Color<[f32; 3], LinearSrgb> = Color::new([2.0, 1.0, 1.0]);
let display_linear = scene_linear.tonemap::<KhronosPbrNeutral>();

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 colr::{Color, Srgb};
# let color: Color<[f32; 4], Srgb> = Color::new([0.5, 0.5, 0.5, 1.0]);
// Generate two independent uniform random floats in [0, 1)
# let u1 = 0.2; let u2 = 0.7;
let dither = (u1 - u2) / 255.0;
let pixel: Color<[u8; 4], Srgb> = color.to_u8_dithered(dither);

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 colr::{Color, LinearSrgb};
# let src: Color<[f32; 4], LinearSrgb> = Color::new([1.0, 0.0, 0.0, 0.5]);
# let dst: Color<[f32; 4], LinearSrgb> = Color::new([0.0, 0.0, 1.0, 1.0]);
let result = src.premultiply().over(dst.premultiply()).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.

[dependencies]
colr = { version = "0.2", default-features = false, features = ["libm"] }

To support both std and no_std builds in a library crate:

[features]
default = ["std"]
std     = ["colr/std"]
libm    = ["colr/libm"]

[dependencies]
colr = { version = "0.2", default-features = 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 colr::primaries::{Primaries, derive_rgb_to_xyz};
use colr::illuminant::D65;
use colr::math::Mat3;

pub struct CustomPrimaries;

const CUSTOM_TO_XYZ: Mat3 = derive_rgb_to_xyz(
    [0.680, 0.320],  // red
    [0.265, 0.690],  // green
    [0.150, 0.060],  // blue
    D65::WHITE_POINT_XYZ,
);

impl Primaries for CustomPrimaries {
    type Native = D65;
    const R: [f32; 2] = [0.680, 0.320];
    const G: [f32; 2] = [0.265, 0.690];
    const B: [f32; 2] = [0.150, 0.060];
    const TO_XYZ_NATIVE:  Mat3 = CUSTOM_TO_XYZ;
    const FROM_XYZ_NATIVE: Mat3 = Mat3::invert(&CUSTOM_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

at your option.