#color-space #rgb #xyz #color #graphics #rec709

srgb

sRGB primitives and constants — lightweight crate with functions and constants needed when manipulating sRGB colours

11 releases

0.3.3 Jan 31, 2024
0.3.2 Sep 20, 2022
0.3.1 Jun 19, 2022
0.3.0 Jan 3, 2022
0.1.4 Apr 30, 2021

#154 in Images


Used in 2 crates

LGPL-3.0-or-later

2MB
1.5K SLoC

Contains (ELF exe/lib, 3.5MB) gamma8, (ELF exe/lib, 3.5MB) tmp

sRGB primitives and constants

The crate provides primitives for manipulating colours in sRGB colour space.

Specifically, it provides functions for converting between sRGB, linear sRGB and XYZ colour spaces; exposes definition of the D65 reference white point together with XYZ conversion matrices; and finally provides functions for handling Rec.709 components encoding.

It offers low-level primitives needed to work with sRGB standard. Those primitives can be used by other libraries which need to convert between sRGB and other colour spaces or blend sRGB colours together.

Functions provided in the main module implement conversions between sRGB and XYZ colour spaces. Functions in gamma submodule provide functions for doing gamma compression and expansion; they operate on a single colour component. Lastly, [xyz] submodule provides functions for converting between linear sRGB and XYZ colour spaces as well as constants exposing the matrices used by those functions.

The crate includes highly-optimised 8-bit gamma functions both when converting from an 8-bit compressed value to a floating point linear value as well as conversion in the opposite direction. The latter is over two and a half times faster than naïve implementation of the gamma compression formula.

Usage

Using this package with Cargo projects requires adding a single dependency:

[dependencies]
srgb = "0.3"

With it in place, it’s now possible to write an application which converts an sRGB colour into other colour spaces:

#[derive(Debug)]
struct RGBColour(u8, u8, u8);

impl RGBColour {
    fn parse(value: &str) -> Option<Self> {
        value.strip_prefix('#')
            .and_then(|v| (v.len() == 6 && !v.starts_with('+')).then(|| v))
            .and_then(|v| u32::from_str_radix(v, 16).ok())
            .map(|v| Self((v >> 16) as u8, (v >> 8) as u8, v as u8))
    }

    fn normalise(&self) -> (f32, f32, f32) {
        let [r, g, b] = srgb::normalised_from_u8([self.0, self.1, self.2]);
        (r, g, b)
        // Alternatively divide each component by 255 manually
    }

    fn expand_gamma(&self) -> (f32, f32, f32) {
        (
            srgb::gamma::expand_u8(self.0),
            srgb::gamma::expand_u8(self.1),
            srgb::gamma::expand_u8(self.2),
        )
        // Alternatively a convenience function is provided as well:
        // let [r, g, b] = srgb::gamma::linear_from_u8([self.0, self.1, self.2]);
        // (r, g, b)
    }

    fn to_xyz(&self) -> (f32, f32, f32) {
        let linear = srgb::gamma::linear_from_u8([self.0, self.1, self.2]);
        let [r, g, b] = srgb::xyz::xyz_from_linear(linear);
        (r, g, b)
        // Alternatively, if a custom matrix multiplication is available:
        // let [r, g, b] = matrix_product(
        //     srgb::xyz::XYZ_FROM_SRGB_MATRIX, linear);
    }
}

fn main() {
    for arg in std::env::args().into_iter().skip(1) {
        if let Some(rgb) = RGBColour::parse(&arg[..]) {
            println!("sRGB:       {:?}", rgb);
            println!("Normalised: {:?}", rgb.normalise());
            println!("Linear:     {:?}", rgb.expand_gamma());
            println!("XYZ:        {:?}", rgb.to_xyz());
        } else {
            eprintln!("expected ‘#rrggbb’ but got {}", arg);
        }
    }
}

rgb crate support

This crate doesn’t have an explicit rgb crate support. However, since all functions taking an (s)RGB colour as argument accept impl Into<[f32; 3]> or impl Into<[u8; 3]> it is possible to pass RGB structure to them. Similarly, such functions return [f32; 3] or [u8; 3]which can be converted into an RGB structure.

extern crate rgb;
use rgb::ComponentMap;

fn parse(value: &str) -> Option<rgb::RGB8> {
    value.strip_prefix('#')
        .and_then(|v| (v.len() == 6 && !v.starts_with('+')).then(|| v))
        .and_then(|v| u32::from_str_radix(v, 16).ok())
        .map(|v| (rgb::RGB::new((v >> 16) as u8, (v >> 8) as u8, v as u8)))
}

fn normalise(colour: rgb::RGB8) -> rgb::RGB<f32> {
    srgb::normalised_from_u8(colour).into()
}

fn expand_gamma(colour: rgb::RGB8) -> rgb::RGB<f32> {
    colour.map(srgb::gamma::expand_u8)
}

fn to_xyz(colour: rgb::RGB8) -> (f32, f32, f32) {
    let linear = srgb::gamma::linear_from_u8(colour);
    let [r, g, b] = srgb::xyz::xyz_from_linear(linear);
    (r, g, b)
}

fn main() {
    for arg in std::env::args().into_iter().skip(1) {
        if let Some(colour) = parse(&arg[..]) {
            println!("sRGB:       {:?}", colour);
            println!("Normalised: {:?}", normalise(colour));
            println!("Linear:     {:?}", expand_gamma(colour));
            println!("XYZ:        {:?}", to_xyz(colour));
        } else {
            eprintln!("expected ‘#rrggbb’ but got {}", arg);
        }
    }
}

No runtime deps