colr 0.1.1

A general purpose, extensible color type unifying storage, channel layouts, and color spaces at the type level.
Documentation

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 themselves are generic over their storage representation, which itself is generic over its layout.

Usage

use colr::{Color, Srgb, Rgb, Oklab, Oklch, XyzD65};

// Construct an sRGB color. Layout is part of the type.
let srgb: Color<[f32; 3], Srgb<Rgb>> = Color::new_unchecked([0.8, 0.4, 0.2]);

// Convert to Oklab for perceptual work. The path goes through XYZ
// automatically when you chain transforms. All matrices are compile-time.
let lab: Color<[f32; 3], Oklab> = srgb
    .transform::<Color<[f32; 3], XyzD65>>()
    .transform();

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

// Converting from a large space back to sRGB can produce out-of-gamut
// values. try_transform gives you the error and the clamped result.
let result = muted
    .transform::<Color<[f32; 3], Oklab>>()
    .transform::<Color<[f32; 3], XyzD65>>()
    .try_transform::<Color<[f32; 3], Srgb<Rgb>>>();

let final_color = result.unwrap_or_else(|e| e.clamped);

Color 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. Oklab and Oklch have D65 baked into the specification. The aliases LabD65 and LChD65 cover the common case.

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 spaces

Alias Illuminant Typical use
XyzD65 D65 sRGB, P3, Rec. 2020 connection space
XyzD50 D50 ICC profile connection space
XyzAces ACES white ACES pipeline connection space

Conversions between different XYZ illuminants apply Bradford chromatic adaptation, computed at compile time.

Planned spaces

CMYK support is planned. The intent is to support ICC-described CMYK profiles as a runtime context parameter on the transform, consistent with how other context-parameterized transforms work in this library. There is no timeline for this yet.

If a space from an open standard is not listed above, please open an issue.

Transforms

Use transform for conversions that are always in range, and try_transform for conversions into a bounded space where the source may not fit.

Method When to use
transform Infallible: RGB to XYZ, layout reorders, Lab
try_transform Fallible: XYZ or large gamut to bounded RGB
transform_via Two steps through an intermediate, both infallible
try_transform_via Two steps; returns error if either leg clips

try_transform returns Result<Dst, OutOfGamut<Dst>>. The OutOfGamut value always contains the clamped approximation, so a usable result is always present regardless of whether you propagate the error:

# use colr::{Color, Srgb, Rgb, XyzD65};
# let src: Color<[f32; 3], XyzD65> = Color::new_unchecked([2.0, 1.0, 1.0]);
let value = src.try_transform::<Color<[f32; 3], Srgb<Rgb>>>().unwrap_or_else(|e| e.clamped);

Transform context

Most transforms are fully static and require no runtime data. The Ctx parameter on Transform exists for cases where runtime data is needed:

Transform Ctx
sRGB to XYZ ()
XYZ to CAM16-UCS ViewingConditions
Relative to absolute XYZ f32 (cd/m2)
Device CMYK to XYZ (planned) IccProfile

Implement Transform<Src, YourCtx> for custom transforms.

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, Bgra, Rgba};
# let rgba: Color<[f32; 4], Srgb<Rgba>> = Color::new_unchecked([1.0, 0.0, 0.0, 1.0]);
let bgra: Color<[f32; 4], Srgb<Bgra>> = rgba.transform();

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

Custom adaptation matrices can be computed at compile time with adapt::<A>.

no_std support

no_std support is available with the libm feature. Either std or libm must be enabled; the crate will not compile without at least one.

[dependencies]
colr = { version = "0.1", 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.1", default-features = false }

Optional features

std is the default feature and enables standard library math functions. The remaining features are opt-in.

Feature Description
glam Adds VecN types as valid color impls
libm Uses libm math functions for no_std targets

Adding a custom primary set

Implement Primaries for your type and register it with the provided macros. 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::{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);
}

Then call impl_native! and any needed impl_adapted! to register the XYZ connection hubs for your target illuminant. See src/primaries.rs for examples.

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, in any build profile.

Color<S, Sp> is #[repr(transparent)] over its storage type. The space is carried only as PhantomData. There is no runtime memory overhead from the type-level bookkeeping.

Minimum Supported Rust Version (MSRV)

The minimum supported version of Rust for this crate 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 the crate doesn't fit your needs, is missing any obvious 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.