visual-cortex-capture 0.7.0

Screen-capture abstraction for visual-cortex: frames, regions, rates, the FrameSource trait, and the macOS ScreenCaptureKit backend.
use std::time::SystemTime;

use crate::error::CaptureError;
use crate::region::PxRect;

/// A single captured frame in BGRA8 layout, row-major, stride = width * 4.
#[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(),
        })
    }

    /// A frame filled with one BGRA color. Handy for tests and fakes.
    ///
    /// # Panics
    /// Panics if `width * height * 4` overflows the allocator's capacity limits.
    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(),
        }
    }

    /// Build a frame by evaluating `f(x, y) -> BGRA` for every pixel.
    /// Handy for synthetic test imagery and demos.
    ///
    /// # Panics
    /// Panics if `width * height * 4` overflows the allocator's capacity
    /// limits (same policy as [`Frame::solid`]).
    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
    }

    /// Borrow a rectangular view. Errors if `rect` exceeds the frame bounds.
    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 })
    }
}

/// A borrowed rectangular view into a frame. Zero-copy; rows are slices of the frame.
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
    }

    /// Iterate rows of the view; each row is `w * 4` BGRA bytes.
    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]
        })
    }

    /// Copy the view into one contiguous BGRA buffer.
    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() {
        // 4x3 frame, pixels numbered 0..12; pixel n = [n, n, n, 255]
        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);
        // row 1 of frame starts at pixel 4; x=1 -> pixels 5,6
        assert_eq!(rows[0], &[5, 5, 5, 255, 6, 6, 6, 255][..]);
        // row 2 -> pixels 9,10
        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);
        // pixel (2, 1): B=2, G=1, R=7, A=255 at byte offset ((1*3)+2)*4 = 20
        assert_eq!(&f.data()[20..24], &[2, 1, 7, 255]);
    }
}