Skip to main content

visual_cortex_capture/
frame.rs

1use std::time::SystemTime;
2
3use crate::error::CaptureError;
4use crate::region::PxRect;
5
6/// A single captured frame in BGRA8 layout, row-major, stride = width * 4.
7#[derive(Debug, Clone)]
8pub struct Frame {
9    width: u32,
10    height: u32,
11    data: Vec<u8>,
12    timestamp: SystemTime,
13}
14
15impl Frame {
16    pub fn new(width: u32, height: u32, data: Vec<u8>) -> Result<Self, CaptureError> {
17        let expected = width as u64 * height as u64 * 4;
18        if data.len() as u64 != expected {
19            return Err(CaptureError::InvalidFrameData {
20                width,
21                height,
22                expected: expected as usize,
23                actual: data.len(),
24            });
25        }
26        Ok(Self {
27            width,
28            height,
29            data,
30            timestamp: SystemTime::now(),
31        })
32    }
33
34    /// A frame filled with one BGRA color. Handy for tests and fakes.
35    ///
36    /// # Panics
37    /// Panics if `width * height * 4` overflows the allocator's capacity limits.
38    pub fn solid(width: u32, height: u32, bgra: [u8; 4]) -> Self {
39        let data = bgra.repeat(width as usize * height as usize);
40        Self {
41            width,
42            height,
43            data,
44            timestamp: SystemTime::now(),
45        }
46    }
47
48    /// Build a frame by evaluating `f(x, y) -> BGRA` for every pixel.
49    /// Handy for synthetic test imagery and demos.
50    ///
51    /// # Panics
52    /// Panics if `width * height * 4` overflows the allocator's capacity
53    /// limits (same policy as [`Frame::solid`]).
54    pub fn from_fn(width: u32, height: u32, mut f: impl FnMut(u32, u32) -> [u8; 4]) -> Self {
55        let mut data = Vec::with_capacity(width as usize * height as usize * 4);
56        for y in 0..height {
57            for x in 0..width {
58                data.extend_from_slice(&f(x, y));
59            }
60        }
61        Self {
62            width,
63            height,
64            data,
65            timestamp: SystemTime::now(),
66        }
67    }
68
69    pub fn width(&self) -> u32 {
70        self.width
71    }
72    pub fn height(&self) -> u32 {
73        self.height
74    }
75    pub fn data(&self) -> &[u8] {
76        &self.data
77    }
78    pub fn timestamp(&self) -> SystemTime {
79        self.timestamp
80    }
81
82    /// Borrow a rectangular view. Errors if `rect` exceeds the frame bounds.
83    pub fn view(&self, rect: PxRect) -> Result<FrameView<'_>, CaptureError> {
84        let in_bounds = rect.w > 0
85            && rect.h > 0
86            && rect.x.checked_add(rect.w).is_some_and(|r| r <= self.width)
87            && rect.y.checked_add(rect.h).is_some_and(|b| b <= self.height);
88        if !in_bounds {
89            return Err(CaptureError::InvalidRegion(format!(
90                "{rect:?} out of bounds for {}x{}",
91                self.width, self.height
92            )));
93        }
94        Ok(FrameView { frame: self, rect })
95    }
96}
97
98/// A borrowed rectangular view into a frame. Zero-copy; rows are slices of the frame.
99pub struct FrameView<'a> {
100    frame: &'a Frame,
101    rect: PxRect,
102}
103
104impl<'a> FrameView<'a> {
105    pub fn width(&self) -> u32 {
106        self.rect.w
107    }
108    pub fn height(&self) -> u32 {
109        self.rect.h
110    }
111    pub fn rect(&self) -> PxRect {
112        self.rect
113    }
114
115    /// Iterate rows of the view; each row is `w * 4` BGRA bytes.
116    pub fn rows(&self) -> impl Iterator<Item = &'a [u8]> + '_ {
117        let fw = self.frame.width as usize;
118        let data: &'a [u8] = &self.frame.data;
119        let PxRect { x, y, w, h } = self.rect;
120        (y..y + h).map(move |row| {
121            let start = (row as usize * fw + x as usize) * 4;
122            &data[start..start + w as usize * 4]
123        })
124    }
125
126    /// Copy the view into one contiguous BGRA buffer.
127    pub fn to_vec(&self) -> Vec<u8> {
128        let mut out = Vec::with_capacity(self.rect.w as usize * self.rect.h as usize * 4);
129        for row in self.rows() {
130            out.extend_from_slice(row);
131        }
132        out
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn new_validates_byte_length() {
142        let ok = Frame::new(2, 2, vec![0u8; 16]);
143        assert!(ok.is_ok());
144
145        let err = Frame::new(2, 2, vec![0u8; 15]).unwrap_err();
146        assert!(matches!(
147            err,
148            CaptureError::InvalidFrameData {
149                expected: 16,
150                actual: 15,
151                ..
152            }
153        ));
154    }
155
156    #[test]
157    fn solid_fills_every_pixel() {
158        let f = Frame::solid(3, 2, [1, 2, 3, 4]);
159        assert_eq!(f.width(), 3);
160        assert_eq!(f.height(), 2);
161        assert_eq!(f.data().len(), 24);
162        assert!(f.data().chunks(4).all(|px| px == [1, 2, 3, 4]));
163    }
164
165    #[test]
166    fn view_crops_rows_correctly() {
167        // 4x3 frame, pixels numbered 0..12; pixel n = [n, n, n, 255]
168        let data: Vec<u8> = (0..12u8).flat_map(|n| [n, n, n, 255]).collect();
169        let f = Frame::new(4, 3, data).unwrap();
170        let view = f
171            .view(PxRect {
172                x: 1,
173                y: 1,
174                w: 2,
175                h: 2,
176            })
177            .unwrap();
178
179        assert_eq!(view.width(), 2);
180        assert_eq!(view.height(), 2);
181        let rows: Vec<&[u8]> = view.rows().collect();
182        assert_eq!(rows.len(), 2);
183        // row 1 of frame starts at pixel 4; x=1 -> pixels 5,6
184        assert_eq!(rows[0], &[5, 5, 5, 255, 6, 6, 6, 255][..]);
185        // row 2 -> pixels 9,10
186        assert_eq!(rows[1], &[9, 9, 9, 255, 10, 10, 10, 255][..]);
187    }
188
189    #[test]
190    fn view_to_vec_is_contiguous() {
191        let f = Frame::solid(4, 4, [7, 8, 9, 255]);
192        let view = f
193            .view(PxRect {
194                x: 0,
195                y: 0,
196                w: 2,
197                h: 2,
198            })
199            .unwrap();
200        assert_eq!(view.to_vec().len(), 16);
201    }
202
203    #[test]
204    fn view_rejects_out_of_bounds_rect() {
205        let f = Frame::solid(4, 4, [0, 0, 0, 255]);
206        assert!(f
207            .view(PxRect {
208                x: 3,
209                y: 0,
210                w: 2,
211                h: 1
212            })
213            .is_err());
214    }
215
216    #[test]
217    fn from_fn_evaluates_every_pixel() {
218        let f = Frame::from_fn(3, 2, |x, y| [x as u8, y as u8, 7, 255]);
219        assert_eq!(f.width(), 3);
220        assert_eq!(f.height(), 2);
221        // pixel (2, 1): B=2, G=1, R=7, A=255 at byte offset ((1*3)+2)*4 = 20
222        assert_eq!(&f.data()[20..24], &[2, 1, 7, 255]);
223    }
224}