#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_sign_loss)]
use rayon::prelude::*;
use thiserror::Error;
#[derive(Debug, Error, PartialEq)]
pub enum ParallelScaleError {
#[error("source buffer too small: expected {expected}, got {actual}")]
BufferTooSmall {
expected: usize,
actual: usize,
},
#[error("zero dimension in {dim}")]
ZeroDimension {
dim: &'static str,
},
}
#[inline]
fn bilinear_weight(t: f32) -> (f32, f32) {
(1.0 - t, t)
}
fn cubic_weights(t: f32) -> [f32; 4] {
let t2 = t * t;
let t3 = t2 * t;
let a = -0.5f32;
[
a * t3 - 2.0 * a * t2 + a * t,
(2.0 + a) * t3 - (3.0 + a) * t2 + 1.0,
-(2.0 + a) * t3 + (3.0 + 2.0 * a) * t2 - a * t,
-a * t3 + a * t2,
]
}
#[derive(Debug, Clone)]
pub struct ParallelScaler {
pub src_w: usize,
pub src_h: usize,
pub dst_w: usize,
pub dst_h: usize,
}
impl ParallelScaler {
pub fn new(src_w: usize, src_h: usize, dst_w: usize, dst_h: usize) -> Self {
Self {
src_w,
src_h,
dst_w,
dst_h,
}
}
fn validate(&self, src: &[f32]) -> Result<(), ParallelScaleError> {
if self.src_w == 0 || self.src_h == 0 {
return Err(ParallelScaleError::ZeroDimension { dim: "source" });
}
if self.dst_w == 0 || self.dst_h == 0 {
return Err(ParallelScaleError::ZeroDimension { dim: "destination" });
}
let expected = self.src_w * self.src_h;
if src.len() < expected {
return Err(ParallelScaleError::BufferTooSmall {
expected,
actual: src.len(),
});
}
Ok(())
}
pub fn scale_bilinear(&self, src: &[f32]) -> Result<Vec<f32>, ParallelScaleError> {
self.validate(src)?;
let sw = self.src_w;
let sh = self.src_h;
let dw = self.dst_w;
let dh = self.dst_h;
let x_scale = sw as f32 / dw as f32;
let y_scale = sh as f32 / dh as f32;
let mut output = vec![0.0f32; dw * dh];
output.par_chunks_mut(dw).enumerate().for_each(|(dy, row)| {
let src_y = dy as f32 * y_scale;
let y0 = (src_y.floor() as usize).min(sh - 1);
let y1 = (y0 + 1).min(sh - 1);
let (wy0, wy1) = bilinear_weight(src_y - src_y.floor());
for (dx, pixel) in row.iter_mut().enumerate() {
let src_x = dx as f32 * x_scale;
let x0 = (src_x.floor() as usize).min(sw - 1);
let x1 = (x0 + 1).min(sw - 1);
let (wx0, wx1) = bilinear_weight(src_x - src_x.floor());
let v00 = src[y0 * sw + x0];
let v10 = src[y0 * sw + x1];
let v01 = src[y1 * sw + x0];
let v11 = src[y1 * sw + x1];
*pixel = wy0 * (wx0 * v00 + wx1 * v10) + wy1 * (wx0 * v01 + wx1 * v11);
}
});
Ok(output)
}
pub fn scale_bicubic(&self, src: &[f32]) -> Result<Vec<f32>, ParallelScaleError> {
self.validate(src)?;
let sw = self.src_w;
let sh = self.src_h;
let dw = self.dst_w;
let dh = self.dst_h;
let x_scale = sw as f32 / dw as f32;
let y_scale = sh as f32 / dh as f32;
let mut output = vec![0.0f32; dw * dh];
output.par_chunks_mut(dw).enumerate().for_each(|(dy, row)| {
let src_y = dy as f32 * y_scale;
let iy = src_y.floor() as isize;
let fy = src_y - src_y.floor();
let wy = cubic_weights(fy);
for (dx, pixel) in row.iter_mut().enumerate() {
let src_x = dx as f32 * x_scale;
let ix = src_x.floor() as isize;
let fx = src_x - src_x.floor();
let wx = cubic_weights(fx);
let mut val = 0.0f32;
for j in 0..4i32 {
let sy = (iy + j as isize - 1).max(0).min(sh as isize - 1) as usize;
for i in 0..4i32 {
let sx = (ix + i as isize - 1).max(0).min(sw as isize - 1) as usize;
val += wy[j as usize] * wx[i as usize] * src[sy * sw + sx];
}
}
*pixel = val;
}
});
Ok(output)
}
pub fn scale_nearest(&self, src: &[f32]) -> Result<Vec<f32>, ParallelScaleError> {
self.validate(src)?;
let sw = self.src_w;
let sh = self.src_h;
let dw = self.dst_w;
let dh = self.dst_h;
let x_scale = sw as f32 / dw as f32;
let y_scale = sh as f32 / dh as f32;
let mut output = vec![0.0f32; dw * dh];
output.par_chunks_mut(dw).enumerate().for_each(|(dy, row)| {
let sy = ((dy as f32 * y_scale) as usize).min(sh - 1);
for (dx, pixel) in row.iter_mut().enumerate() {
let sx = ((dx as f32 * x_scale) as usize).min(sw - 1);
*pixel = src[sy * sw + sx];
}
});
Ok(output)
}
pub fn scale_factors(&self) -> (f32, f32) {
let x = self.src_w as f32 / self.dst_w.max(1) as f32;
let y = self.src_h as f32 / self.dst_h.max(1) as f32;
(x, y)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bilinear_upscale_output_size() {
let scaler = ParallelScaler::new(4, 4, 8, 8);
let src = vec![1.0f32; 16];
let dst = scaler.scale_bilinear(&src).expect("ok");
assert_eq!(dst.len(), 64);
}
#[test]
fn test_bilinear_uniform_preserves_value() {
let scaler = ParallelScaler::new(8, 8, 4, 4);
let src = vec![0.5f32; 64];
let dst = scaler.scale_bilinear(&src).expect("ok");
for &v in &dst {
assert!(
(v - 0.5).abs() < 1e-4,
"uniform value should be preserved, got {v}"
);
}
}
#[test]
fn test_bicubic_output_size() {
let scaler = ParallelScaler::new(6, 6, 12, 12);
let src = vec![0.3f32; 36];
let dst = scaler.scale_bicubic(&src).expect("ok");
assert_eq!(dst.len(), 144);
}
#[test]
fn test_bicubic_uniform_preserves_value() {
let scaler = ParallelScaler::new(8, 8, 4, 4);
let src = vec![0.7f32; 64];
let dst = scaler.scale_bicubic(&src).expect("ok");
for &v in &dst {
assert!(
(v - 0.7).abs() < 0.05,
"uniform bicubic should stay near 0.7, got {v}"
);
}
}
#[test]
fn test_nearest_output_size() {
let scaler = ParallelScaler::new(3, 3, 6, 6);
let src = vec![1.0f32; 9];
let dst = scaler.scale_nearest(&src).expect("ok");
assert_eq!(dst.len(), 36);
}
#[test]
fn test_nearest_preserves_exact_values() {
let scaler = ParallelScaler::new(2, 2, 4, 4);
let src = vec![10.0, 20.0, 30.0, 40.0];
let dst = scaler.scale_nearest(&src).expect("ok");
assert_eq!(dst[0], 10.0);
assert_eq!(dst[1], 10.0);
}
#[test]
fn test_zero_source_dimension_error() {
let scaler = ParallelScaler::new(0, 4, 4, 4);
let result = scaler.scale_bilinear(&[]);
assert!(matches!(
result,
Err(ParallelScaleError::ZeroDimension { dim: "source" })
));
}
#[test]
fn test_zero_dest_dimension_error() {
let scaler = ParallelScaler::new(4, 4, 0, 4);
let src = vec![1.0f32; 16];
let result = scaler.scale_bilinear(&src);
assert!(matches!(
result,
Err(ParallelScaleError::ZeroDimension { dim: "destination" })
));
}
#[test]
fn test_buffer_too_small_error() {
let scaler = ParallelScaler::new(4, 4, 2, 2);
let src = vec![1.0f32; 8]; let result = scaler.scale_bilinear(&src);
assert!(matches!(
result,
Err(ParallelScaleError::BufferTooSmall { .. })
));
}
#[test]
fn test_scale_factors() {
let scaler = ParallelScaler::new(100, 200, 50, 100);
let (sx, sy) = scaler.scale_factors();
assert!((sx - 2.0).abs() < 1e-6);
assert!((sy - 2.0).abs() < 1e-6);
}
#[test]
fn test_identity_scale_bilinear() {
let scaler = ParallelScaler::new(4, 4, 4, 4);
let src: Vec<f32> = (0..16).map(|i| i as f32).collect();
let dst = scaler.scale_bilinear(&src).expect("ok");
for (i, (&s, &d)) in src.iter().zip(dst.iter()).enumerate() {
assert!(
(s - d).abs() < 1e-3,
"identity bilinear mismatch at {i}: {s} vs {d}"
);
}
}
#[test]
fn test_identity_scale_nearest() {
let scaler = ParallelScaler::new(4, 4, 4, 4);
let src: Vec<f32> = (0..16).map(|i| i as f32).collect();
let dst = scaler.scale_nearest(&src).expect("ok");
for (i, (&s, &d)) in src.iter().zip(dst.iter()).enumerate() {
assert!(
(s - d).abs() < 1e-6,
"identity nearest mismatch at {i}: {s} vs {d}"
);
}
}
#[test]
fn test_debug_format() {
let scaler = ParallelScaler::new(10, 10, 20, 20);
let s = format!("{scaler:?}");
assert!(s.contains("ParallelScaler"));
}
#[test]
fn test_downscale_bilinear_output_values_in_range() {
let scaler = ParallelScaler::new(8, 8, 2, 2);
let src: Vec<f32> = (0..64).map(|i| i as f32 / 63.0).collect();
let dst = scaler.scale_bilinear(&src).expect("ok");
for &v in &dst {
assert!(v >= 0.0 && v <= 1.0, "value {v} out of [0,1] range");
}
}
#[test]
fn test_error_display() {
let e = ParallelScaleError::BufferTooSmall {
expected: 16,
actual: 8,
};
let s = e.to_string();
assert!(s.contains("16"));
assert!(s.contains("8"));
}
}