use crate::raster::{Raster, RasterError};
pub fn downscale_half(src: &Raster) -> Result<Raster, RasterError> {
let fmt = src.format();
if fmt.has_alpha() {
downscale_half_alpha(src)
} else {
downscale_half_noalpha(src)
}
}
fn downscale_half_noalpha(src: &Raster) -> Result<Raster, RasterError> {
let dst_w = src.width().div_ceil(2);
let dst_h = src.height().div_ceil(2);
let fmt = src.format();
let bpp = fmt.bytes_per_pixel();
let bpc = fmt.bytes_per_channel();
let channels = fmt.channels();
let src_stride = src.stride();
let src_data = src.data();
let mut dst = vec![0u8; dst_w as usize * dst_h as usize * bpp];
for dy in 0..dst_h {
for dx in 0..dst_w {
let sx = dx * 2;
let sy = dy * 2;
let x_count = if sx + 1 < src.width() { 2u32 } else { 1 };
let y_count = if sy + 1 < src.height() { 2u32 } else { 1 };
let dst_offset = (dy as usize * dst_w as usize + dx as usize) * bpp;
for c in 0..channels {
let mut sum: u32 = 0;
let count = x_count * y_count;
for oy in 0..y_count {
for ox in 0..x_count {
let src_offset =
(sy + oy) as usize * src_stride + (sx + ox) as usize * bpp + c * bpc;
if bpc == 1 {
sum += src_data[src_offset] as u32;
} else {
let val = u16::from_ne_bytes([
src_data[src_offset],
src_data[src_offset + 1],
]);
sum += val as u32;
}
}
}
let avg = (sum + count / 2) / count;
if bpc == 1 {
dst[dst_offset + c] = avg as u8;
} else {
let bytes = (avg as u16).to_ne_bytes();
dst[dst_offset + c * 2] = bytes[0];
dst[dst_offset + c * 2 + 1] = bytes[1];
}
}
}
}
Raster::new(dst_w, dst_h, fmt, dst)
}
fn downscale_half_alpha(src: &Raster) -> Result<Raster, RasterError> {
let dst_w = src.width().div_ceil(2);
let dst_h = src.height().div_ceil(2);
let fmt = src.format();
let bpp = fmt.bytes_per_pixel();
let bpc = fmt.bytes_per_channel();
let channels = fmt.channels();
let alpha_idx = channels - 1;
let src_stride = src.stride();
let src_data = src.data();
let mut dst = vec![0u8; dst_w as usize * dst_h as usize * bpp];
for dy in 0..dst_h {
for dx in 0..dst_w {
let sx = dx * 2;
let sy = dy * 2;
let x_count = if sx + 1 < src.width() { 2u32 } else { 1 };
let y_count = if sy + 1 < src.height() { 2u32 } else { 1 };
let count = x_count * y_count;
let dst_offset = (dy as usize * dst_w as usize + dx as usize) * bpp;
let mut alphas = [0.0f64; 4];
let mut pixel_offsets = [0usize; 4];
let mut n = 0;
for oy in 0..y_count {
for ox in 0..x_count {
let off = (sy + oy) as usize * src_stride + (sx + ox) as usize * bpp;
pixel_offsets[n] = off;
alphas[n] = if bpc == 1 {
src_data[off + alpha_idx] as f64
} else {
u16::from_ne_bytes([
src_data[off + alpha_idx * 2],
src_data[off + alpha_idx * 2 + 1],
]) as f64
};
n += 1;
}
}
let alpha_sum: f64 = alphas[..n].iter().sum();
let avg_alpha = alpha_sum / count as f64;
if avg_alpha == 0.0 {
for c in 0..channels {
if bpc == 1 {
dst[dst_offset + c] = 0;
} else {
dst[dst_offset + c * 2] = 0;
dst[dst_offset + c * 2 + 1] = 0;
}
}
} else {
for c in 0..alpha_idx {
let mut weighted_sum = 0.0f64;
for i in 0..n {
let val = if bpc == 1 {
src_data[pixel_offsets[i] + c] as f64
} else {
u16::from_ne_bytes([
src_data[pixel_offsets[i] + c * 2],
src_data[pixel_offsets[i] + c * 2 + 1],
]) as f64
};
weighted_sum += alphas[i] * val;
}
let result = weighted_sum / (count as f64 * avg_alpha);
if bpc == 1 {
dst[dst_offset + c] = result as u8;
} else {
let bytes = (result as u16).to_ne_bytes();
dst[dst_offset + c * 2] = bytes[0];
dst[dst_offset + c * 2 + 1] = bytes[1];
}
}
if bpc == 1 {
dst[dst_offset + alpha_idx] = avg_alpha as u8;
} else {
let bytes = (avg_alpha as u16).to_ne_bytes();
dst[dst_offset + alpha_idx * 2] = bytes[0];
dst[dst_offset + alpha_idx * 2 + 1] = bytes[1];
}
}
}
}
Raster::new(dst_w, dst_h, fmt, dst)
}
pub fn downscale_to(src: &Raster, dst_w: u32, dst_h: u32) -> Result<Raster, RasterError> {
if dst_w == 0 || dst_h == 0 {
return Err(RasterError::ZeroDimension {
width: dst_w,
height: dst_h,
});
}
let fmt = src.format();
let bpp = fmt.bytes_per_pixel();
let bpc = fmt.bytes_per_channel();
let channels = fmt.channels();
let src_stride = src.stride();
let src_data = src.data();
let src_w = src.width();
let src_h = src.height();
let mut dst = vec![0u8; dst_w as usize * dst_h as usize * bpp];
for dy in 0..dst_h {
for dx in 0..dst_w {
let sx0 = (dx as u64 * src_w as u64 / dst_w as u64) as u32;
let sy0 = (dy as u64 * src_h as u64 / dst_h as u64) as u32;
let sx1 = (((dx + 1) as u64 * src_w as u64).div_ceil(dst_w as u64)) as u32;
let sy1 = (((dy + 1) as u64 * src_h as u64).div_ceil(dst_h as u64)) as u32;
let sx1 = sx1.min(src_w);
let sy1 = sy1.min(src_h);
let dst_offset = (dy as usize * dst_w as usize + dx as usize) * bpp;
let count = (sx1 - sx0) * (sy1 - sy0);
if count == 0 {
continue;
}
for c in 0..channels {
let mut sum: u64 = 0;
for sy in sy0..sy1 {
for sx in sx0..sx1 {
let src_offset = sy as usize * src_stride + sx as usize * bpp + c * bpc;
if bpc == 1 {
sum += src_data[src_offset] as u64;
} else {
let val = u16::from_ne_bytes([
src_data[src_offset],
src_data[src_offset + 1],
]);
sum += val as u64;
}
}
}
let avg = (sum + count as u64 / 2) / count as u64;
if bpc == 1 {
dst[dst_offset + c] = avg as u8;
} else {
let bytes = (avg as u16).to_ne_bytes();
dst[dst_offset + c * 2] = bytes[0];
dst[dst_offset + c * 2 + 1] = bytes[1];
}
}
}
}
Raster::new(dst_w, dst_h, fmt, dst)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pixel::PixelFormat;
fn solid_raster(w: u32, h: u32, pixel: &[u8], fmt: PixelFormat) -> Raster {
let bpp = fmt.bytes_per_pixel();
assert_eq!(pixel.len(), bpp);
let mut data = Vec::with_capacity(w as usize * h as usize * bpp);
for _ in 0..(w * h) {
data.extend_from_slice(pixel);
}
Raster::new(w, h, fmt, data).unwrap()
}
#[test]
fn half_even_dimensions() {
let src = solid_raster(4, 4, &[200], PixelFormat::Gray8);
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.width(), 2);
assert_eq!(dst.height(), 2);
assert!(dst.data().iter().all(|&b| b == 200));
}
#[test]
fn half_odd_dimensions() {
let src = solid_raster(5, 5, &[100], PixelFormat::Gray8);
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.width(), 3);
assert_eq!(dst.height(), 3);
assert!(dst.data().iter().all(|&b| b == 100));
}
#[test]
fn half_1x1_stays_1x1() {
let src = solid_raster(1, 1, &[42], PixelFormat::Gray8);
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.width(), 1);
assert_eq!(dst.height(), 1);
assert_eq!(dst.data(), &[42]);
}
#[test]
fn half_averaging_works() {
let data = vec![10, 20, 30, 40]; let src = Raster::new(2, 2, PixelFormat::Gray8, data).unwrap();
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.width(), 1);
assert_eq!(dst.height(), 1);
assert_eq!(dst.data()[0], 25);
}
#[test]
fn half_rgb8() {
let src = solid_raster(2, 2, &[255, 0, 0], PixelFormat::Rgb8);
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.width(), 1);
assert_eq!(dst.height(), 1);
assert_eq!(dst.data(), &[255, 0, 0]);
}
#[test]
fn half_rgba8() {
let src = solid_raster(4, 4, &[100, 150, 200, 255], PixelFormat::Rgba8);
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.width(), 2);
assert_eq!(dst.height(), 2);
for chunk in dst.data().chunks(4) {
assert_eq!(chunk, &[100, 150, 200, 255]);
}
}
#[test]
fn half_preserves_format() {
for fmt in [PixelFormat::Gray8, PixelFormat::Rgb8, PixelFormat::Rgba8] {
let bpp = fmt.bytes_per_pixel();
let pixel: Vec<u8> = (0..bpp).map(|i| (i * 50) as u8).collect();
let src = solid_raster(8, 8, &pixel, fmt);
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.format(), fmt);
}
}
#[test]
fn half_iterative_to_1x1() {
let mut r = solid_raster(256, 256, &[128], PixelFormat::Gray8);
while r.width() > 1 || r.height() > 1 {
r = downscale_half(&r).unwrap();
}
assert_eq!(r.width(), 1);
assert_eq!(r.height(), 1);
assert_eq!(r.data()[0], 128);
}
#[test]
fn downscale_to_same_size() {
let src = solid_raster(10, 10, &[77], PixelFormat::Gray8);
let dst = downscale_to(&src, 10, 10).unwrap();
assert_eq!(dst.width(), 10);
assert_eq!(dst.height(), 10);
assert!(dst.data().iter().all(|&b| b == 77));
}
#[test]
fn downscale_to_zero_rejected() {
let src = solid_raster(10, 10, &[1], PixelFormat::Gray8);
assert!(downscale_to(&src, 0, 5).is_err());
assert!(downscale_to(&src, 5, 0).is_err());
}
#[test]
fn downscale_to_solid_preserved() {
let src = solid_raster(100, 100, &[200, 100, 50], PixelFormat::Rgb8);
let dst = downscale_to(&src, 33, 25).unwrap();
assert_eq!(dst.width(), 33);
assert_eq!(dst.height(), 25);
for chunk in dst.data().chunks(3) {
assert_eq!(chunk, &[200, 100, 50]);
}
}
#[test]
fn half_rgba_solid_opaque() {
let src = solid_raster(4, 4, &[100, 150, 200, 255], PixelFormat::Rgba8);
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.width(), 2);
assert_eq!(dst.height(), 2);
for chunk in dst.data().chunks(4) {
assert_eq!(chunk, &[100, 150, 200, 255]);
}
}
#[test]
fn half_rgba_fully_transparent() {
let src = solid_raster(2, 2, &[100, 200, 50, 0], PixelFormat::Rgba8);
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.data(), &[0, 0, 0, 0]);
}
#[test]
fn half_rgba_alpha_weights_color() {
let data = vec![
255, 0, 0, 255, 255, 0, 0, 255, 0, 255, 0, 0, 0, 255, 0, 0, ];
let src = Raster::new(2, 2, PixelFormat::Rgba8, data).unwrap();
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.data()[0], 255, "R should be 255 (alpha-weighted)");
assert_eq!(
dst.data()[1],
0,
"G should be 0 (transparent pixels ignored)"
);
assert_eq!(dst.data()[2], 0, "B should be 0");
assert_eq!(dst.data()[3], 127, "A should be 127 (truncated from 127.5)");
}
#[test]
fn half_rgba_partial_alpha() {
let data = vec![
100, 0, 0, 200, 200, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, ];
let src = Raster::new(2, 2, PixelFormat::Rgba8, data).unwrap();
let dst = downscale_half(&src).unwrap();
let avg_alpha = (200.0 + 50.0) / 4.0;
let expected_r = (200.0 * 100.0 + 50.0 * 200.0) / (4.0 * avg_alpha);
assert_eq!(dst.data()[0], expected_r as u8, "R alpha-weighted");
assert_eq!(dst.data()[3], avg_alpha as u8, "A averaged");
}
#[test]
fn half_rgba_odd_dimensions() {
let data = vec![
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 128, ];
let src = Raster::new(3, 1, PixelFormat::Rgba8, data).unwrap();
let dst = downscale_half(&src).unwrap();
assert_eq!(dst.width(), 2);
assert_eq!(dst.height(), 1);
assert_eq!(dst.data()[0], 127); assert_eq!(dst.data()[1], 127); assert_eq!(dst.data()[3], 255); }
}