gem 0.1.1

Color representations and conversions
Documentation
//! Generic conversions between [`Srgb`] and *any* pixel format that
//! implements [`RgbColor`] + [`RgbChannelScale`].
//!
//! This replaces what would otherwise be a hand-written `From` impl per pixel
//! format (and is exactly the kind of matrix that went stale in practice:
//! earlier versions of this crate had `Srgb: From<Abgr8888>` but no
//! `Abgr8888: From<Srgb>`, and no conversions at all for half the built-in
//! formats). [`RgbChannelScale`] is implemented once per format (see
//! `src/rgb/formats/*.rs` and the `define_packed_*!` macros), and every
//! format gets both conversion directions for free.

use crate::{
    rgb::{HasBlue, HasGreen, HasRed},
    space::{RgbChannelScale, Srgb, channel::Channel},
};

// `C -> Srgb` can be a single blanket impl: `Srgb` is a local type, so this
// direction isn't restricted by coherence regardless of what `C` is.
impl<C> From<C> for Srgb
where
    C: RgbChannelScale,
    <C as HasRed>::Component: Channel,
    <C as HasGreen>::Component: Channel,
    <C as HasBlue>::Component: Channel,
{
    fn from(c: C) -> Self {
        Self {
            r: c.red().to_f32() / C::RED_MAX,
            g: c.green().to_f32() / C::GREEN_MAX,
            b: c.blue().to_f32() / C::BLUE_MAX,
        }
    }
}

// `Srgb -> C` cannot be a single blanket impl over a bare generic `C`: Rust's
// coherence rules forbid `impl<C> From<LocalType> for C`. Every *local*
// struct constructor (Rgb<T>, Bgr<T>, the alpha wrappers, and the packed
// formats generated by `define_packed_*!`) gets its own one-line `From<Srgb>`
// next to its definition, forwarding to `FromSrgb::from_srgb` — the shared
// scaling logic still lives in exactly one place. Third-party `RgbColor`
// implementors call `FromSrgb::from_srgb` directly (see that trait's docs).

#[cfg(test)]
mod tests {
    use crate::{
        rgb::{Abgr8888, Argb1555, Argb4444, Argb8888, Bgr888, Rgb565, Rgb888, RgbF32},
        space::Srgb,
    };

    #[test]
    fn rgb888_roundtrip() {
        let pixel = Rgb888::from_rgb(200, 50, 100);
        let srgb = Srgb::from(pixel);
        let back = Rgb888::from(srgb);
        assert_eq!(back, pixel);
    }

    #[test]
    fn bgr888_roundtrip() {
        let pixel = Bgr888::from_bgr(200, 50, 100);
        let back = Bgr888::from(Srgb::from(pixel));
        assert_eq!(back, pixel);
    }

    #[test]
    fn abgr8888_roundtrip_previously_missing_direction() {
        // Before this change, `Abgr8888: From<Srgb>` did not exist at all.
        let srgb = Srgb::new(1.0, 0.2, 0.4);
        let pixel: Abgr8888 = srgb.into();
        let back: Srgb = pixel.into();
        assert!((back.r - srgb.r).abs() < 0.01);
        assert!((back.g - srgb.g).abs() < 0.01);
        assert!((back.b - srgb.b).abs() < 0.01);
    }

    #[test]
    fn argb8888_roundtrip_previously_missing_direction() {
        let srgb = Srgb::new(0.1, 0.9, 0.3);
        let pixel: Argb8888 = srgb.into();
        let back: Srgb = pixel.into();
        assert!((back.r - srgb.r).abs() < 0.01);
        assert!((back.g - srgb.g).abs() < 0.01);
        assert!((back.b - srgb.b).abs() < 0.01);
    }

    #[test]
    fn rgb565_conversion_scales_by_bit_depth_not_255() {
        // A fully-saturated 5-bit red channel (31) must normalize to 1.0, not
        // 31 / 255. This is the bug a naive "always divide by 255" blanket
        // impl would reintroduce.
        let pixel = Rgb565::from_rgb(31, 63, 31);
        let srgb = Srgb::from(pixel);
        assert!((srgb.r - 1.0).abs() < 1e-5, "r={}", srgb.r);
        assert!((srgb.g - 1.0).abs() < 1e-5, "g={}", srgb.g);
        assert!((srgb.b - 1.0).abs() < 1e-5, "b={}", srgb.b);
    }

    #[test]
    fn rgb565_roundtrip_previously_missing_entirely() {
        let pixel = Rgb565::from_rgb(31, 40, 10);
        let back = Rgb565::from(Srgb::from(pixel));
        assert_eq!(back, pixel);
    }

    #[test]
    fn argb1555_rgb_roundtrip_previously_missing_entirely() {
        // `Srgb` carries no alpha channel, so round-tripping through it resets
        // alpha to the type's `Default` (0, fully transparent) — only red,
        // green, and blue survive the trip. This matches `RgbColor::from_rgb`'s
        // existing "other components use `Default`" contract.
        use crate::{
            alpha::HasAlpha,
            rgb::{HasBlue, HasGreen, HasRed},
        };
        let pixel = Argb1555::from_rgb(31, 31, 31);
        let back = Argb1555::from(Srgb::from(pixel));
        assert_eq!((back.red(), back.green(), back.blue()), (31, 31, 31));
        assert_eq!(back.alpha(), 0);
    }

    #[test]
    fn argb4444_rgb_roundtrip_previously_missing_entirely() {
        use crate::{
            alpha::HasAlpha,
            rgb::{HasBlue, HasGreen, HasRed},
        };
        let pixel = Argb4444::from_argb(15, 15, 0, 15);
        let back = Argb4444::from(Srgb::from(pixel));
        assert_eq!((back.red(), back.green(), back.blue()), (15, 0, 15));
        assert_eq!(back.alpha(), 0);
    }

    #[test]
    fn rgb_f32_roundtrip_previously_missing_entirely() {
        let pixel = RgbF32::from_rgb(0.25, 0.5, 0.75);
        let srgb = Srgb::from(pixel);
        assert!((srgb.r - 0.25).abs() < 1e-6);
        assert!((srgb.g - 0.5).abs() < 1e-6);
        assert!((srgb.b - 0.75).abs() < 1e-6);
    }

    #[test]
    fn custom_rgb_u16_gets_conversion_for_free() {
        use crate::rgb::Rgb;
        let pixel: Rgb<u16> = Rgb::from_rgb(65_535, 0, 32_768);
        let srgb = Srgb::from(pixel);
        assert!((srgb.r - 1.0).abs() < 1e-4);
        assert!(srgb.g.abs() < 1e-4);
        assert!((srgb.b - 0.5).abs() < 1e-3);
    }
}