use rayon::prelude::*;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Error)]
pub enum BlendError {
#[error("Buffer size mismatch: expected {expected}, got {actual}")]
BufferSizeMismatch { expected: usize, actual: usize },
#[error("Invalid dimensions: {width}x{height}")]
InvalidDimensions { width: u32, height: u32 },
#[error("Pixel count overflow for {width}x{height}")]
PixelCountOverflow { width: u32, height: u32 },
#[error("Mask length mismatch: expected {expected}, got {actual}")]
MaskLengthMismatch { expected: usize, actual: usize },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlendMode {
AlphaComposite,
Additive,
Multiply,
Screen,
Overlay,
SoftLight,
Difference,
Dissolve,
}
impl BlendMode {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::AlphaComposite => "alpha_composite",
Self::Additive => "additive",
Self::Multiply => "multiply",
Self::Screen => "screen",
Self::Overlay => "overlay",
Self::SoftLight => "soft_light",
Self::Difference => "difference",
Self::Dissolve => "dissolve",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct BlendStats {
pub pixels_blended: u64,
pub mode: Option<BlendMode>,
pub opacity: u8,
}
#[derive(Debug, Clone, Default)]
pub struct BlendKernel;
impl BlendKernel {
fn validate_rgba(buf: &[u8], width: u32, height: u32) -> Result<usize, BlendError> {
if width == 0 || height == 0 {
return Err(BlendError::InvalidDimensions { width, height });
}
let pixels = (width as usize)
.checked_mul(height as usize)
.ok_or(BlendError::PixelCountOverflow { width, height })?;
let expected = pixels * 4;
if buf.len() != expected {
return Err(BlendError::BufferSizeMismatch {
expected,
actual: buf.len(),
});
}
Ok(pixels)
}
pub fn blend(
src: &[u8],
dst: &mut [u8],
width: u32,
height: u32,
mode: BlendMode,
opacity: u8,
) -> Result<BlendStats, BlendError> {
Self::validate_rgba(src, width, height)?;
let pixels = Self::validate_rgba(dst, width, height)?;
let op = opacity as f32 / 255.0;
src.par_chunks(4)
.zip(dst.par_chunks_mut(4))
.for_each(|(s, d)| {
blend_pixel(s, d, mode, op);
});
Ok(BlendStats {
pixels_blended: pixels as u64,
mode: Some(mode),
opacity,
})
}
pub fn blend_masked(
src: &[u8],
dst: &mut [u8],
mask: &[u8],
width: u32,
height: u32,
mode: BlendMode,
global_opacity: u8,
) -> Result<BlendStats, BlendError> {
Self::validate_rgba(src, width, height)?;
let pixels = Self::validate_rgba(dst, width, height)?;
if mask.len() != pixels {
return Err(BlendError::MaskLengthMismatch {
expected: pixels,
actual: mask.len(),
});
}
let go = global_opacity as f32 / 255.0;
src.par_chunks(4)
.zip(dst.par_chunks_mut(4))
.zip(mask.par_iter())
.for_each(|((s, d), &m)| {
let op = go * (m as f32 / 255.0);
blend_pixel(s, d, mode, op);
});
Ok(BlendStats {
pixels_blended: pixels as u64,
mode: Some(mode),
opacity: global_opacity,
})
}
pub fn composite_layers(
layers: &[(&[u8], u8)],
width: u32,
height: u32,
) -> Result<Vec<u8>, BlendError> {
if width == 0 || height == 0 {
return Err(BlendError::InvalidDimensions { width, height });
}
let pixels = (width as usize)
.checked_mul(height as usize)
.ok_or(BlendError::PixelCountOverflow { width, height })?;
let buf_size = pixels * 4;
for (i, (layer, _)) in layers.iter().enumerate() {
if layer.len() != buf_size {
return Err(BlendError::BufferSizeMismatch {
expected: buf_size,
actual: layer.len(),
});
}
let _ = i;
}
if layers.is_empty() {
return Ok(vec![0u8; buf_size]);
}
let mut acc = vec![0u8; buf_size];
for (layer, opacity) in layers {
let op = *opacity as f32 / 255.0;
layer
.par_chunks(4)
.zip(acc.par_chunks_mut(4))
.for_each(|(s, d)| {
blend_pixel(s, d, BlendMode::AlphaComposite, op);
});
}
Ok(acc)
}
pub fn apply_tint(
src: &[u8],
dst: &mut [u8],
width: u32,
height: u32,
tint: [u8; 4],
) -> Result<(), BlendError> {
Self::validate_rgba(src, width, height)?;
Self::validate_rgba(dst, width, height)?;
src.par_chunks(4)
.zip(dst.par_chunks_mut(4))
.for_each(|(s, d)| {
for c in 0..4 {
let v = (s[c] as u32 * tint[c] as u32 + 127) / 255;
d[c] = v.min(255) as u8;
}
});
Ok(())
}
pub fn premultiply_alpha(buf: &mut [u8], width: u32, height: u32) -> Result<(), BlendError> {
Self::validate_rgba(buf, width, height)?;
buf.par_chunks_mut(4).for_each(|px| {
let a = px[3] as u32;
for c in 0..3 {
px[c] = ((px[c] as u32 * a + 127) / 255) as u8;
}
});
Ok(())
}
pub fn unpremultiply_alpha(buf: &mut [u8], width: u32, height: u32) -> Result<(), BlendError> {
Self::validate_rgba(buf, width, height)?;
buf.par_chunks_mut(4).for_each(|px| {
let a = px[3] as f32;
if a > 0.0 {
for c in 0..3 {
px[c] = (px[c] as f32 / a * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
});
Ok(())
}
}
fn blend_pixel(s: &[u8], d: &mut [u8], mode: BlendMode, opacity: f32) {
let sa = (s[3] as f32 / 255.0) * opacity;
match mode {
BlendMode::AlphaComposite => alpha_composite(s, d, sa),
BlendMode::Additive => additive(s, d, sa),
BlendMode::Multiply => multiply(s, d, sa),
BlendMode::Screen => screen(s, d, sa),
BlendMode::Overlay => overlay(s, d, sa),
BlendMode::SoftLight => soft_light(s, d, sa),
BlendMode::Difference => difference(s, d, sa),
BlendMode::Dissolve => dissolve(s, d, sa),
}
}
fn alpha_composite(s: &[u8], d: &mut [u8], sa: f32) {
let da = d[3] as f32 / 255.0;
let out_a = sa + da * (1.0 - sa);
if out_a < 1e-9 {
d[0] = 0;
d[1] = 0;
d[2] = 0;
d[3] = 0;
return;
}
for c in 0..3 {
let sc = s[c] as f32 / 255.0;
let dc = d[c] as f32 / 255.0;
let out_c = (sc * sa + dc * da * (1.0 - sa)) / out_a;
d[c] = (out_c * 255.0).round().clamp(0.0, 255.0) as u8;
}
d[3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
}
fn additive(s: &[u8], d: &mut [u8], sa: f32) {
for c in 0..3 {
let v = d[c] as f32 + s[c] as f32 * sa;
d[c] = v.round().clamp(0.0, 255.0) as u8;
}
}
fn multiply(s: &[u8], d: &mut [u8], sa: f32) {
for c in 0..3 {
let dc = d[c] as f32;
let sc = s[c] as f32;
let blended = dc * sc / 255.0;
d[c] = lerp_channel(dc, blended, sa);
}
}
fn screen(s: &[u8], d: &mut [u8], sa: f32) {
for c in 0..3 {
let dc = d[c] as f32;
let sc = s[c] as f32;
let blended = 255.0 - (255.0 - dc) * (255.0 - sc) / 255.0;
d[c] = lerp_channel(dc, blended, sa);
}
}
fn overlay(s: &[u8], d: &mut [u8], sa: f32) {
for c in 0..3 {
let dc = d[c] as f32 / 255.0;
let sc = s[c] as f32 / 255.0;
let blended = if dc < 0.5 {
2.0 * dc * sc
} else {
1.0 - 2.0 * (1.0 - dc) * (1.0 - sc)
};
d[c] = lerp_channel(d[c] as f32, blended * 255.0, sa);
}
}
fn soft_light(s: &[u8], d: &mut [u8], sa: f32) {
for c in 0..3 {
let dc = d[c] as f32 / 255.0;
let sc = s[c] as f32 / 255.0;
let blended = 2.0 * dc * sc + dc * dc * (1.0 - 2.0 * sc);
d[c] = lerp_channel(d[c] as f32, blended * 255.0, sa);
}
}
fn difference(s: &[u8], d: &mut [u8], sa: f32) {
for c in 0..3 {
let dc = d[c] as f32;
let sc = s[c] as f32;
let blended = (dc - sc).abs();
d[c] = lerp_channel(dc, blended, sa);
}
}
fn dissolve(s: &[u8], d: &mut [u8], sa: f32) {
let hash =
xorshift32(s[0] as u32 ^ (s[1] as u32 * 17) ^ (d[0] as u32 * 31) ^ (d[1] as u32 * 7));
let threshold = (hash & 0xFF) as f32 / 255.0;
if sa > threshold {
for c in 0..3 {
d[c] = s[c];
}
d[3] = s[3];
}
}
#[inline]
fn lerp_channel(a: f32, b: f32, t: f32) -> u8 {
(a + (b - a) * t).round().clamp(0.0, 255.0) as u8
}
#[inline]
fn xorshift32(mut x: u32) -> u32 {
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_blend_mode_labels() {
assert_eq!(BlendMode::AlphaComposite.label(), "alpha_composite");
assert_eq!(BlendMode::Additive.label(), "additive");
assert_eq!(BlendMode::Multiply.label(), "multiply");
assert_eq!(BlendMode::Screen.label(), "screen");
assert_eq!(BlendMode::Overlay.label(), "overlay");
assert_eq!(BlendMode::SoftLight.label(), "soft_light");
assert_eq!(BlendMode::Difference.label(), "difference");
assert_eq!(BlendMode::Dissolve.label(), "dissolve");
}
#[test]
fn test_blend_invalid_dims() {
let src = vec![0u8; 4];
let mut dst = vec![0u8; 4];
let err = BlendKernel::blend(&src, &mut dst, 0, 1, BlendMode::Additive, 255);
assert!(matches!(err, Err(BlendError::InvalidDimensions { .. })));
}
#[test]
fn test_blend_buffer_mismatch() {
let src = vec![0u8; 8]; let mut dst = vec![0u8; 4];
let err = BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Additive, 255);
assert!(matches!(err, Err(BlendError::BufferSizeMismatch { .. })));
}
#[test]
fn test_blend_masked_mask_mismatch() {
let src = vec![255u8; 4 * 4 * 4];
let mut dst = vec![0u8; 4 * 4 * 4];
let mask = vec![255u8; 10]; let err = BlendKernel::blend_masked(&src, &mut dst, &mask, 4, 4, BlendMode::Multiply, 255);
assert!(matches!(err, Err(BlendError::MaskLengthMismatch { .. })));
}
#[test]
fn test_opacity_zero_preserves_dst() -> Result<(), BlendError> {
let src: Vec<u8> = vec![255, 0, 0, 255]; let original_dst = vec![0u8, 128, 255, 255]; let mut dst = original_dst.clone();
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::AlphaComposite, 0)?;
for (orig, &out) in original_dst.iter().zip(dst.iter()) {
let diff = (*orig as i16 - out as i16).abs();
assert!(diff <= 1, "channel diff={diff}");
}
Ok(())
}
#[test]
fn test_alpha_composite_fully_opaque_src() -> Result<(), BlendError> {
let src = vec![200u8, 100, 50, 255]; let mut dst = vec![0u8, 0, 0, 255];
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::AlphaComposite, 255)?;
assert_eq!(dst[0], 200);
assert_eq!(dst[1], 100);
assert_eq!(dst[2], 50);
assert_eq!(dst[3], 255);
Ok(())
}
#[test]
fn test_additive_blend_clamps_to_255() -> Result<(), BlendError> {
let src = vec![200u8, 200, 200, 255];
let mut dst = vec![100u8, 100, 100, 255];
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Additive, 255)?;
assert_eq!(dst[0], 255, "200+100=300 → clamp to 255");
Ok(())
}
#[test]
fn test_additive_blend_zero_src() -> Result<(), BlendError> {
let src = vec![0u8, 0, 0, 255];
let original = vec![100u8, 150, 200, 255];
let mut dst = original.clone();
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Additive, 255)?;
assert_eq!(dst[..3], original[..3]);
Ok(())
}
#[test]
fn test_multiply_with_white_src_unchanged() -> Result<(), BlendError> {
let src = vec![255u8, 255, 255, 255]; let original = vec![100u8, 150, 200, 255];
let mut dst = original.clone();
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Multiply, 255)?;
for c in 0..3 {
let diff = (original[c] as i16 - dst[c] as i16).abs();
assert!(diff <= 1, "channel {c}: diff={diff}");
}
Ok(())
}
#[test]
fn test_multiply_with_black_src_yields_zero() -> Result<(), BlendError> {
let src = vec![0u8, 0, 0, 255]; let mut dst = vec![200u8, 150, 100, 255];
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Multiply, 255)?;
for c in 0..3 {
assert_eq!(dst[c], 0, "channel {c} should be 0");
}
Ok(())
}
#[test]
fn test_screen_with_black_src_unchanged() -> Result<(), BlendError> {
let src = vec![0u8, 0, 0, 255]; let original = vec![100u8, 150, 200, 255];
let mut dst = original.clone();
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Screen, 255)?;
for c in 0..3 {
let diff = (original[c] as i16 - dst[c] as i16).abs();
assert!(diff <= 1, "channel {c}: diff={diff}");
}
Ok(())
}
#[test]
fn test_screen_with_white_src_yields_white() -> Result<(), BlendError> {
let src = vec![255u8, 255, 255, 255]; let mut dst = vec![100u8, 150, 200, 255];
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Screen, 255)?;
for c in 0..3 {
assert_eq!(
dst[c], 255,
"channel {c} should be 255 after screen with white"
);
}
Ok(())
}
#[test]
fn test_difference_with_same_src_dst_yields_black() -> Result<(), BlendError> {
let src = vec![100u8, 150, 200, 255];
let mut dst = vec![100u8, 150, 200, 255];
BlendKernel::blend(&src, &mut dst, 1, 1, BlendMode::Difference, 255)?;
for c in 0..3 {
assert_eq!(dst[c], 0, "difference of equal values should be 0");
}
Ok(())
}
#[test]
fn test_masked_blend_all_opaque() -> Result<(), BlendError> {
let w = 2u32;
let h = 2u32;
let src = vec![255u8; (w * h * 4) as usize];
let mut dst = vec![0u8; (w * h * 4) as usize];
let mask = vec![255u8; (w * h) as usize]; BlendKernel::blend_masked(&src, &mut dst, &mask, w, h, BlendMode::AlphaComposite, 255)?;
for &v in &dst {
assert_eq!(v, 255);
}
Ok(())
}
#[test]
fn test_masked_blend_all_transparent_preserves_dst() -> Result<(), BlendError> {
let w = 2u32;
let h = 2u32;
let src = vec![255u8; (w * h * 4) as usize];
let original_dst = vec![100u8; (w * h * 4) as usize];
let mut dst = original_dst.clone();
let mask = vec![0u8; (w * h) as usize]; BlendKernel::blend_masked(&src, &mut dst, &mask, w, h, BlendMode::AlphaComposite, 255)?;
assert_eq!(dst, original_dst);
Ok(())
}
#[test]
fn test_composite_layers_empty_returns_transparent() -> Result<(), BlendError> {
let result = BlendKernel::composite_layers(&[], 4, 4)?;
assert_eq!(result.len(), 4 * 4 * 4);
assert!(result.iter().all(|&v| v == 0));
Ok(())
}
#[test]
fn test_composite_layers_single_opaque() -> Result<(), BlendError> {
let layer = vec![200u8, 100, 50, 255]; let result = BlendKernel::composite_layers(&[(&layer, 255)], 1, 1)?;
assert_eq!(result.len(), 4);
assert_eq!(result[0], 200);
assert_eq!(result[1], 100);
assert_eq!(result[2], 50);
Ok(())
}
#[test]
fn test_apply_tint_white_tint_unchanged() -> Result<(), BlendError> {
let src = vec![100u8, 150, 200, 255];
let mut dst = vec![0u8; 4];
BlendKernel::apply_tint(&src, &mut dst, 1, 1, [255, 255, 255, 255])?;
for c in 0..3 {
let diff = (src[c] as i16 - dst[c] as i16).abs();
assert!(diff <= 1, "channel {c}: diff={diff}");
}
Ok(())
}
#[test]
fn test_apply_tint_black_tint_yields_black() -> Result<(), BlendError> {
let src = vec![200u8, 150, 100, 255];
let mut dst = vec![0u8; 4];
BlendKernel::apply_tint(&src, &mut dst, 1, 1, [0, 0, 0, 0])?;
assert_eq!(dst[0], 0);
assert_eq!(dst[1], 0);
assert_eq!(dst[2], 0);
Ok(())
}
#[test]
fn test_premultiply_alpha_full_opaque() -> Result<(), BlendError> {
let mut buf = vec![200u8, 100, 50, 255];
BlendKernel::premultiply_alpha(&mut buf, 1, 1)?;
assert_eq!(buf[0], 200);
assert_eq!(buf[1], 100);
assert_eq!(buf[2], 50);
Ok(())
}
#[test]
fn test_premultiply_alpha_half_opacity() -> Result<(), BlendError> {
let mut buf = vec![200u8, 200, 200, 128];
BlendKernel::premultiply_alpha(&mut buf, 1, 1)?;
let expected = (200u32 * 128 + 127) / 255;
let diff = (buf[0] as i32 - expected as i32).abs();
assert!(
diff <= 1,
"premultiplied R: got {}, expected ~{}",
buf[0],
expected
);
Ok(())
}
#[test]
fn test_unpremultiply_alpha_zero_alpha() -> Result<(), BlendError> {
let mut buf = vec![100u8, 100, 100, 0]; BlendKernel::unpremultiply_alpha(&mut buf, 1, 1)?;
assert_eq!(buf[0], 100); Ok(())
}
#[test]
fn test_premultiply_unpremultiply_roundtrip() -> Result<(), BlendError> {
let original = vec![200u8, 150, 100, 200];
let mut buf = original.clone();
BlendKernel::premultiply_alpha(&mut buf, 1, 1)?;
BlendKernel::unpremultiply_alpha(&mut buf, 1, 1)?;
for c in 0..3 {
let diff = (original[c] as i16 - buf[c] as i16).abs();
assert!(
diff <= 2,
"channel {c}: orig={} back={} diff={diff}",
original[c],
buf[c]
);
}
Ok(())
}
#[test]
fn test_blend_stats_returned() -> Result<(), BlendError> {
let src = vec![0u8; 4 * 4 * 4];
let mut dst = vec![0u8; 4 * 4 * 4];
let stats = BlendKernel::blend(&src, &mut dst, 4, 4, BlendMode::Screen, 200)?;
assert_eq!(stats.pixels_blended, 16);
assert_eq!(stats.mode, Some(BlendMode::Screen));
assert_eq!(stats.opacity, 200);
Ok(())
}
}