colr 0.1.0

Type-safe, zero-cost color science library with compile-time color space transforms
Documentation
# colr

[![Latest Version]][crates.io]
[![docs]][docs.rs]
[![Minimum Supported Rust Version]][Rust 1.82]

A general purpose, type-safe color library.

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

```rust
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:

```rust
# 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:

```rust
# 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.

```toml
[dependencies]
colr = { version = "0.1", default-features = false, features = ["libm"] }
```

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

```toml
[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.

```rust
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

- Apache License, Version 2.0 ([LICENSE-APACHE]LICENSE-APACHE
  or <http://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT]LICENSE-MIT
  or <http://opensource.org/licenses/MIT>)

at your option.

[Latest Version]: https://img.shields.io/crates/v/colr.svg
[crates.io]: https://crates.io/crates/colr/
[docs]: https://docs.rs/colr/badge.svg
[docs.rs]: https://docs.rs/colr/
[Minimum Supported Rust Version]: https://img.shields.io/badge/Rust-1.85-blue?color=fc8d62&logo=rust
[Rust 1.82]: https://github.com/rust-lang/rust/blob/master/RELEASES.md