pub fn gaussian_blur(buf: &mut [u8], width: usize, height: usize, radius: usize) {
if radius == 0 || width == 0 || height == 0 {
return;
}
let kernel = build_kernel(radius);
let mut temp = vec![0u8; buf.len()];
blur_horizontal(buf, &mut temp, width, height, &kernel, radius);
blur_vertical(&temp, buf, width, height, &kernel, radius);
}
fn build_kernel(radius: usize) -> Vec<f32> {
let sigma = (radius as f32) / 3.0;
let sigma2 = 2.0 * sigma * sigma;
let mut kernel = Vec::with_capacity(radius + 1);
let mut sum = 0.0f32;
for i in 0..=radius {
let w = (-(i as f32 * i as f32) / sigma2).exp();
kernel.push(w);
sum += if i == 0 { w } else { 2.0 * w };
}
for w in &mut kernel {
*w /= sum;
}
kernel
}
fn blur_horizontal(
src: &[u8],
dst: &mut [u8],
width: usize,
height: usize,
kernel: &[f32],
_radius: usize,
) {
for y in 0..height {
let row_off = y * width * 4;
for x in 0..width {
let mut r = 0.0f32;
let mut g = 0.0f32;
let mut b = 0.0f32;
let mut a = 0.0f32;
for (k, &w) in kernel.iter().enumerate() {
if k == 0 {
let off = row_off + x * 4;
r += src[off] as f32 * w;
g += src[off + 1] as f32 * w;
b += src[off + 2] as f32 * w;
a += src[off + 3] as f32 * w;
} else {
let lx = x.saturating_sub(k);
let off = row_off + lx * 4;
r += src[off] as f32 * w;
g += src[off + 1] as f32 * w;
b += src[off + 2] as f32 * w;
a += src[off + 3] as f32 * w;
let rx = (x + k).min(width - 1);
let off = row_off + rx * 4;
r += src[off] as f32 * w;
g += src[off + 1] as f32 * w;
b += src[off + 2] as f32 * w;
a += src[off + 3] as f32 * w;
}
}
let off = row_off + x * 4;
dst[off] = r.round().clamp(0.0, 255.0) as u8;
dst[off + 1] = g.round().clamp(0.0, 255.0) as u8;
dst[off + 2] = b.round().clamp(0.0, 255.0) as u8;
dst[off + 3] = a.round().clamp(0.0, 255.0) as u8;
}
}
}
fn blur_vertical(
src: &[u8],
dst: &mut [u8],
width: usize,
height: usize,
kernel: &[f32],
_radius: usize,
) {
for y in 0..height {
for x in 0..width {
let mut r = 0.0f32;
let mut g = 0.0f32;
let mut b = 0.0f32;
let mut a = 0.0f32;
for (k, &w) in kernel.iter().enumerate() {
if k == 0 {
let off = (y * width + x) * 4;
r += src[off] as f32 * w;
g += src[off + 1] as f32 * w;
b += src[off + 2] as f32 * w;
a += src[off + 3] as f32 * w;
} else {
let ty = y.saturating_sub(k);
let off = (ty * width + x) * 4;
r += src[off] as f32 * w;
g += src[off + 1] as f32 * w;
b += src[off + 2] as f32 * w;
a += src[off + 3] as f32 * w;
let by = (y + k).min(height - 1);
let off = (by * width + x) * 4;
r += src[off] as f32 * w;
g += src[off + 1] as f32 * w;
b += src[off + 2] as f32 * w;
a += src[off + 3] as f32 * w;
}
}
let off = (y * width + x) * 4;
dst[off] = r.round().clamp(0.0, 255.0) as u8;
dst[off + 1] = g.round().clamp(0.0, 255.0) as u8;
dst[off + 2] = b.round().clamp(0.0, 255.0) as u8;
dst[off + 3] = a.round().clamp(0.0, 255.0) as u8;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blur_preserves_solid_color() {
let mut buf = vec![128u8; 10 * 10 * 4];
gaussian_blur(&mut buf, 10, 10, 2);
assert_eq!(buf[0], 128);
assert_eq!(buf[40], 128);
}
#[test]
fn blur_smooths_edge() {
let mut buf = vec![0u8; 10 * 10 * 4];
let center = (5 * 10 + 5) * 4;
buf[center] = 255;
buf[center + 1] = 255;
buf[center + 2] = 255;
buf[center + 3] = 255;
gaussian_blur(&mut buf, 10, 10, 2);
assert!(buf[center] < 255);
assert!(buf[center] > 0);
let neighbor = (5 * 10 + 6) * 4;
assert!(buf[neighbor] > 0);
}
#[test]
fn kernel_sums_to_one() {
let kernel = build_kernel(5);
let sum: f32 = kernel[0] + 2.0 * kernel[1..].iter().sum::<f32>();
assert!((sum - 1.0).abs() < 0.01, "Kernel sum = {}", sum);
}
}