#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
#![allow(dead_code)]
use crate::simd_interp::separable_filter_pass_simd;
#[must_use]
pub fn cubic_weight(t: f64) -> f64 {
let at = t.abs();
if at < 1.0 {
(9.0 * at * at * at - 15.0 * at * at + 6.0) / 6.0
} else if at < 2.0 {
(-3.0 * at * at * at + 15.0 * at * at - 24.0 * at + 12.0) / 6.0
} else {
0.0
}
}
#[must_use]
pub fn bicubic_sample(src: &[u8], x: f64, y: f64, width: usize, height: usize) -> [u8; 3] {
let x0 = x.floor() as i64;
let y0 = y.floor() as i64;
let fx = x - x0 as f64;
let fy = y - y0 as f64;
let w = width as i64;
let h = height as i64;
let clamp_x = |v: i64| v.clamp(0, w - 1) as usize;
let clamp_y = |v: i64| v.clamp(0, h - 1) as usize;
let mut channels = [0.0f64; 3];
for ky in -1i64..=2 {
let wy = cubic_weight(fy - ky as f64);
if wy.abs() < 1e-10 {
continue;
}
let sy = clamp_y(y0 + ky);
for kx in -1i64..=2 {
let wx = cubic_weight(fx - kx as f64);
let weight = wx * wy;
if weight.abs() < 1e-10 {
continue;
}
let sx = clamp_x(x0 + kx);
let base = (sy * width + sx) * 3;
for c in 0..3 {
channels[c] += src[base + c] as f64 * weight;
}
}
}
[
channels[0].round().clamp(0.0, 255.0) as u8,
channels[1].round().clamp(0.0, 255.0) as u8,
channels[2].round().clamp(0.0, 255.0) as u8,
]
}
fn build_bicubic_filter_weights(src_len: usize, dst_len: usize) -> (Vec<Vec<f32>>, Vec<usize>) {
let scale = src_len as f64 / dst_len as f64;
let mut weights: Vec<Vec<f32>> = Vec::with_capacity(dst_len);
let mut offsets: Vec<usize> = Vec::with_capacity(dst_len);
for d in 0..dst_len {
let center = (d as f64 + 0.5) * scale - 0.5;
let x0 = center.floor() as i64;
let start = x0 - 1;
let end = x0 + 2;
let src_max = (src_len as i64) - 1;
let clamped_start = start.clamp(0, src_max);
let clamped_end = end.clamp(0, src_max);
let span = (clamped_end - clamped_start + 1) as usize;
let mut acc = vec![0.0f32; span];
for k in start..=end {
let w = cubic_weight(center - k as f64) as f32;
let idx = k.clamp(0, src_max) - clamped_start;
acc[idx as usize] += w;
}
let sum: f32 = acc.iter().sum();
if sum.abs() > 1e-8 {
for w in &mut acc {
*w /= sum;
}
}
offsets.push(clamped_start as usize);
weights.push(acc);
}
(weights, offsets)
}
fn bicubic_filter_row_simd(src_row: &[f32], dst_len: usize) -> Vec<f32> {
let src_len = src_row.len();
if src_len == 0 || dst_len == 0 {
return Vec::new();
}
let (weights, offsets) = build_bicubic_filter_weights(src_len, dst_len);
separable_filter_pass_simd(src_row, &weights, &offsets)
}
#[must_use]
pub fn bicubic_resize(
src: &[u8],
src_w: usize,
src_h: usize,
dst_w: usize,
dst_h: usize,
) -> Vec<u8> {
if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
return Vec::new();
}
let (h_weights, h_offsets) = build_bicubic_filter_weights(src_w, dst_w);
let mut h_pass = vec![0.0f32; src_h * dst_w * 3];
for y in 0..src_h {
for c in 0..3usize {
let row_f32: Vec<f32> = (0..src_w)
.map(|x| src[y * src_w * 3 + x * 3 + c] as f32)
.collect();
let filtered = separable_filter_pass_simd(&row_f32, &h_weights, &h_offsets);
for (x, val) in filtered.into_iter().enumerate() {
h_pass[y * dst_w * 3 + x * 3 + c] = val;
}
}
}
let (v_weights, v_offsets) = build_bicubic_filter_weights(src_h, dst_h);
let mut dst = vec![0u8; dst_w * dst_h * 3];
for x in 0..dst_w {
for c in 0..3usize {
let col_f32: Vec<f32> = (0..src_h)
.map(|y| h_pass[y * dst_w * 3 + x * 3 + c])
.collect();
let filtered = separable_filter_pass_simd(&col_f32, &v_weights, &v_offsets);
for (y, val) in filtered.into_iter().enumerate() {
let clamped = val.round().clamp(0.0, 255.0) as u8;
dst[y * dst_w * 3 + x * 3 + c] = clamped;
}
}
}
dst
}
#[must_use]
pub fn bicubic_resize_scalar(
src: &[u8],
src_w: usize,
src_h: usize,
dst_w: usize,
dst_h: usize,
) -> Vec<u8> {
if src_w == 0 || src_h == 0 || dst_w == 0 || dst_h == 0 {
return Vec::new();
}
let scale_x = src_w as f64 / dst_w as f64;
let scale_y = src_h as f64 / dst_h as f64;
let mut dst = vec![0u8; dst_w * dst_h * 3];
for dy in 0..dst_h {
let sy = (dy as f64 + 0.5) * scale_y - 0.5;
for dx in 0..dst_w {
let sx = (dx as f64 + 0.5) * scale_x - 0.5;
let pixel = bicubic_sample(src, sx, sy, src_w, src_h);
let base = (dy * dst_w + dx) * 3;
dst[base] = pixel[0];
dst[base + 1] = pixel[1];
dst[base + 2] = pixel[2];
}
}
dst
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cubic_weight_at_zero() {
assert!((cubic_weight(0.0) - 1.0).abs() < 1e-10);
}
#[test]
fn test_cubic_weight_at_two() {
assert!((cubic_weight(2.0)).abs() < 1e-10);
assert!((cubic_weight(-2.0)).abs() < 1e-10);
assert!((cubic_weight(3.0)).abs() < 1e-10);
}
#[test]
fn test_cubic_weight_symmetric() {
for i in 1..20 {
let t = i as f64 * 0.1;
let diff = (cubic_weight(t) - cubic_weight(-t)).abs();
assert!(diff < 1e-10, "asymmetric at t={t}: diff={diff}");
}
}
#[test]
fn test_cubic_weight_continuous_at_one() {
let left = cubic_weight(1.0 - 1e-9);
let right = cubic_weight(1.0 + 1e-9);
assert!(
(left - right).abs() < 1e-4,
"discontinuity at 1: left={left} right={right}"
);
}
#[test]
fn test_bicubic_sample_exact_pixel() {
let src = vec![
255u8, 0, 0, 0, 255, 0, 0, 0, 255, 128, 128, 0,
]; let p = bicubic_sample(&src, 0.0, 0.0, 2, 2);
assert!(
p[0] > p[1] && p[0] > p[2],
"expected reddish pixel, got {:?}",
p
);
}
#[test]
fn test_bicubic_sample_clamped_oob() {
let src = vec![200u8, 100, 50, 80, 40, 20, 160, 80, 40, 64, 32, 16];
let _ = bicubic_sample(&src, -5.0, -5.0, 2, 2);
let _ = bicubic_sample(&src, 100.0, 100.0, 2, 2);
}
#[test]
fn test_bicubic_resize_returns_correct_size() {
let src = vec![128u8; 4 * 4 * 3]; let dst = bicubic_resize(&src, 4, 4, 8, 8);
assert_eq!(dst.len(), 8 * 8 * 3);
}
#[test]
fn test_bicubic_resize_uniform_image() {
let src = vec![100u8; 4 * 4 * 3];
let dst = bicubic_resize(&src, 4, 4, 8, 8);
for (i, &v) in dst.iter().enumerate() {
assert!((v as i32 - 100).abs() <= 2, "pixel {i}: got {v}");
}
}
#[test]
fn test_bicubic_resize_same_size() {
let src: Vec<u8> = (0..27).map(|i| (i * 9) as u8).collect();
let dst = bicubic_resize(&src, 3, 3, 3, 3);
assert_eq!(dst.len(), 27);
}
#[test]
fn test_bicubic_resize_zero_dimension_returns_empty() {
let src = vec![0u8; 16 * 3];
assert!(bicubic_resize(&src, 0, 4, 8, 8).is_empty());
assert!(bicubic_resize(&src, 4, 4, 0, 8).is_empty());
}
#[test]
fn test_bicubic_resize_downscale_2x() {
let src = vec![200u8; 8 * 8 * 3];
let dst = bicubic_resize(&src, 8, 8, 4, 4);
assert_eq!(dst.len(), 4 * 4 * 3);
for &v in &dst {
assert!((v as i32 - 200).abs() <= 3, "got {v}");
}
}
#[test]
fn test_bicubic_resize_single_pixel() {
let src = vec![42u8, 84, 126];
let dst = bicubic_resize(&src, 1, 1, 3, 3);
assert_eq!(dst.len(), 27);
for chunk in dst.chunks(3) {
assert_eq!(chunk[0], 42);
assert_eq!(chunk[1], 84);
assert_eq!(chunk[2], 126);
}
}
#[test]
fn test_bicubic_resize_channels_independent() {
let src: Vec<u8> = (0..16).flat_map(|_| [255u8, 0, 0]).collect();
let dst = bicubic_resize(&src, 4, 4, 4, 4);
for chunk in dst.chunks(3) {
assert!(chunk[0] > 200, "R should be high, got {}", chunk[0]);
}
}
#[test]
fn test_bicubic_filter_row_simd_output_len() {
let src: Vec<f32> = (0..8).map(|i| i as f32 * 10.0).collect();
let out = bicubic_filter_row_simd(&src, 4);
assert_eq!(out.len(), 4);
for &v in &out {
assert!(v >= -10.0 && v <= 90.0, "out of range: {v}");
}
}
#[test]
fn test_bicubic_filter_row_simd_upsample_len() {
let src: Vec<f32> = (0..8).map(|i| i as f32 * 5.0).collect();
let out = bicubic_filter_row_simd(&src, 16);
assert_eq!(out.len(), 16);
}
#[test]
fn test_build_bicubic_filter_weights_count() {
let (weights, offsets) = build_bicubic_filter_weights(8, 4);
assert_eq!(weights.len(), 4);
assert_eq!(offsets.len(), 4);
for w in &weights {
let sum: f32 = w.iter().sum();
assert!((sum - 1.0).abs() < 1e-5, "weights must sum to 1, got {sum}");
}
}
#[test]
fn test_bicubic_simd_vs_scalar_bitclose() {
let w = 100usize;
let h = 100usize;
let src: Vec<u8> = (0..w * h)
.flat_map(|i| {
let x = i % w;
let y = i / w;
[
((x * 255) / w) as u8,
((y * 255) / h) as u8,
(((x + y) * 255) / (w + h)) as u8,
]
})
.collect();
let dw = 50usize;
let dh = 50usize;
let simd_out = bicubic_resize(&src, w, h, dw, dh);
assert_eq!(simd_out.len(), dw * dh * 3);
assert_eq!(simd_out.len(), dw * dh * 3, "output size mismatch");
let scalar_out = bicubic_resize_scalar(&src, w, h, dw, dh);
assert_eq!(scalar_out.len(), dw * dh * 3);
let border = 4usize;
for y in border..dh.saturating_sub(border) {
for x in border..dw.saturating_sub(border) {
for c in 0..3 {
let i = (y * dw + x) * 3 + c;
let s = simd_out[i] as i32;
let r = scalar_out[i] as i32;
assert!(
(s - r).abs() <= 6,
"interior mismatch at ({x},{y}) ch{c}: simd={s}, scalar={r}"
);
}
}
}
}
}