#[derive(Clone, Debug)]
pub struct DebandConfig {
pub enabled: bool,
pub threshold: f32,
pub range: u32,
pub seed: u32,
}
impl Default for DebandConfig {
fn default() -> Self {
Self {
enabled: true,
threshold: 64.0,
range: 15,
seed: 0x1337,
}
}
}
pub fn apply_deband(rgba: &[u8], width: usize, height: usize, config: &DebandConfig) -> Vec<u8> {
if !config.enabled || width == 0 || height == 0 {
return rgba.to_vec();
}
let mut output = vec![0u8; rgba.len()];
let range = config.range as i32;
for y in 0..height {
for x in 0..width {
let idx = (y * width + x) * 4;
let src = read_pixel(rgba, idx);
if src[3] == 0 {
write_pixel(&mut output, idx, src);
continue;
}
let (ox, oy) = sample_offset(config.seed, x as u32, y as u32, range);
let refs = sample_cross(rgba, width, height, x, y, ox, oy);
let blended = blend_deband(src, refs, config.threshold);
write_pixel(&mut output, idx, blended);
}
}
output
}
#[inline]
fn read_pixel(rgba: &[u8], idx: usize) -> [u8; 4] {
[rgba[idx], rgba[idx + 1], rgba[idx + 2], rgba[idx + 3]]
}
#[inline]
fn write_pixel(output: &mut [u8], idx: usize, pixel: [u8; 4]) {
output[idx..idx + 4].copy_from_slice(&pixel);
}
fn sample_offset(seed: u32, x: u32, y: u32, range: i32) -> (i32, i32) {
let hash = seed
.wrapping_mul(0x9E3779B9)
.wrapping_add(x.wrapping_mul(0x85EBCA6B))
.wrapping_add(y.wrapping_mul(0xC2B2AE35));
let ox = ((hash & 0xFFFF) as i32 % (range * 2 + 1)) - range;
let oy = (((hash >> 16) & 0xFFFF) as i32 % (range * 2 + 1)) - range;
(ox.max(1), oy.max(1)) }
fn sample_cross(
rgba: &[u8],
width: usize,
height: usize,
x: usize,
y: usize,
ox: i32,
oy: i32,
) -> [[u8; 4]; 4] {
let sample_at = |dx: i32, dy: i32| -> [u8; 4] {
let nx = (x as i32 + dx).clamp(0, width as i32 - 1) as usize;
let ny = (y as i32 + dy).clamp(0, height as i32 - 1) as usize;
let idx = (ny * width + nx) * 4;
read_pixel(rgba, idx)
};
[
sample_at(0, -oy), sample_at(0, oy), sample_at(-ox, 0), sample_at(ox, 0), ]
}
fn blend_deband(src: [u8; 4], refs: [[u8; 4]; 4], threshold: f32) -> [u8; 4] {
let mut result = [0u8; 4];
for c in 0..3 {
let s = src[c] as f32;
let r: [f32; 4] = [
refs[0][c] as f32,
refs[1][c] as f32,
refs[2][c] as f32,
refs[3][c] as f32,
];
let avg = (r[0] + r[1] + r[2] + r[3]) * 0.25;
let avg_dif = (avg - s).abs();
let max_dif = r.iter().map(|&v| (v - s).abs()).fold(0.0f32, f32::max);
let mid_dif_v = ((r[0] + r[1]) * 0.5 - s).abs(); let mid_dif_h = ((r[2] + r[3]) * 0.5 - s).abs();
let factor = compute_factor(avg_dif, max_dif, mid_dif_v, mid_dif_h, threshold);
let blended = s + (avg - s) * factor;
result[c] = blended.clamp(0.0, 255.0) as u8;
}
result[3] = src[3];
result
}
#[inline]
fn compute_factor(avg_dif: f32, max_dif: f32, mid_v: f32, mid_h: f32, thresh: f32) -> f32 {
let saturate = |x: f32| x.clamp(0.0, 1.0);
let t1 = thresh;
let t2 = thresh * 0.75;
let f1 = saturate(3.0 * (1.0 - avg_dif / t1));
let f2 = saturate(3.0 * (1.0 - max_dif / t1));
let f3 = saturate(3.0 * (1.0 - mid_v / t2));
let f4 = saturate(3.0 * (1.0 - mid_h / t2));
(f1 * f2 * f3 * f4).powf(0.1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = DebandConfig::default();
assert!(config.enabled);
assert_eq!(config.threshold, 64.0);
assert_eq!(config.range, 15);
}
#[test]
fn test_disabled_passthrough() {
let rgba = vec![255, 0, 0, 255, 0, 255, 0, 255];
let config = DebandConfig {
enabled: false,
..Default::default()
};
let result = apply_deband(&rgba, 2, 1, &config);
assert_eq!(result, rgba);
}
#[test]
fn test_transparent_skip() {
let rgba = vec![255, 0, 0, 0]; let config = DebandConfig {
enabled: true,
..Default::default()
};
let result = apply_deband(&rgba, 1, 1, &config);
assert_eq!(result[3], 0); }
#[test]
fn test_sample_offset_deterministic() {
let (ox1, oy1) = sample_offset(0x1337, 10, 20, 15);
let (ox2, oy2) = sample_offset(0x1337, 10, 20, 15);
assert_eq!((ox1, oy1), (ox2, oy2)); }
}