use std::time::SystemTime;
use crate::error::CaptureError;
use crate::region::PxRect;
#[derive(Debug, Clone)]
pub struct Frame {
width: u32,
height: u32,
data: Vec<u8>,
timestamp: SystemTime,
}
impl Frame {
pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self, CaptureError> {
let expected = width as u64 * height as u64 * 4;
if data.len() as u64 != expected {
return Err(CaptureError::InvalidFrameData {
width,
height,
expected: expected as usize,
actual: data.len(),
});
}
Ok(Self {
width,
height,
data,
timestamp: SystemTime::now(),
})
}
pub fn solid(width: u32, height: u32, bgra: [u8; 4]) -> Self {
let data = bgra.repeat(width as usize * height as usize);
Self {
width,
height,
data,
timestamp: SystemTime::now(),
}
}
pub fn from_fn(width: u32, height: u32, mut f: impl FnMut(u32, u32) -> [u8; 4]) -> Self {
let mut data = Vec::with_capacity(width as usize * height as usize * 4);
for y in 0..height {
for x in 0..width {
data.extend_from_slice(&f(x, y));
}
}
Self {
width,
height,
data,
timestamp: SystemTime::now(),
}
}
pub fn width(&self) -> u32 {
self.width
}
pub fn height(&self) -> u32 {
self.height
}
pub fn data(&self) -> &[u8] {
&self.data
}
pub fn timestamp(&self) -> SystemTime {
self.timestamp
}
pub fn view(&self, rect: PxRect) -> Result<FrameView<'_>, CaptureError> {
let in_bounds = rect.w > 0
&& rect.h > 0
&& rect.x.checked_add(rect.w).is_some_and(|r| r <= self.width)
&& rect.y.checked_add(rect.h).is_some_and(|b| b <= self.height);
if !in_bounds {
return Err(CaptureError::InvalidRegion(format!(
"{rect:?} out of bounds for {}x{}",
self.width, self.height
)));
}
Ok(FrameView { frame: self, rect })
}
}
pub struct FrameView<'a> {
frame: &'a Frame,
rect: PxRect,
}
impl<'a> FrameView<'a> {
pub fn width(&self) -> u32 {
self.rect.w
}
pub fn height(&self) -> u32 {
self.rect.h
}
pub fn rect(&self) -> PxRect {
self.rect
}
pub fn rows(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
let fw = self.frame.width as usize;
let data: &'a [u8] = &self.frame.data;
let PxRect { x, y, w, h } = self.rect;
(y..y + h).map(move |row| {
let start = (row as usize * fw + x as usize) * 4;
&data[start..start + w as usize * 4]
})
}
pub fn to_vec(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.rect.w as usize * self.rect.h as usize * 4);
for row in self.rows() {
out.extend_from_slice(row);
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_validates_byte_length() {
let ok = Frame::new(2, 2, vec![0u8; 16]);
assert!(ok.is_ok());
let err = Frame::new(2, 2, vec![0u8; 15]).unwrap_err();
assert!(matches!(
err,
CaptureError::InvalidFrameData {
expected: 16,
actual: 15,
..
}
));
}
#[test]
fn solid_fills_every_pixel() {
let f = Frame::solid(3, 2, [1, 2, 3, 4]);
assert_eq!(f.width(), 3);
assert_eq!(f.height(), 2);
assert_eq!(f.data().len(), 24);
assert!(f.data().chunks(4).all(|px| px == [1, 2, 3, 4]));
}
#[test]
fn view_crops_rows_correctly() {
let data: Vec<u8> = (0..12u8).flat_map(|n| [n, n, n, 255]).collect();
let f = Frame::new(4, 3, data).unwrap();
let view = f
.view(PxRect {
x: 1,
y: 1,
w: 2,
h: 2,
})
.unwrap();
assert_eq!(view.width(), 2);
assert_eq!(view.height(), 2);
let rows: Vec<&[u8]> = view.rows().collect();
assert_eq!(rows.len(), 2);
assert_eq!(rows[0], &[5, 5, 5, 255, 6, 6, 6, 255][..]);
assert_eq!(rows[1], &[9, 9, 9, 255, 10, 10, 10, 255][..]);
}
#[test]
fn view_to_vec_is_contiguous() {
let f = Frame::solid(4, 4, [7, 8, 9, 255]);
let view = f
.view(PxRect {
x: 0,
y: 0,
w: 2,
h: 2,
})
.unwrap();
assert_eq!(view.to_vec().len(), 16);
}
#[test]
fn view_rejects_out_of_bounds_rect() {
let f = Frame::solid(4, 4, [0, 0, 0, 255]);
assert!(f
.view(PxRect {
x: 3,
y: 0,
w: 2,
h: 1
})
.is_err());
}
#[test]
fn from_fn_evaluates_every_pixel() {
let f = Frame::from_fn(3, 2, |x, y| [x as u8, y as u8, 7, 255]);
assert_eq!(f.width(), 3);
assert_eq!(f.height(), 2);
assert_eq!(&f.data()[20..24], &[2, 1, 7, 255]);
}
}