kahlo 0.0.3

Optimized software rendering library.
Documentation
use crate::formats::PixelFormatSpec;

// pub use rgb::

pub type Colour = rgb::Rgba<u8, u8>;

/// Helper type alias for Americans.
pub type Color = Colour;

#[derive(Clone, Copy, PartialEq, Debug)]
/// Describes a blending mode between a backdrop and a source pixel.
pub enum BlendMode {
    /// Use source colour without doing any blending.
    SourceCopy,
    /// Use the alpha value of both the source and backdrop pixels.
    Simple,
    /// Only use the alpha value of the source.
    SourceAlpha,
    /// Only use the alpha value of the backdrop.
    BackdropAlpha,
    /// Multiply colour channels together.
    Multiply,
}

impl BlendMode {
    // computes a*alpha + b*(1-alpha)
    const fn mix(a: u8, b: u8, alpha: u8) -> u8 {
        (((a as u16 * alpha as u16) + (b as u16 * (255 - alpha as u16)) + 128) / 255) as u8
    }

    // computes a*alpha1 + b*alpha2*(1-alpha1)
    const fn mix2(a: u8, b: u8, alpha1: u8, alpha2: u8) -> u8 {
        let a_term = a as u32 * alpha1 as u32;
        let b_term = b as u32 * alpha2 as u32 * (255 - alpha1) as u32;
        (((a_term + (b_term + 128) / 255) + 128) / 255) as u8
    }

    /// Generic colour blending function.
    pub const fn blend(&self, source: &Colour, backdrop: &Colour) -> Colour {
        // Alpha compositing colour and alpha formulae: [1]
        // - co = Cs x αs + Cb x αb x (1 - αs)
        // - αo = αs + αb x (1 - αs)
        // [1]: https://www.w3.org/TR/compositing/#simplealphacompositing
        match self {
            Self::SourceCopy => *source,
            Self::Simple => Colour {
                r: Self::mix2(source.r, backdrop.r, source.a, backdrop.a),
                g: Self::mix2(source.g, backdrop.g, source.a, backdrop.a),
                b: Self::mix2(source.b, backdrop.b, source.a, backdrop.a),
                a: Self::mix(255, backdrop.a, source.a),
            },
            Self::SourceAlpha => Colour {
                r: Self::mix(source.r, backdrop.r, source.a),
                g: Self::mix(source.g, backdrop.g, source.a),
                b: Self::mix(source.b, backdrop.b, source.a),
                a: Self::mix(255, backdrop.a, source.a),
            },
            Self::BackdropAlpha => Colour {
                r: Self::mix(source.r, backdrop.r, backdrop.a),
                g: Self::mix(source.g, backdrop.g, backdrop.a),
                b: Self::mix(source.b, backdrop.b, backdrop.a),
                a: Self::mix(255, source.a, backdrop.a),
            },
            Self::Multiply => Colour {
                r: ((source.r as u16 * backdrop.r as u16 + 128) / 255) as u8,
                g: ((source.g as u16 * backdrop.g as u16 + 128) / 255) as u8,
                b: ((source.b as u16 * backdrop.b as u16 + 128) / 255) as u8,
                a: ((source.a as u16 * backdrop.a as u16 + 128) / 255) as u8,
            },
        }
    }
}

pub(crate) fn write_as_bytes<Format: PixelFormatSpec>(col: &Colour, to: &mut [u8]) {
    if let Some(offset) = Format::RED_PACK.index() {
        to[offset as usize] = col.r;
    }
    if let Some(offset) = Format::GREEN_PACK.index() {
        to[offset as usize] = col.g;
    }
    if let Some(offset) = Format::BLUE_PACK.index() {
        to[offset as usize] = col.b;
    }
    if let Some(offset) = Format::ALPHA_PACK.index() {
        to[offset as usize] = col.a;
    }
}

pub(crate) fn read_from_bytes<Format: PixelFormatSpec>(from: &[u8]) -> Colour {
    Colour {
        r: if let Some(offset) = Format::RED_PACK.index() {
            from[offset as usize]
        } else {
            255
        },
        g: if let Some(offset) = Format::GREEN_PACK.index() {
            from[offset as usize]
        } else {
            255
        },
        b: if let Some(offset) = Format::BLUE_PACK.index() {
            from[offset as usize]
        } else {
            255
        },
        a: if let Some(offset) = Format::ALPHA_PACK.index() {
            from[offset as usize]
        } else {
            255
        },
    }
}

#[cfg(test)]
#[allow(non_upper_case_globals)]
pub mod test {
    use super::{BlendMode, Colour};
    use crate::palette::css;
    use rstest::rstest;
    use rstest_reuse::{apply, template};

    /// Colour blending test case
    #[derive(Clone, Copy)]
    pub struct BlendTestCase {
        pub desc: &'static str,
        pub source: Colour,
        pub backdrop: Colour,

        pub simple_result: Option<Colour>,
        pub source_result: Option<Colour>,
        pub backdrop_result: Option<Colour>,
        pub multiply_result: Option<Colour>,
    }

    pub const fn c(v: u32, a: u8) -> Colour {
        Colour::new(
            ((v >> 16) & 0xff) as u8,
            ((v >> 8) & 0xff) as u8,
            ((v >> 0) & 0xff) as u8,
            a,
        )
    }
    pub const tgrey: Colour = c(0x808080, 0x80);
    pub const unknown: Colour = c(0, 0);

    #[allow(unused)]
    pub const empty_case: BlendTestCase = BlendTestCase {
        desc: "",
        source: unknown,
        backdrop: unknown,
        simple_result: None,
        source_result: None,
        backdrop_result: None,
        multiply_result: None,
    };

    #[template]
    #[rstest]
    #[case(BlendTestCase {
        desc: "opaque white over opaque black (#1)",
        source: css::white,
        backdrop: css::black,
        simple_result: Some(css::white),
        source_result: Some(css::white),
        backdrop_result: Some(css::white),
        multiply_result: Some(css::black),
    })]
    #[case(BlendTestCase {
        desc: "opaque black over opaque white (#2)",
        source: css::black,
        backdrop: css::white,
        simple_result: Some(css::black),
        source_result: Some(css::black),
        backdrop_result: Some(css::black),
        multiply_result: Some(css::black),
    })]
    #[case(BlendTestCase {
        desc: "transparent gray over opaque red (#3)",
        source: tgrey,
        backdrop: css::red,
        simple_result: Some(c(0xbf4040, 0xff)),
        source_result: Some(c(0xbf4040, 0xff)),
        backdrop_result: Some(c(0x808080, 0xff)),
        multiply_result: Some(c(0x800000, 0x80)),
    })]
    #[case(BlendTestCase {
        desc: "transparent gray over transparent gray (#4)",
        source: tgrey,
        backdrop: tgrey,
        simple_result: Some(c(0x606060, 0xc0)),
        source_result: Some(c(0x808080, 0xc0)),
        backdrop_result: Some(c(0x808080, 0xc0)),
        multiply_result: Some(c(0x404040, 0x40)),
    })]
    #[case(BlendTestCase {
        desc: "regression test for initial incorrect AVX output (#5)",
        source: c(0x71fc28, 0x81),
        backdrop: c(0x3589ce, 0x5d),
        simple_result: Some(c(0x439839, 0xaf)),
        source_result: Some(c(0x53c37a, 0xaf)),
        backdrop_result: Some(c(0x4bb391, 0xaf)),
        multiply_result: Some(c(0x178720, 0x2f)),
        ..empty_case
    })]
    #[case(BlendTestCase {
        desc: "regression test for initial incorrect generic output (#6)",
        source: c(0xa1e09e, 0xb5),
        backdrop: c(0xcc7c12, 0x3e),
        simple_result: Some(c(0x81a871, 0xc7)),
        source_result: Some(c(0xadc375, 0xc7)),
        backdrop_result: Some(c(0xc29434, 0xc7)),
        multiply_result: Some(c(0x816d0b, 0x2c)),
        ..empty_case
    })]
    fn blend_template(#[case] tc: BlendTestCase) {}

    #[apply(blend_template)]
    fn simple_blend_mode(tc: BlendTestCase) {
        println!("blending case: {}", tc.desc);
        let r = tc
            .simple_result
            .expect("there is no expected simple result for this case");
        println!("blending {:?} and {:?}", tc.source, tc.backdrop);
        println!("expecting {r:?}");
        assert_eq!(r, BlendMode::Simple.blend(&tc.source, &tc.backdrop),)
    }

    #[apply(blend_template)]
    fn source_alpha_blend_mode(tc: BlendTestCase) {
        println!("blending case: {}", tc.desc);
        let r = tc
            .source_result
            .expect("there is no expected source result for this case");
        println!("blending {:?} and {:?}", tc.source, tc.backdrop);
        println!("expecting {r:?}");
        assert_eq!(r, BlendMode::SourceAlpha.blend(&tc.source, &tc.backdrop),)
    }

    #[apply(blend_template)]
    fn backdrop_alpha_blend_mode(tc: BlendTestCase) {
        println!("blending case: {}", tc.desc);
        let r = tc
            .backdrop_result
            .expect("there is no expected backdrop result for this case");
        println!("blending {:?} and {:?}", tc.source, tc.backdrop);
        println!("expecting {r:?}");
        assert_eq!(r, BlendMode::BackdropAlpha.blend(&tc.source, &tc.backdrop),)
    }

    #[apply(blend_template)]
    fn multiply_alpha_blend_mode(tc: BlendTestCase) {
        println!("blending case: {}", tc.desc);
        let r = tc
            .multiply_result
            .expect("there is no expected multiply result for this case");
        println!("blending {:?} and {:?}", tc.source, tc.backdrop);
        println!("expecting {r:?}");
        assert_eq!(r, BlendMode::Multiply.blend(&tc.source, &tc.backdrop),)
    }
}