use crate::filter::{ONE, Weights};
#[inline]
fn finish_u8(accumulator: i32) -> u8 {
let rounded = (accumulator + (ONE / 2)) >> 14;
rounded.clamp(0, 255) as u8
}
#[inline]
fn finish_u16(accumulator: f32) -> u16 {
let rounded = accumulator + 0.5;
if rounded <= 0.0 {
0
} else if rounded >= 65535.0 {
65535
} else {
rounded as u16
}
}
pub fn row_u8(input: &[u8], output: &mut [u8], channels: usize, weights: &Weights) {
match channels {
1 => row_u8_n::<1>(input, output, weights),
2 => row_u8_n::<2>(input, output, weights),
3 => row_u8_n::<3>(input, output, weights),
4 => row_u8_n::<4>(input, output, weights),
other => row_u8_general(input, output, other, weights),
}
}
fn row_u8_n<const C: usize>(input: &[u8], output: &mut [u8], weights: &Weights) {
for (out_index, run) in weights.runs().iter().enumerate() {
let coefficients = weights.quantized(run);
let first = run.start as usize * C;
let taps = run.len as usize;
let Some(window) = input.get(first..first + taps * C) else {
continue;
};
let Some(target) = output.get_mut(out_index * C..(out_index + 1) * C) else {
continue;
};
let mut accumulators = [0_i32; C];
for (pixel, &coefficient) in window.chunks_exact(C).zip(coefficients) {
for (slot, &sample) in accumulators.iter_mut().zip(pixel) {
*slot += i32::from(sample) * coefficient;
}
}
for (slot, &value) in target.iter_mut().zip(accumulators.iter()) {
*slot = finish_u8(value);
}
}
}
fn row_u8_general(input: &[u8], output: &mut [u8], channels: usize, weights: &Weights) {
for (out_index, run) in weights.runs().iter().enumerate() {
let coefficients = weights.quantized(run);
let first = run.start as usize * channels;
let taps = run.len as usize;
let Some(window) = input.get(first..first + taps * channels) else {
continue;
};
let Some(target) = output.get_mut(out_index * channels..(out_index + 1) * channels) else {
continue;
};
let mut accumulators = [0_i32; MAX_CHANNELS];
for (pixel, &coefficient) in window.chunks_exact(channels).zip(coefficients) {
for (slot, &sample) in accumulators.iter_mut().zip(pixel) {
*slot += i32::from(sample) * coefficient;
}
}
for (channel, slot) in target.iter_mut().enumerate() {
*slot = finish_u8(accumulators.get(channel).copied().unwrap_or(0));
}
}
}
pub fn row_u16(input: &[u16], output: &mut [u16], channels: usize, weights: &Weights) {
for (out_index, run) in weights.runs().iter().enumerate() {
let coefficients = weights.exact(run);
let first = run.start as usize * channels;
let taps = run.len as usize;
let Some(window) = input.get(first..first + taps * channels) else {
continue;
};
let Some(target) = output.get_mut(out_index * channels..(out_index + 1) * channels) else {
continue;
};
let mut accumulators = [0.0_f32; MAX_CHANNELS];
for (tap, &coefficient) in coefficients.iter().enumerate() {
let Some(pixel) = window.get(tap * channels..(tap + 1) * channels) else {
continue;
};
for (channel, &sample) in pixel.iter().enumerate() {
if let Some(slot) = accumulators.get_mut(channel) {
*slot += f32::from(sample) * coefficient;
}
}
}
for (channel, slot) in target.iter_mut().enumerate() {
*slot = finish_u16(accumulators.get(channel).copied().unwrap_or(0.0));
}
}
}
pub fn row_f32(input: &[f32], output: &mut [f32], channels: usize, weights: &Weights) {
for (out_index, run) in weights.runs().iter().enumerate() {
let coefficients = weights.exact(run);
let first = run.start as usize * channels;
let taps = run.len as usize;
let Some(window) = input.get(first..first + taps * channels) else {
continue;
};
let Some(target) = output.get_mut(out_index * channels..(out_index + 1) * channels) else {
continue;
};
let mut accumulators = [0.0_f32; MAX_CHANNELS];
for (tap, &coefficient) in coefficients.iter().enumerate() {
let Some(pixel) = window.get(tap * channels..(tap + 1) * channels) else {
continue;
};
for (channel, &sample) in pixel.iter().enumerate() {
if let Some(slot) = accumulators.get_mut(channel) {
*slot += sample * coefficient;
}
}
}
for (channel, slot) in target.iter_mut().enumerate() {
*slot = accumulators.get(channel).copied().unwrap_or(0.0);
}
}
}
pub const MAX_CHANNELS: usize = 4;
pub fn column_u8(
source: &[u8],
row_len: usize,
start: usize,
coefficients: &[i32],
accumulator: &mut [i32],
out: &mut [u8],
) {
accumulator.fill(0);
for (tap, &coefficient) in coefficients.iter().enumerate() {
let at = (start + tap) * row_len;
let Some(row) = source.get(at..at + row_len) else {
continue;
};
for (slot, &sample) in accumulator.iter_mut().zip(row) {
*slot += i32::from(sample) * coefficient;
}
}
for (slot, &value) in out.iter_mut().zip(accumulator.iter()) {
*slot = finish_u8(value);
}
}
pub fn column_u16(
source: &[u16],
row_len: usize,
start: usize,
coefficients: &[f32],
accumulator: &mut [f32],
out: &mut [u16],
) {
accumulator.fill(0.0);
for (tap, &coefficient) in coefficients.iter().enumerate() {
let at = (start + tap) * row_len;
let Some(row) = source.get(at..at + row_len) else {
continue;
};
for (slot, &sample) in accumulator.iter_mut().zip(row) {
*slot += f32::from(sample) * coefficient;
}
}
for (slot, &value) in out.iter_mut().zip(accumulator.iter()) {
*slot = finish_u16(value);
}
}
pub fn column_f32(
source: &[f32],
row_len: usize,
start: usize,
coefficients: &[f32],
accumulator: &mut [f32],
out: &mut [f32],
) {
accumulator.fill(0.0);
for (tap, &coefficient) in coefficients.iter().enumerate() {
let at = (start + tap) * row_len;
let Some(row) = source.get(at..at + row_len) else {
continue;
};
for (slot, &sample) in accumulator.iter_mut().zip(row) {
*slot += sample * coefficient;
}
}
out.copy_from_slice(accumulator);
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic,
reason = "tests operate on known-good values and assert shapes directly"
)]
mod tests {
use super::*;
use crate::filter::Filter;
const ALL: [Filter; 7] = [
Filter::Nearest,
Filter::Box,
Filter::Bilinear,
Filter::CatmullRom,
Filter::Mitchell,
Filter::Lanczos2,
Filter::Lanczos3,
];
#[test]
fn a_constant_row_stays_constant_at_every_scale() {
for filter in ALL {
for (input_len, output_len) in [(64, 32), (32, 64), (64, 64), (100, 37), (7, 200)] {
for channels in 1..=4 {
let weights = Weights::build(filter, input_len, output_len).unwrap();
let input = vec![200_u8; input_len as usize * channels];
let mut output = vec![0_u8; output_len as usize * channels];
row_u8(&input, &mut output, channels, &weights);
assert!(
output.iter().all(|&v| v == 200),
"{} {input_len}->{output_len}x{channels} did not stay flat: {:?}",
filter.as_str(),
&output[..output.len().min(8)]
);
}
}
}
}
#[test]
fn a_one_to_one_resize_is_the_identity() {
for filter in ALL {
let weights = Weights::build(filter, 64, 64).unwrap();
let input: Vec<u8> = (0..64_u32).map(|i| (i * 3 % 256) as u8).collect();
let mut output = vec![0_u8; 64];
row_u8(&input, &mut output, 1, &weights);
assert_eq!(output, input, "{} changed pixels at 1:1", filter.as_str());
}
}
#[test]
fn channels_are_resampled_independently() {
let weights = Weights::build(Filter::Bilinear, 8, 4).unwrap();
let mut input = vec![0_u8; 8 * 4];
for (pixel, chunk) in input.chunks_exact_mut(4).enumerate() {
chunk[0] = 255;
chunk[1] = 0;
chunk[2] = if pixel < 4 { 255 } else { 0 };
chunk[3] = 128;
}
let mut output = vec![0_u8; 4 * 4];
row_u8(&input, &mut output, 4, &weights);
for pixel in output.chunks_exact(4) {
assert_eq!(pixel[0], 255, "channel 0 should stay saturated");
assert_eq!(pixel[1], 0, "channel 1 should stay zero");
assert_eq!(pixel[3], 128, "channel 3 should stay constant");
}
}
#[test]
fn overshoot_is_clamped_not_wrapped() {
let weights = Weights::build(Filter::Lanczos3, 32, 30).unwrap();
let mut input = vec![0_u8; 32];
for slot in input.iter_mut().skip(16) {
*slot = 255;
}
let mut output = vec![0_u8; 30];
row_u8(&input, &mut output, 1, &weights);
assert_eq!(output.first().copied(), Some(0), "left of the edge");
assert_eq!(output.last().copied(), Some(255), "right of the edge");
}
#[test]
fn the_eight_bit_path_is_bit_identical_run_to_run() {
let weights = Weights::build(Filter::Lanczos3, 500, 137).unwrap();
let input: Vec<u8> = (0..500_u32 * 3).map(|i| (i * 31 % 251) as u8).collect();
let mut first = vec![0_u8; 137 * 3];
row_u8(&input, &mut first, 3, &weights);
for _ in 0..8 {
let mut again = vec![0_u8; 137 * 3];
row_u8(&input, &mut again, 3, &weights);
assert_eq!(again, first, "the 8-bit kernel is not deterministic");
}
}
#[test]
fn sixteen_bit_and_float_paths_track_the_eight_bit_one() {
let weights = Weights::build(Filter::CatmullRom, 64, 40).unwrap();
let eight: Vec<u8> = (0..64_u32).map(|i| (i * 4 % 256) as u8).collect();
let wide: Vec<u16> = eight.iter().map(|&v| u16::from(v) * 257).collect();
let floats: Vec<f32> = eight.iter().map(|&v| f32::from(v)).collect();
let mut out8 = vec![0_u8; 40];
let mut out16 = vec![0_u16; 40];
let mut outf = vec![0.0_f32; 40];
row_u8(&eight, &mut out8, 1, &weights);
row_u16(&wide, &mut out16, 1, &weights);
row_f32(&floats, &mut outf, 1, &weights);
for (index, &value) in out8.iter().enumerate() {
let from16 = (f32::from(out16[index]) / 257.0).round() as i32;
let fromf = outf[index].round() as i32;
assert!(
(i32::from(value) - from16).abs() <= 1,
"16-bit differs at {index}: {value} vs {from16}"
);
assert!(
(i32::from(value) - fromf).abs() <= 1,
"float differs at {index}: {value} vs {fromf}"
);
}
}
#[test]
fn float_output_is_not_clamped() {
let weights = Weights::build(Filter::Bilinear, 4, 2).unwrap();
for value in [-5.0_f32, 900.0] {
let input = vec![value; 4];
let mut output = vec![0.0_f32; 2];
row_f32(&input, &mut output, 1, &weights);
for &got in &output {
assert!(
(got - value).abs() < 1e-3,
"{value} came back as {got}, so the float path is clamping"
);
}
}
}
#[test]
fn a_single_sample_input_is_not_a_panic() {
for filter in ALL {
let weights = Weights::build(filter, 1, 16).unwrap();
let input = vec![77_u8];
let mut output = vec![0_u8; 16];
row_u8(&input, &mut output, 1, &weights);
assert!(
output.iter().all(|&v| v == 77),
"{} on a 1-sample input gave {output:?}",
filter.as_str()
);
}
}
#[test]
fn a_mismatched_output_length_truncates_rather_than_panicking() {
let weights = Weights::build(Filter::Box, 8, 4).unwrap();
let input = vec![1_u8; 8];
let mut short = vec![0_u8; 2];
row_u8(&input, &mut short, 1, &weights);
let mut long = vec![0_u8; 16];
row_u8(&input, &mut long, 1, &weights);
}
}