#[cfg(feature = "fft-blur")]
use oxifft::{convolve_mode, ConvMode};
pub const FFT_BLUR_MIN_RADIUS: f32 = 32.0;
#[inline]
pub fn should_use_fft_blur(blur_radius: f32) -> bool {
blur_radius >= FFT_BLUR_MIN_RADIUS
}
#[cfg(feature = "fft-blur")]
pub fn gaussian_blur_alpha_fft(alpha: &mut [f32], width: usize, height: usize, kernel: &[f32]) {
debug_assert_eq!(alpha.len(), width * height);
if width == 0 || height == 0 || kernel.is_empty() {
return;
}
let full_kernel = build_full_kernel(kernel);
let mut tmp = vec![0.0f32; width * height];
for y in 0..height {
let row = &alpha[y * width..(y + 1) * width];
let result = convolve_mode::<f32>(row, &full_kernel, ConvMode::Same);
let out_row = &mut tmp[y * width..(y + 1) * width];
let copy_len = result.len().min(width);
out_row[..copy_len].copy_from_slice(&result[..copy_len]);
}
let mut col_buf = vec![0.0f32; height];
for x in 0..width {
for y in 0..height {
col_buf[y] = tmp[y * width + x];
}
let result = convolve_mode::<f32>(&col_buf, &full_kernel, ConvMode::Same);
for y in 0..height {
let v = if y < result.len() { result[y] } else { 0.0 };
alpha[y * width + x] = v.clamp(0.0, 1.0);
}
}
}
#[cfg(not(feature = "fft-blur"))]
#[allow(unused_variables)]
pub fn gaussian_blur_alpha_fft(alpha: &mut [f32], width: usize, height: usize, kernel: &[f32]) {
}
#[cfg_attr(not(feature = "fft-blur"), allow(dead_code))]
fn build_full_kernel(half: &[f32]) -> Vec<f32> {
if half.is_empty() {
return Vec::new();
}
let r = half.len() - 1; let mut full = Vec::with_capacity(2 * r + 1);
for i in (1..=r).rev() {
full.push(half[i]);
}
full.extend_from_slice(half);
full
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "fft-blur")]
use crate::shadow::gaussian_kernel;
#[test]
fn should_use_fft_blur_threshold() {
assert!(!should_use_fft_blur(16.0));
assert!(!should_use_fft_blur(31.9));
assert!(should_use_fft_blur(32.0));
assert!(should_use_fft_blur(64.0));
}
#[test]
fn build_full_kernel_symmetric() {
let half = vec![1.0f32, 0.5, 0.25];
let full = build_full_kernel(&half);
assert_eq!(full.len(), 5);
assert!((full[0] - 0.25).abs() < 1e-6);
assert!((full[2] - 1.0).abs() < 1e-6);
assert!((full[4] - 0.25).abs() < 1e-6);
}
#[test]
fn build_full_kernel_single_tap() {
let half = vec![1.0f32];
let full = build_full_kernel(&half);
assert_eq!(full.len(), 1);
assert!((full[0] - 1.0).abs() < 1e-6);
}
#[test]
fn build_full_kernel_empty() {
let full = build_full_kernel(&[]);
assert!(full.is_empty());
}
#[cfg(feature = "fft-blur")]
#[test]
fn fft_blur_spreads_energy() {
let w = 15;
let h = 15;
let mut alpha = vec![0.0f32; w * h];
alpha[7 * w + 7] = 1.0; let kernel = gaussian_kernel(4.0);
gaussian_blur_alpha_fft(&mut alpha, w, h, &kernel);
assert!(alpha[7 * w + 8] > 0.0, "right neighbour should be non-zero");
assert!(alpha[6 * w + 7] > 0.0, "top neighbour should be non-zero");
assert!(alpha[7 * w + 7] >= alpha[7 * w + 8]);
}
#[cfg(feature = "fft-blur")]
#[test]
fn fft_blur_matches_direct_approximately() {
use crate::shadow::gaussian_blur_alpha;
let w = 17;
let h = 17;
let init = |buf: &mut Vec<f32>| {
*buf = vec![0.0f32; w * h];
buf[8 * w + 8] = 1.0; };
let kernel = gaussian_kernel(6.0);
let mut direct = Vec::new();
init(&mut direct);
gaussian_blur_alpha(&mut direct, w, h, &kernel);
let mut fft = Vec::new();
init(&mut fft);
gaussian_blur_alpha_fft(&mut fft, w, h, &kernel);
for (i, (&d, &f)) in direct.iter().zip(fft.iter()).enumerate() {
assert!(
(d - f).abs() < 0.02,
"pixel {i}: direct={d:.4} fft={f:.4} differ by more than 2%"
);
}
}
#[cfg(feature = "fft-blur")]
#[test]
fn fft_blur_empty_alpha_noop() {
let kernel = gaussian_kernel(4.0);
let mut alpha: Vec<f32> = Vec::new();
gaussian_blur_alpha_fft(&mut alpha, 0, 0, &kernel);
}
}