use super::*;
fn resize_rgba8_scalar(
src: &[u8],
src_w: usize,
src_h: usize,
dst_w: usize,
dst_h: usize,
filter: Filter,
) -> Vec<u8> {
if filter == Filter::Nearest {
let dst_len = dst_w * dst_h * CHANNELS;
return resize_nearest(src, src_w, src_h, dst_w, dst_h, dst_len).unwrap();
}
let src_pm = premultiply_rgba(src).unwrap();
let hc = precompute_coeffs(src_w, dst_w, filter).unwrap();
let vc = precompute_coeffs(src_h, dst_h, filter).unwrap();
let mut inter = vec![0u8; src_h * dst_w * CHANNELS];
let mut dst = vec![0u8; dst_w * dst_h * CHANNELS];
convolve_axis_scalar(&src_pm, src_w, src_h, &mut inter, dst_w, &hc);
convolve_vertical_scalar(&inter, dst_w, src_h, &mut dst, dst_h, &vc);
unpremultiply_rgba(&mut dst);
dst
}
fn make_src(w: usize, h: usize, seed: u32) -> Vec<u8> {
let mut s = seed.wrapping_add(1);
let mut v = Vec::with_capacity(w * h * CHANNELS);
for _ in 0..w * h * CHANNELS {
s = s.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
v.push((s >> 24) as u8);
}
v
}
#[test]
fn neon_matches_scalar_across_sizes_and_filters() {
let filters = [
Filter::Bilinear,
Filter::Bicubic,
Filter::Lanczos3,
Filter::Nearest,
];
let cases = [
(4usize, 4usize, 2usize, 2usize),
(3, 5, 7, 2),
(5, 3, 2, 8),
(8, 6, 4, 3),
(2, 2, 9, 9),
(5, 1, 2, 1),
(1, 5, 1, 2),
(7, 7, 7, 7),
(16, 9, 5, 11),
];
for (i, &(sw, sh, dw, dh)) in cases.iter().enumerate() {
let src = make_src(sw, sh, i as u32 * 7 + 1);
for &f in &filters {
let dispatched = resize_rgba8(&src, sw, sh, dw, dh, f).unwrap();
let scalar = resize_rgba8_scalar(&src, sw, sh, dw, dh, f);
assert_eq!(
dispatched, scalar,
"NEON-vs-scalar differential mismatch for {f:?} {sw}x{sh}->{dw}x{dh}"
);
}
}
}
#[test]
fn rejects_zero_dimensions() {
let src = [0u8; 4]; for (sw, sh, dw, dh) in [(0, 1, 2, 2), (1, 0, 2, 2), (1, 1, 0, 2), (1, 1, 2, 0)] {
let r = resize_rgba8(
&src[..sw.max(1) * sh.max(1) * CHANNELS],
sw,
sh,
dw,
dh,
Filter::Bilinear,
);
assert!(
matches!(r, Err(Error::OutOfRange(_))),
"zero dim {sw}x{sh}->{dw}x{dh} must be OutOfRange, got {r:?}"
);
}
}
#[test]
fn rejects_src_buffer_length_mismatch() {
let src = [0u8; 4]; let r = resize_rgba8(&src, 2, 2, 1, 1, Filter::Bilinear);
assert!(matches!(r, Err(Error::LengthMismatch(_))), "got {r:?}");
}
#[test]
fn rejects_overflowing_dst_product() {
let src = [0u8; 4];
let big = usize::MAX / 2 + 1;
let r = resize_rgba8(&src, 1, 1, big, big, Filter::Bilinear);
assert!(matches!(r, Err(Error::ArithmeticOverflow(_))), "got {r:?}");
}
#[test]
fn rejects_skinny_to_wide_oversized_intermediate() {
let src = vec![0u8; 131072 * CHANNELS];
for f in [Filter::Bilinear, Filter::Bicubic, Filter::Lanczos3] {
let r = resize_rgba8(&src, 1, 131072, 131072, 1, f);
match &r {
Err(Error::CapExceeded(p)) => {
assert_eq!(p.cap_name(), "MAX_DECODED_IMAGE_BYTES");
assert!(
p.context().contains("intermediate"),
"{f:?}: CapExceeded context should name the intermediate buffer, got: {}",
p.context()
);
}
_ => panic!("{f:?}: 1x131072->131072x1 must reject the ~68 GiB intermediate, got {r:?}"),
}
}
}
#[test]
fn wide_to_skinny_does_not_abort() {
let src = vec![0u8; 131072 * CHANNELS];
for f in [Filter::Bilinear, Filter::Bicubic, Filter::Lanczos3] {
let r = resize_rgba8(&src, 131072, 1, 1, 131072, f);
match r {
Ok(out) => assert_eq!(
out.len(),
131072 * CHANNELS,
"{f:?}: wide->skinny output must be exactly dst_w*dst_h*4"
),
Err(Error::CapExceeded(_)) | Err(Error::OutOfMemory) => {}
Err(other) => panic!("{f:?}: unexpected error {other:?}"),
}
}
}
#[test]
fn rejects_huge_intermediate_with_tiny_ends() {
let src = vec![7u8; 131072 * CHANNELS];
let r = resize_rgba8(&src, 1, 131072, 131072, 1, Filter::Bicubic);
assert!(
matches!(r, Err(Error::CapExceeded(_))),
"huge intermediate with tiny source+dest must be CapExceeded, got {r:?}"
);
}
#[test]
fn rejects_oversized_coefficient_table() {
let r = precompute_coeffs(1, 200_000_000, Filter::Bilinear);
assert!(
matches!(r, Err(Error::CapExceeded(_))),
"200M-wide coefficient table must exceed the 512 MiB cap (got Ok or wrong error)"
);
let src = vec![0u8; 4 * CHANNELS];
let r2 = resize_rgba8(&src, 1, 4, 200_000_000, 1, Filter::Bilinear);
assert!(
matches!(r2, Err(Error::CapExceeded(_))),
"resize to a 200M-wide target must be CapExceeded, got {r2:?}"
);
}
#[test]
fn checked_buffer_bytes_caps_and_overflows() {
assert_eq!(
checked_buffer_bytes(1024, 4, "ok").unwrap(),
4096,
"under-cap product must pass through"
);
assert_eq!(
checked_buffer_bytes(MAX_DECODED_IMAGE_BYTES, 1, "at-cap").unwrap(),
MAX_DECODED_IMAGE_BYTES,
"a buffer exactly at the cap must be allowed"
);
assert!(
matches!(
checked_buffer_bytes(MAX_DECODED_IMAGE_BYTES + 1, 1, "over"),
Err(Error::CapExceeded(_))
),
"one byte over the cap must be rejected"
);
assert!(
matches!(
checked_buffer_bytes(usize::MAX, 4, "overflow"),
Err(Error::ArithmeticOverflow(_))
),
"a product overflowing usize must be rejected (not wrap)"
);
}
#[test]
fn output_length_is_exact() {
let src = make_src(8, 6, 3);
for f in [
Filter::Nearest,
Filter::Bilinear,
Filter::Bicubic,
Filter::Lanczos3,
] {
let out = resize_rgba8(&src, 8, 6, 5, 4, f).unwrap();
assert_eq!(out.len(), 5 * 4 * CHANNELS, "filter {f:?} output length");
}
}
#[test]
fn constant_image_is_preserved() {
let mut src = Vec::with_capacity(6 * 6 * CHANNELS);
for _ in 0..6 * 6 {
src.extend_from_slice(&[123, 45, 200, 255]);
}
for f in [Filter::Bilinear, Filter::Bicubic, Filter::Lanczos3] {
for &(dw, dh) in &[(3usize, 3usize), (9, 9), (4, 7)] {
let out = resize_rgba8(&src, 6, 6, dw, dh, f).unwrap();
for px in out.chunks_exact(CHANNELS) {
assert_eq!(
px,
&[123, 45, 200, 255],
"constant must survive {f:?} -> {dw}x{dh}"
);
}
}
}
}
#[test]
fn muldiv255_matches_pil_and_is_opaque_identity() {
for c in 0u8..=255 {
assert_eq!(
muldiv255(c, 255),
c,
"MULDIV255({c}, 255) must equal {c} (opaque identity)"
);
assert_eq!(muldiv255(c, 0), 0, "MULDIV255({c}, 0) must be 0");
}
assert_eq!(muldiv255(255, 128), 128, "MULDIV255(255,128) hand-checked");
assert_eq!(muldiv255(200, 100), 78, "MULDIV255(200,100) hand-checked");
}
#[test]
fn premultiply_unpremultiply_opaque_is_identity() {
let src: Vec<u8> = (0u8..=255).flat_map(|c| [c, 255 - c, c / 2, 255]).collect();
let pm = premultiply_rgba(&src).unwrap();
assert_eq!(pm, src, "premultiply must be identity for opaque alpha");
let mut round = pm;
unpremultiply_rgba(&mut round);
assert_eq!(
round, src,
"unpremultiply must be identity for opaque alpha"
);
}
#[test]
fn premultiply_transparent_pixel_zeros_colour() {
let src = vec![255u8, 128, 64, 0]; let pm = premultiply_rgba(&src).unwrap();
assert_eq!(
pm,
vec![0, 0, 0, 0],
"premultiply of a transparent pixel must zero the colour channels"
);
let mut round = pm;
unpremultiply_rgba(&mut round);
assert_eq!(
round,
vec![0, 0, 0, 0],
"unpremultiply of a zero-alpha pixel keeps colour 0 (PIL passthrough)"
);
}
#[test]
fn unpremultiply_clips_and_divides_like_pil() {
let mut buf = vec![64u8, 0, 0, 128];
unpremultiply_rgba(&mut buf);
assert_eq!(
buf[0], 127,
"unpremultiply 64 over alpha 128: 255*64/128=127"
);
assert_eq!(buf[3], 128, "alpha unchanged");
let mut buf2 = vec![200u8, 0, 0, 100];
unpremultiply_rgba(&mut buf2);
assert_eq!(
buf2[0], 255,
"unpremultiply must clamp an over-alpha colour to 255"
);
}
#[test]
fn resize_premultiplied_transparent_red_opaque_blue() {
let src = [255u8, 0, 0, 0, 0, 0, 255, 255]; for f in [Filter::Bilinear, Filter::Bicubic, Filter::Lanczos3] {
let out = resize_rgba8(&src, 2, 1, 1, 1, f).unwrap();
assert_eq!(
out,
vec![0, 0, 255, 128],
"{f:?}: premultiplied-alpha resize must give pure blue (0,0,255,128)"
);
}
let nn = resize_rgba8(&src, 2, 1, 1, 1, Filter::Nearest).unwrap();
assert_eq!(
nn,
vec![0, 0, 255, 255],
"NEAREST must not premultiply — gathers the opaque-blue pixel verbatim"
);
}
#[test]
fn precompute_coeffs_weights_sum_to_unity_fixedpoint() {
let one = 1i64 << PRECISION_BITS;
for f in [Filter::Bilinear, Filter::Bicubic, Filter::Lanczos3] {
for &(insz, outsz) in &[(8usize, 3usize), (3, 8), (5, 5), (16, 4)] {
let c = precompute_coeffs(insz, outsz, f).unwrap();
for o in 0..outsz {
let (_, n) = c.bounds[o];
let s: i64 = c.weights[o * c.ksize..o * c.ksize + n]
.iter()
.map(|&w| i64::from(w))
.sum();
let tol = n as i64 + 1;
assert!(
(s - one).abs() <= tol,
"{f:?} {insz}->{outsz} out {o}: tap sum {s} not within {tol} of {one}"
);
}
}
}
}