colr 0.3.1

A general purpose, extensible color type unifying color models and their operations at the type level.
Documentation
//! Quantization operations for converting continuous signals to discrete integers.

use crate::model::Rgb;
use crate::primaries::Primaries;
use crate::transfer::TransferFunction;
use crate::{BackingStore, ChannelMap, Color};

#[inline(always)]
fn quantize_f32_to_u8(x: f32) -> u8 {
    (x * 255.0).round().clamp(0.0, 255.0) as u8
}

impl<M> Color<[f32; 3], M>
where
    M: BackingStore<[f32; 3]> + BackingStore<[u8; 3]>,
{
    /// Quantizes the channels from `f32` [0.0, 1.0] to `u8` [0, 255].
    ///
    /// Uses standard scaling, half-adjust rounding, and clamping.
    #[inline(always)]
    pub fn to_u8(self) -> Color<[u8; 3], M> {
        let [c0, c1, c2] = self.inner();
        Color::new([
            quantize_f32_to_u8(c0),
            quantize_f32_to_u8(c1),
            quantize_f32_to_u8(c2),
        ])
    }
}

impl<M> Color<[f32; 4], M>
where
    M: BackingStore<[f32; 4]> + BackingStore<[u8; 4]>,
{
    /// Quantizes the channels from `f32` [0.0, 1.0] to `u8` [0, 255].
    ///
    /// Uses standard scaling, half-adjust rounding, and clamping.
    #[inline(always)]
    pub fn to_u8(self) -> Color<[u8; 4], M> {
        let [c0, c1, c2, c3] = self.inner();
        Color::new([
            quantize_f32_to_u8(c0),
            quantize_f32_to_u8(c1),
            quantize_f32_to_u8(c2),
            quantize_f32_to_u8(c3),
        ])
    }
}

impl<P, TF, L> Color<[f32; 3], Rgb<P, TF, L>>
where
    P: Primaries,
    TF: TransferFunction,
    L: BackingStore<[f32; 3]> + BackingStore<[u8; 3]> + ChannelMap<3>,
{
    /// Quantizes the color to `u8` with an applied dither to prevent banding.
    ///
    /// The `dither` value is added to the unscaled `f32` color channels before
    /// scaling and rounding. For mathematically perfect 8-bit quantization
    /// (Triangular PDF), generate the dither value by subtracting two independent
    /// uniform random variables in the range `[0.0, 1.0/255.0)`.
    #[inline(always)]
    pub fn to_u8_dithered(self, dither: f32) -> Color<[u8; 3], Rgb<P, TF, L>> {
        let mut out = self.inner();
        let [ri, gi, bi] = L::INDICES;

        out[ri] += dither;
        out[gi] += dither;
        out[bi] += dither;

        Color::new([
            quantize_f32_to_u8(out[0]),
            quantize_f32_to_u8(out[1]),
            quantize_f32_to_u8(out[2]),
        ])
    }
}

impl<P, TF, L> Color<[f32; 4], Rgb<P, TF, L>>
where
    P: Primaries,
    TF: TransferFunction,
    L: BackingStore<[f32; 4]> + BackingStore<[u8; 4]> + ChannelMap<4>,
{
    /// Quantizes the color to `u8` with an applied dither to prevent banding.
    ///
    /// The `dither` value is added to the color channels (R, G, B) before scaling
    /// and rounding. The alpha channel is quantized directly without dither to
    /// preserve clean transparency masks.
    #[inline(always)]
    pub fn to_u8_dithered(self, dither: f32) -> Color<[u8; 4], Rgb<P, TF, L>> {
        let mut out = self.inner();
        let [ri, gi, bi, _ai] = L::INDICES;

        // Dither color channels only
        out[ri] += dither;
        out[gi] += dither;
        out[bi] += dither;

        Color::new([
            quantize_f32_to_u8(out[0]),
            quantize_f32_to_u8(out[1]),
            quantize_f32_to_u8(out[2]),
            quantize_f32_to_u8(out[3]),
        ])
    }
}