use crate::RangaError;
use crate::pixel::{PixelBuffer, PixelFormat};
#[inline(always)]
fn div255(val: u16) -> u16 {
let tmp = val + 128;
(tmp + (tmp >> 8)) >> 8
}
fn validate_rgba8(op: &str, buf: &PixelBuffer) -> Result<(), RangaError> {
if buf.format != PixelFormat::Rgba8 {
return Err(RangaError::InvalidFormat(format!(
"{op}: expected Rgba8, got {:?}",
buf.format
)));
}
Ok(())
}
fn validate_same_size(a: &PixelBuffer, b: &PixelBuffer) -> Result<(), RangaError> {
if a.width != b.width || a.height != b.height {
return Err(RangaError::DimensionMismatch {
expected: a.pixel_count(),
actual: b.pixel_count(),
});
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn premultiply_alpha(buf: &mut PixelBuffer) -> Result<(), RangaError> {
validate_rgba8("premultiply_alpha", buf)?;
for pixel in buf.data.chunks_exact_mut(4) {
let a = pixel[3] as u16;
pixel[0] = div255(pixel[0] as u16 * a) as u8;
pixel[1] = div255(pixel[1] as u16 * a) as u8;
pixel[2] = div255(pixel[2] as u16 * a) as u8;
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn unpremultiply_alpha(buf: &mut PixelBuffer) -> Result<(), RangaError> {
validate_rgba8("unpremultiply_alpha", buf)?;
for pixel in buf.data.chunks_exact_mut(4) {
let a = pixel[3] as u16;
if a == 0 {
continue;
}
pixel[0] = ((pixel[0] as u16 * 255) / a).min(255) as u8;
pixel[1] = ((pixel[1] as u16 * 255) / a).min(255) as u8;
pixel[2] = ((pixel[2] as u16 * 255) / a).min(255) as u8;
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn apply_mask(buf: &mut PixelBuffer, mask: &PixelBuffer) -> Result<(), RangaError> {
validate_rgba8("apply_mask", buf)?;
validate_rgba8("apply_mask", mask)?;
validate_same_size(buf, mask)?;
for (pixel, mask_pixel) in buf.data.chunks_exact_mut(4).zip(mask.data.chunks_exact(4)) {
let mask_val = mask_pixel[0] as u16; pixel[3] = div255(pixel[3] as u16 * mask_val) as u8;
}
Ok(())
}
#[must_use = "returns a new blended buffer"]
pub fn dissolve(
a: &PixelBuffer,
b: &PixelBuffer,
progress: f32,
) -> Result<PixelBuffer, RangaError> {
validate_rgba8("dissolve", a)?;
validate_rgba8("dissolve", b)?;
validate_same_size(a, b)?;
let t = progress.clamp(0.0, 1.0);
let inv_t = 1.0 - t;
let mut out = vec![0u8; a.data.len()];
for (i, (pa, pb)) in a.data.iter().zip(b.data.iter()).enumerate() {
out[i] = (*pa as f32 * inv_t + *pb as f32 * t + 0.5).clamp(0.0, 255.0) as u8;
}
PixelBuffer::new(out, a.width, a.height, PixelFormat::Rgba8)
}
#[must_use = "this returns a Result that may contain an error"]
pub fn fade(buf: &mut PixelBuffer, progress: f32) -> Result<(), RangaError> {
validate_rgba8("fade", buf)?;
let p = progress.clamp(0.0, 1.0);
for pixel in buf.data.chunks_exact_mut(4) {
pixel[0] = (pixel[0] as f32 * p + 0.5).clamp(0.0, 255.0) as u8;
pixel[1] = (pixel[1] as f32 * p + 0.5).clamp(0.0, 255.0) as u8;
pixel[2] = (pixel[2] as f32 * p + 0.5).clamp(0.0, 255.0) as u8;
}
Ok(())
}
#[must_use = "returns a new wiped buffer"]
pub fn wipe(a: &PixelBuffer, b: &PixelBuffer, progress: f32) -> Result<PixelBuffer, RangaError> {
validate_rgba8("wipe", a)?;
validate_rgba8("wipe", b)?;
validate_same_size(a, b)?;
let threshold = (progress.clamp(0.0, 1.0) * a.width as f32) as u32;
let w = a.width as usize;
let mut out = vec![0u8; a.data.len()];
for y in 0..a.height as usize {
for x in 0..w {
let i = (y * w + x) * 4;
let src = if (x as u32) < threshold {
&b.data
} else {
&a.data
};
out[i..i + 4].copy_from_slice(&src[i..i + 4]);
}
}
PixelBuffer::new(out, a.width, a.height, PixelFormat::Rgba8)
}
#[must_use = "this returns a Result that may contain an error"]
pub fn fill_solid(buf: &mut PixelBuffer, color: [u8; 4]) -> Result<(), RangaError> {
validate_rgba8("fill_solid", buf)?;
for pixel in buf.data.chunks_exact_mut(4) {
pixel.copy_from_slice(&color);
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn gradient_linear(
buf: &mut PixelBuffer,
start: [u8; 4],
end: [u8; 4],
) -> Result<(), RangaError> {
validate_rgba8("gradient_linear", buf)?;
let w = buf.width as f32;
if w < 1.0 {
return Ok(());
}
for y in 0..buf.height as usize {
for x in 0..buf.width as usize {
let t = x as f32 / (w - 1.0).max(1.0);
let i = (y * buf.width as usize + x) * 4;
for c in 0..4 {
buf.data[i + c] = (start[c] as f32 + t * (end[c] as f32 - start[c] as f32) + 0.5)
.clamp(0.0, 255.0) as u8;
}
}
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn gradient_linear_angled(
buf: &mut PixelBuffer,
start: [u8; 4],
end: [u8; 4],
angle_deg: f32,
) -> Result<(), RangaError> {
validate_rgba8("gradient_linear_angled", buf)?;
let w = buf.width as f32;
let h = buf.height as f32;
if w < 1.0 || h < 1.0 {
return Ok(());
}
let angle_rad = angle_deg.to_radians();
let dx = angle_rad.cos();
let dy = angle_rad.sin();
let corners = [
(0.0f32, 0.0f32),
(w - 1.0, 0.0),
(0.0, h - 1.0),
(w - 1.0, h - 1.0),
];
let mut min_proj = f32::MAX;
let mut max_proj = f32::MIN;
for (cx, cy) in corners {
let p = cx * dx + cy * dy;
min_proj = min_proj.min(p);
max_proj = max_proj.max(p);
}
let range = max_proj - min_proj;
if range < 1e-6 {
return Ok(());
}
for y in 0..buf.height as usize {
for x in 0..buf.width as usize {
let proj = x as f32 * dx + y as f32 * dy;
let t = ((proj - min_proj) / range).clamp(0.0, 1.0);
let i = (y * buf.width as usize + x) * 4;
for c in 0..4 {
buf.data[i + c] = (start[c] as f32 + t * (end[c] as f32 - start[c] as f32) + 0.5)
.clamp(0.0, 255.0) as u8;
}
}
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn gradient_radial(
buf: &mut PixelBuffer,
center: (f32, f32),
radius: f32,
start: [u8; 4],
end: [u8; 4],
) -> Result<(), RangaError> {
validate_rgba8("gradient_radial", buf)?;
let r = radius.max(1e-6);
for y in 0..buf.height as usize {
for x in 0..buf.width as usize {
let dx = x as f32 - center.0;
let dy = y as f32 - center.1;
let dist = (dx * dx + dy * dy).sqrt();
let t = (dist / r).clamp(0.0, 1.0);
let i = (y * buf.width as usize + x) * 4;
for c in 0..4 {
buf.data[i + c] = (start[c] as f32 + t * (end[c] as f32 - start[c] as f32) + 0.5)
.clamp(0.0, 255.0) as u8;
}
}
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn composite_at(
src: &PixelBuffer,
dst: &mut PixelBuffer,
x: i32,
y: i32,
opacity: f32,
) -> Result<(), RangaError> {
validate_rgba8("composite_at", src)?;
validate_rgba8("composite_at", dst)?;
let dw = dst.width as i32;
let dh = dst.height as i32;
let sw = src.width as i32;
let sh = src.height as i32;
let x0 = x.max(0);
let y0 = y.max(0);
let x1 = (x + sw).min(dw);
let y1 = (y + sh).min(dh);
if x0 >= x1 || y0 >= y1 {
return Ok(()); }
let op = (opacity.clamp(0.0, 1.0) * 255.0) as u16;
if op == 0 {
return Ok(());
}
let dst_stride = dst.width as usize;
let src_stride = src.width as usize;
for dy in y0..y1 {
let sy = (dy - y) as usize;
for dx in x0..x1 {
let sx = (dx - x) as usize;
let si = (sy * src_stride + sx) * 4;
let di = (dy as usize * dst_stride + dx as usize) * 4;
let sa = div255(src.data[si + 3] as u16 * op);
if sa == 0 {
continue;
}
if sa >= 255 && op >= 255 {
dst.data[di..di + 4].copy_from_slice(&src.data[si..si + 4]);
continue;
}
let inv_sa = 255u16 - sa;
dst.data[di] = div255(src.data[si] as u16 * sa + dst.data[di] as u16 * inv_sa) as u8;
dst.data[di + 1] =
div255(src.data[si + 1] as u16 * sa + dst.data[di + 1] as u16 * inv_sa) as u8;
dst.data[di + 2] =
div255(src.data[si + 2] as u16 * sa + dst.data[di + 2] as u16 * inv_sa) as u8;
dst.data[di + 3] = div255(sa * 255 + dst.data[di + 3] as u16 * inv_sa).min(255) as u8;
}
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn composite_at_argb(
src: &PixelBuffer,
dst: &mut PixelBuffer,
x: i32,
y: i32,
opacity: f32,
) -> Result<(), RangaError> {
if src.format != PixelFormat::Argb8 {
return Err(RangaError::InvalidFormat(format!(
"composite_at_argb: expected Argb8, got {:?}",
src.format
)));
}
if dst.format != PixelFormat::Argb8 {
return Err(RangaError::InvalidFormat(format!(
"composite_at_argb: expected Argb8, got {:?}",
dst.format
)));
}
let dw = dst.width as i32;
let dh = dst.height as i32;
let sw = src.width as i32;
let sh = src.height as i32;
let x0 = x.max(0);
let y0 = y.max(0);
let x1 = (x + sw).min(dw);
let y1 = (y + sh).min(dh);
if x0 >= x1 || y0 >= y1 {
return Ok(());
}
let op = (opacity.clamp(0.0, 1.0) * 255.0) as u16;
if op == 0 {
return Ok(());
}
let dst_stride = dst.width as usize;
let src_stride = src.width as usize;
for dy in y0..y1 {
let sy = (dy - y) as usize;
for dx in x0..x1 {
let sx = (dx - x) as usize;
let si = (sy * src_stride + sx) * 4;
let di = (dy as usize * dst_stride + dx as usize) * 4;
let sa = div255(src.data[si] as u16 * op);
if sa == 0 {
continue;
}
if sa >= 255 && op >= 255 {
dst.data[di..di + 4].copy_from_slice(&src.data[si..si + 4]);
continue;
}
let inv_sa = 255u16 - sa;
dst.data[di] = div255(sa * 255 + dst.data[di] as u16 * inv_sa).min(255) as u8;
dst.data[di + 1] =
div255(src.data[si + 1] as u16 * sa + dst.data[di + 1] as u16 * inv_sa) as u8;
dst.data[di + 2] =
div255(src.data[si + 2] as u16 * sa + dst.data[di + 2] as u16 * inv_sa) as u8;
dst.data[di + 3] =
div255(src.data[si + 3] as u16 * sa + dst.data[di + 3] as u16 * inv_sa) as u8;
}
}
Ok(())
}
#[must_use = "this returns a Result that may contain an error"]
pub fn fill_checkerboard(
buf: &mut PixelBuffer,
block_size: u32,
color_a: [u8; 4],
color_b: [u8; 4],
) -> Result<(), RangaError> {
validate_rgba8("fill_checkerboard", buf)?;
let bs = block_size.max(1) as usize;
for y in 0..buf.height as usize {
for x in 0..buf.width as usize {
let i = (y * buf.width as usize + x) * 4;
let color = if ((x / bs) + (y / bs)).is_multiple_of(2) {
&color_a
} else {
&color_b
};
buf.data[i..i + 4].copy_from_slice(color);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn premultiply_unpremultiply_roundtrip() {
let mut buf = PixelBuffer::new(vec![200, 100, 50, 128], 1, 1, PixelFormat::Rgba8).unwrap();
let original = buf.data.clone();
premultiply_alpha(&mut buf).unwrap();
assert_ne!(buf.data[0..3], original[0..3]);
unpremultiply_alpha(&mut buf).unwrap();
for (i, (&got, &exp)) in buf.data.iter().zip(original.iter()).enumerate().take(3) {
assert!((got as i16 - exp as i16).unsigned_abs() <= 1, "channel {i}");
}
}
#[test]
fn premultiply_fully_opaque_unchanged() {
let mut buf = PixelBuffer::new(vec![200, 100, 50, 255], 1, 1, PixelFormat::Rgba8).unwrap();
let original = buf.data.clone();
premultiply_alpha(&mut buf).unwrap();
assert_eq!(buf.data, original);
}
#[test]
fn premultiply_fully_transparent_zeroes() {
let mut buf = PixelBuffer::new(vec![200, 100, 50, 0], 1, 1, PixelFormat::Rgba8).unwrap();
premultiply_alpha(&mut buf).unwrap();
assert_eq!(buf.data[0], 0);
assert_eq!(buf.data[1], 0);
assert_eq!(buf.data[2], 0);
}
#[test]
fn apply_mask_half_alpha() {
let mut buf = PixelBuffer::new(vec![255, 0, 0, 255], 1, 1, PixelFormat::Rgba8).unwrap();
let mask = PixelBuffer::new(vec![128, 128, 128, 255], 1, 1, PixelFormat::Rgba8).unwrap();
apply_mask(&mut buf, &mask).unwrap();
assert_eq!(buf.data[3], 128);
}
#[test]
fn dissolve_endpoints() {
let a = PixelBuffer::new(vec![255, 0, 0, 255], 1, 1, PixelFormat::Rgba8).unwrap();
let b = PixelBuffer::new(vec![0, 255, 0, 255], 1, 1, PixelFormat::Rgba8).unwrap();
let at_0 = dissolve(&a, &b, 0.0).unwrap();
let at_1 = dissolve(&a, &b, 1.0).unwrap();
assert_eq!(at_0.data, a.data);
assert_eq!(at_1.data, b.data);
}
#[test]
fn dissolve_midpoint() {
let a = PixelBuffer::new(vec![200, 0, 0, 255], 1, 1, PixelFormat::Rgba8).unwrap();
let b = PixelBuffer::new(vec![0, 200, 0, 255], 1, 1, PixelFormat::Rgba8).unwrap();
let mid = dissolve(&a, &b, 0.5).unwrap();
assert!((mid.data[0] as i16 - 100).abs() <= 1);
assert!((mid.data[1] as i16 - 100).abs() <= 1);
}
#[test]
fn fade_zero_is_black() {
let mut buf = PixelBuffer::new(vec![200, 200, 200, 255], 1, 1, PixelFormat::Rgba8).unwrap();
fade(&mut buf, 0.0).unwrap();
assert_eq!(buf.data[0], 0);
assert_eq!(buf.data[3], 255); }
#[test]
fn wipe_half() {
let a = PixelBuffer::new(vec![255; 4 * 4 * 4], 4, 4, PixelFormat::Rgba8).unwrap();
let b = PixelBuffer::new(vec![0; 4 * 4 * 4], 4, 4, PixelFormat::Rgba8).unwrap();
let result = wipe(&a, &b, 0.5).unwrap();
assert_eq!(result.data[0], 0); assert_eq!(result.data[3 * 4], 255); }
#[test]
fn fill_solid_works() {
let mut buf = PixelBuffer::zeroed(2, 2, PixelFormat::Rgba8);
fill_solid(&mut buf, [255, 128, 64, 255]).unwrap();
assert_eq!(buf.data[0], 255);
assert_eq!(buf.data[5], 128); }
#[test]
fn gradient_endpoints() {
let mut buf = PixelBuffer::zeroed(10, 1, PixelFormat::Rgba8);
gradient_linear(&mut buf, [255, 0, 0, 255], [0, 0, 255, 255]).unwrap();
assert!(buf.data[0] > 200); assert!(buf.data[9 * 4 + 2] > 200); }
#[test]
fn composite_at_basic() {
let mut dst = PixelBuffer::zeroed(10, 10, PixelFormat::Rgba8);
let src = PixelBuffer::new([255, 0, 0, 255].repeat(4), 2, 2, PixelFormat::Rgba8).unwrap();
composite_at(&src, &mut dst, 3, 3, 1.0).unwrap();
let i = (3 * 10 + 3) * 4;
assert!(dst.data[i] > 200); }
#[test]
fn composite_at_clipped() {
let mut dst = PixelBuffer::zeroed(10, 10, PixelFormat::Rgba8);
let src = PixelBuffer::new([255, 0, 0, 255].repeat(4), 2, 2, PixelFormat::Rgba8).unwrap();
composite_at(&src, &mut dst, 9, 9, 1.0).unwrap();
let i = (9 * 10 + 9) * 4;
assert!(dst.data[i] > 200); }
#[test]
fn composite_at_negative_pos() {
let mut dst = PixelBuffer::zeroed(10, 10, PixelFormat::Rgba8);
let src = PixelBuffer::new([255, 0, 0, 255].repeat(4), 2, 2, PixelFormat::Rgba8).unwrap();
composite_at(&src, &mut dst, -1, -1, 1.0).unwrap();
assert!(dst.data[0] > 200);
}
#[test]
fn composite_at_zero_opacity() {
let mut dst = PixelBuffer::zeroed(10, 10, PixelFormat::Rgba8);
let src = PixelBuffer::new([255, 0, 0, 255].repeat(4), 2, 2, PixelFormat::Rgba8).unwrap();
composite_at(&src, &mut dst, 0, 0, 0.0).unwrap();
assert_eq!(dst.data[0], 0); }
#[test]
fn gradient_angled_vertical() {
let mut buf = PixelBuffer::zeroed(10, 10, PixelFormat::Rgba8);
gradient_linear_angled(&mut buf, [255, 0, 0, 255], [0, 0, 255, 255], 90.0).unwrap();
assert!(buf.data[0] > 200);
assert!(buf.data[(9 * 10) * 4 + 2] > 200);
}
#[test]
fn gradient_radial_center() {
let mut buf = PixelBuffer::zeroed(21, 21, PixelFormat::Rgba8);
gradient_radial(
&mut buf,
(10.0, 10.0),
10.0,
[255, 0, 0, 255],
[0, 0, 255, 255],
)
.unwrap();
let ci = (10 * 21 + 10) * 4;
assert!(buf.data[ci] > 250, "center R={}", buf.data[ci]);
assert!(buf.data[ci + 2] < 5, "center B={}", buf.data[ci + 2]);
}
#[test]
fn gradient_radial_edge() {
let mut buf = PixelBuffer::zeroed(21, 21, PixelFormat::Rgba8);
gradient_radial(
&mut buf,
(10.0, 10.0),
10.0,
[255, 0, 0, 255],
[0, 0, 255, 255],
)
.unwrap();
let ei = (10 * 21 + 20) * 4;
assert!(buf.data[ei + 2] > 200, "edge B={}", buf.data[ei + 2]);
}
#[test]
fn checkerboard_alternates() {
let mut buf = PixelBuffer::zeroed(4, 4, PixelFormat::Rgba8);
fill_checkerboard(&mut buf, 2, [100, 100, 100, 255], [200, 200, 200, 255]).unwrap();
assert_eq!(buf.data[0], 100); assert_eq!(buf.data[2 * 4], 200); }
}