use crate::pixel::PixelFormat;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RasterError {
#[error(
"dimensions {width}x{height} with format {format:?} require {expected} bytes, got {actual}"
)]
BufferSizeMismatch {
width: u32,
height: u32,
format: PixelFormat,
expected: usize,
actual: usize,
},
#[error("zero dimension: {width}x{height}")]
ZeroDimension { width: u32, height: u32 },
#[error("region ({x},{y})+({w},{h}) out of bounds for {raster_w}x{raster_h}")]
RegionOutOfBounds {
x: u32,
y: u32,
w: u32,
h: u32,
raster_w: u32,
raster_h: u32,
},
}
#[derive(Debug, Clone)]
pub struct Raster {
width: u32,
height: u32,
format: PixelFormat,
data: Vec<u8>,
}
impl Raster {
pub fn new(
width: u32,
height: u32,
format: PixelFormat,
data: Vec<u8>,
) -> Result<Self, RasterError> {
if width == 0 || height == 0 {
return Err(RasterError::ZeroDimension { width, height });
}
let expected = width as usize * height as usize * format.bytes_per_pixel();
if data.len() != expected {
return Err(RasterError::BufferSizeMismatch {
width,
height,
format,
expected,
actual: data.len(),
});
}
Ok(Self {
width,
height,
format,
data,
})
}
pub fn zeroed(width: u32, height: u32, format: PixelFormat) -> Result<Self, RasterError> {
if width == 0 || height == 0 {
return Err(RasterError::ZeroDimension { width, height });
}
let size = width as usize * height as usize * format.bytes_per_pixel();
Self::new(width, height, format, vec![0u8; size])
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn format(&self) -> PixelFormat {
self.format
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn data_mut(&mut self) -> &mut [u8] {
&mut self.data
}
pub fn stride(&self) -> usize {
self.width as usize * self.format.bytes_per_pixel()
}
pub fn region(&self, x: u32, y: u32, w: u32, h: u32) -> Result<RegionView<'_>, RasterError> {
if x + w > self.width || y + h > self.height || w == 0 || h == 0 {
return Err(RasterError::RegionOutOfBounds {
x,
y,
w,
h,
raster_w: self.width,
raster_h: self.height,
});
}
Ok(RegionView {
raster: self,
x,
y,
w,
h,
})
}
pub fn extract(&self, x: u32, y: u32, w: u32, h: u32) -> Result<Raster, RasterError> {
let view = self.region(x, y, w, h)?;
let bpp = self.format.bytes_per_pixel();
let mut out = Vec::with_capacity(w as usize * h as usize * bpp);
for row in view.rows() {
out.extend_from_slice(row);
}
Raster::new(w, h, self.format, out)
}
}
#[derive(Debug)]
pub struct RegionView<'a> {
raster: &'a Raster,
x: u32,
y: u32,
w: u32,
h: u32,
}
impl<'a> RegionView<'a> {
pub fn width(&self) -> u32 {
self.w
}
pub fn height(&self) -> u32 {
self.h
}
pub fn rows(&self) -> impl Iterator<Item = &'a [u8]> {
let bpp = self.raster.format.bytes_per_pixel();
let stride = self.raster.stride();
let x_offset = self.x as usize * bpp;
let row_len = self.w as usize * bpp;
let data = self.raster.data();
(self.y..self.y + self.h).map(move |row| {
let start = row as usize * stride + x_offset;
&data[start..start + row_len]
})
}
pub fn pixel(&self, px: u32, py: u32) -> Option<&'a [u8]> {
if px >= self.w || py >= self.h {
return None;
}
let bpp = self.raster.format.bytes_per_pixel();
let stride = self.raster.stride();
let abs_x = self.x + px;
let abs_y = self.y + py;
let start = abs_y as usize * stride + abs_x as usize * bpp;
Some(&self.raster.data()[start..start + bpp])
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rgb_raster(w: u32, h: u32) -> Raster {
let bpp = PixelFormat::Rgb8.bytes_per_pixel();
let mut data = vec![0u8; w as usize * h as usize * bpp];
for y in 0..h {
for x in 0..w {
let offset = (y as usize * w as usize + x as usize) * bpp;
data[offset] = x as u8;
data[offset + 1] = y as u8;
data[offset + 2] = (x + y) as u8;
}
}
Raster::new(w, h, PixelFormat::Rgb8, data).unwrap()
}
#[test]
fn new_validates_buffer_size() {
let result = Raster::new(2, 2, PixelFormat::Rgb8, vec![0u8; 11]);
assert!(result.is_err());
let result = Raster::new(2, 2, PixelFormat::Rgb8, vec![0u8; 12]);
assert!(result.is_ok());
}
#[test]
fn zero_dimension_rejected() {
assert!(Raster::new(0, 10, PixelFormat::Rgb8, vec![]).is_err());
assert!(Raster::new(10, 0, PixelFormat::Rgb8, vec![]).is_err());
assert!(Raster::zeroed(0, 5, PixelFormat::Gray8).is_err());
}
#[test]
fn stride_is_width_times_bpp() {
let r = Raster::zeroed(100, 50, PixelFormat::Rgba8).unwrap();
assert_eq!(r.stride(), 400);
}
#[test]
fn region_bounds_checking() {
let r = Raster::zeroed(10, 10, PixelFormat::Rgb8).unwrap();
assert!(r.region(0, 0, 10, 10).is_ok());
assert!(r.region(5, 5, 5, 5).is_ok());
assert!(r.region(5, 5, 6, 5).is_err()); assert!(r.region(0, 0, 0, 5).is_err()); }
#[test]
fn region_pixel_matches_source() {
let r = make_rgb_raster(16, 16);
let view = r.region(4, 3, 8, 8).unwrap();
let px = view.pixel(0, 0).unwrap();
assert_eq!(px, &[4, 3, 7]);
let px = view.pixel(7, 7).unwrap();
assert_eq!(px, &[11, 10, 21]);
}
#[test]
fn region_pixel_out_of_bounds_returns_none() {
let r = Raster::zeroed(10, 10, PixelFormat::Rgb8).unwrap();
let view = r.region(0, 0, 5, 5).unwrap();
assert!(view.pixel(5, 0).is_none());
assert!(view.pixel(0, 5).is_none());
}
#[test]
fn extract_produces_correct_sub_image() {
let r = make_rgb_raster(16, 16);
let sub = r.extract(2, 3, 4, 5).unwrap();
assert_eq!(sub.width(), 4);
assert_eq!(sub.height(), 5);
assert_eq!(sub.format(), PixelFormat::Rgb8);
assert_eq!(sub.data().len(), 4 * 5 * 3);
let bpp = 3;
assert_eq!(sub.data()[0], 2); assert_eq!(sub.data()[1], 3); assert_eq!(sub.data()[2], 5); let last = (4 * 5 - 1) * bpp;
assert_eq!(sub.data()[last], 5);
assert_eq!(sub.data()[last + 1], 7);
assert_eq!(sub.data()[last + 2], 12);
}
#[test]
fn region_rows_iteration() {
let r = make_rgb_raster(8, 8);
let view = r.region(1, 1, 3, 2).unwrap();
let rows: Vec<&[u8]> = view.rows().collect();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].len(), 9); assert_eq!(rows[0][0..3], [1, 1, 2]); assert_eq!(rows[0][3..6], [2, 1, 3]); }
#[test]
fn single_pixel_raster() {
let r = Raster::new(1, 1, PixelFormat::Gray8, vec![42]).unwrap();
assert_eq!(r.width(), 1);
assert_eq!(r.height(), 1);
assert_eq!(r.data(), &[42]);
let view = r.region(0, 0, 1, 1).unwrap();
assert_eq!(view.pixel(0, 0), Some([42].as_slice()));
}
#[test]
fn zeroed_raster_is_all_zeros() {
let r = Raster::zeroed(5, 5, PixelFormat::Rgba8).unwrap();
assert!(r.data().iter().all(|&b| b == 0));
}
}
#[cfg(test)]
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#![proptest_config(ProptestConfig {
failure_persistence: None,
.. ProptestConfig::default()
})]
#[test]
fn buffer_size_invariant(w in 1u32..256, h in 1u32..256) {
for fmt in [PixelFormat::Gray8, PixelFormat::Rgb8, PixelFormat::Rgba8,
PixelFormat::Gray16, PixelFormat::Rgb16, PixelFormat::Rgba16] {
let r = Raster::zeroed(w, h, fmt).unwrap();
prop_assert_eq!(
r.data().len(),
w as usize * h as usize * fmt.bytes_per_pixel()
);
}
}
#[test]
fn extract_matches_region_pixels(
w in 4u32..64, h in 4u32..64,
rx in 0u32..4, ry in 0u32..4,
rw in 1u32..4, rh in 1u32..4,
) {
prop_assume!(rx + rw <= w && ry + rh <= h);
let bpp = PixelFormat::Rgb8.bytes_per_pixel();
let mut data = vec![0u8; w as usize * h as usize * bpp];
for y in 0..h {
for x in 0..w {
let offset = (y as usize * w as usize + x as usize) * bpp;
data[offset] = (x % 256) as u8;
data[offset + 1] = (y % 256) as u8;
data[offset + 2] = ((x + y) % 256) as u8;
}
}
let raster = Raster::new(w, h, PixelFormat::Rgb8, data).unwrap();
let view = raster.region(rx, ry, rw, rh).unwrap();
let extracted = raster.extract(rx, ry, rw, rh).unwrap();
for py in 0..rh {
for px in 0..rw {
let view_pixel = view.pixel(px, py).unwrap();
let ext_offset = (py as usize * rw as usize + px as usize) * bpp;
let ext_pixel = &extracted.data()[ext_offset..ext_offset + bpp];
prop_assert_eq!(view_pixel, ext_pixel);
}
}
}
}
}