coloremetry 0.2.0

small color library written in Rust
Documentation
/*
 *    Copyright 2025 Jared Davis
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
use crate::cie::lab::Lab;
use crate::cie::xyz::XYZ;
use serde::{Deserialize, Serialize};
use std::num::ParseIntError;
use crate::cie::illumination::Illumination;

#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct RGB {
    pub r: u8,
    pub g: u8,
    pub b: u8,
}

impl RGB {
    pub fn new(r: u8, g: u8, b: u8) -> Self {
        Self { r, g, b }
    }

    fn xyz(self, illumination: Illumination) -> XYZ {
        let r = inverse_gamma(linearize(self.r));
        let g = inverse_gamma(linearize(self.g));
        let b = inverse_gamma(linearize(self.b));

        let x = illumination.x[0] * r + illumination.x[1] * g + illumination.x[2] * b;
        let y = illumination.y[0] * r + illumination.y[1] * g + illumination.y[2] * b;
        let z = illumination.z[0] * r + illumination.z[1] * g + illumination.z[2] * b;

        XYZ { x, y, z }
    }

    pub fn lab(self, illumination: Illumination) -> Lab {
        self.xyz(illumination).lab(illumination)
    }
}

impl TryFrom<String> for RGB {
    type Error = ParseIntError;
    fn try_from(value: String) -> Result<Self, Self::Error> {
        let hex = value.trim_start_matches("#");
        let v = u32::from_str_radix(hex, 16)?;
        Ok(RGB::from(v))
    }
}

impl From<u32> for RGB {
    fn from(v: u32) -> Self {
        let r = ((v >> 16) & 0xFF) as u8;
        let g = ((v >> 8) & 0xFF) as u8;
        let b = (v & 0xFF) as u8;

        Self::new(r, g, b)
    }
}

impl From<RGB> for u32 {
    fn from(RGB { r, g, b }: RGB) -> Self {
        ((r as u32) << 16) | ((g as u32) << 8) | (b as u32)
    }
}

impl From<RGB> for String {
    fn from(rgb: RGB) -> Self {
        format!("#{:06X}", u32::from(rgb))
    }
}

fn linearize(v: u8) -> f32 {
    f32::min(v as f32, 255.0) / 255.0
}

fn inverse_gamma(v: f32) -> f32 {
    if v < 0.04045 {
        v * 0.0773993808
    } else {
        f32::powf((v + 0.055) * 0.9478672986, 2.4)
    }
}

#[cfg(test)]
mod tests {
    use crate::cie::rgb::RGB;
    use crate::cie::illumination::D65;

    #[test]
    fn it_converts_hex_to_rgb() {
        assert_eq!(
            RGB::try_from("#FFFFFF".to_string()),
            Ok(RGB::new(0xFF, 0xFF, 0xFF))
        );
        assert_eq!(
            RGB::try_from("#000000".to_string()),
            Ok(RGB::new(0x00, 0x00, 0x00))
        );
        assert_eq!(
            RGB::try_from("#FF0000".to_string()),
            Ok(RGB::new(0xFF, 0x00, 0x00))
        );
        assert_eq!(
            RGB::try_from("#808080".to_string()),
            Ok(RGB::new(0x80, 0x80, 0x80))
        );
        assert_eq!(
            RGB::try_from("#7F11B2".to_string()),
            Ok(RGB::new(127, 17, 178))
        );
    }

    #[test]
    fn it_converts_rgb_to_hex() {
        assert_eq!(String::from(RGB::new(0xFF, 0xFF, 0xFF)), "#FFFFFF");
        assert_eq!(String::from(RGB::new(0x00, 0x00, 0x00)), "#000000");
        assert_eq!(String::from(RGB::new(0xFF, 0x80, 0x80)), "#FF8080");
    }

    macro_rules! assert_lab {
        ($example:expr, $l:expr, $a:expr, $b:expr) => {
            assert!($example.l - $l < 1e-2);
            assert!($example.a - $a < 1e-2);
            assert!($example.b - $b < 1e-2);
        };
    }
    #[test]
    fn it_converts_rgb_to_lab() {
        assert_lab!(RGB::new(0x00, 0x00, 0x00).lab(D65), 0.0, 0.0, 0.0);
        assert_lab!(RGB::new(0xFF, 0x00, 0x00).lab(D65), 53.24, 80.09, 67.20);
        assert_lab!(RGB::new(0xFF, 0xFF, 0x00).lab(D65), 97.14, -21.55, 94.48);
        assert_lab!(RGB::new(0x00, 0xFF, 0x00).lab(D65), 87.74, -86.18, 83.18);
        assert_lab!(RGB::new(0, 255, 255).lab(D65), 91.11, -48.09, -14.130);
        assert_lab!(RGB::new(0, 0, 255).lab(D65), 32.30, 79.19, -107.860);
        assert_lab!(RGB::new(255, 0, 255).lab(D65), 60.32, 98.24, -60.830);
        assert_lab!(RGB::new(255, 255, 255).lab(D65), 100.0, 0.00, 0.000);
        assert_lab!(RGB::new(127, 127, 127).lab(D65), 53.39, 0.00, 0.000);
        assert_lab!(RGB::new(191, 0, 0).lab(D65), 39.77, 64.51, 54.130);
        assert_lab!(RGB::new(127, 0, 0).lab(D65), 25.42, 47.91, 37.910);
        assert_lab!(RGB::new(63, 0, 0).lab(D65), 9.66, 29.68, 15.240);
        assert_lab!(RGB::new(255, 127, 127).lab(D65), 68.00, 48.59, 22.970);
    }
}