sentryshot_filter 0.1.2

Port of FFmpeg's filters
Documentation
// SPDX-License-Identifier: MPL-2.0+

use sentryshot_util::{image_fill_max_pixsteps, pixfmt::PIX_FMT_FLAG_PAL, Frame};
use std::num::NonZeroU16;
use thiserror::Error;

#[derive(Debug, Error, PartialEq, Eq)]
pub enum CropError {
    #[error("size out of bounds: {0}+{1} > {2}")]
    OutOfBounds(u16, u16, u16),

    #[error("add")]
    Add,
}

/// Crops `src_frame` and stores it in `dst_frame`
///
/// `dst_frame` will be resized if necessary.
#[allow(clippy::similar_names)]
pub fn crop(
    src_frame: &Frame,
    dst_frame: &mut Frame,
    x: u16,
    y: u16,
    w: NonZeroU16,
    h: NonZeroU16,
) -> Result<(), CropError> {
    use CropError::*;

    let src_width = src_frame.width().get();
    if x.checked_add(w.get()).ok_or(Add)? > src_width {
        return Err(OutOfBounds(x, w.get(), src_width));
    }
    let src_height = src_frame.height().get();
    if y.checked_add(h.get()).ok_or(Add)? > src_height {
        return Err(OutOfBounds(y, h.get(), src_height));
    }

    let pix_fmt = src_frame.pix_fmt();

    dst_frame.set_width(w);
    dst_frame.set_height(h);
    dst_frame.set_pix_fmt(pix_fmt);

    // The line size doesn't actually change.
    *dst_frame.linesize_mut() = *src_frame.linesize();

    let mut max_step = [0, 0, 0, 0];
    image_fill_max_pixsteps(&mut max_step, &mut [0, 0, 0, 0], Some(pix_fmt));

    let hsub = pix_fmt.log2_chroma_w();
    let vsub = pix_fmt.log2_chroma_h();

    let mut offsets = [0, 0, 0, 0];
    offsets[0] = usize::from(y) * dst_frame.linesize()[0];
    offsets[0] += usize::from(x) * max_step[0];

    if pix_fmt.flags() & PIX_FMT_FLAG_PAL == 0 {
        for i in 1..3 {
            if !dst_frame.data()[i].is_empty() {
                offsets[i] += (usize::from(y) >> vsub) * dst_frame.linesize()[i];
                offsets[i] += (usize::from(x) * max_step[i]) >> hsub;
            }
        }
    }

    // Alpha plane.
    if !dst_frame.data()[3].is_empty() {
        offsets[3] += usize::from(y) * dst_frame.linesize()[3];
        offsets[3] += usize::from(x) * max_step[3];
    }

    #[allow(clippy::needless_range_loop)]
    for i in 0..4 {
        dst_frame.data_mut()[i] = src_frame.data()[i][offsets[i]..].to_owned();
    }

    Ok(())
}

#[allow(clippy::too_many_arguments, clippy::unwrap_used)]
#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;
    use pretty_hex::pretty_hex;
    use sentryshot_util::pixfmt::PixelFormat;
    use std::num::NonZeroU16;
    use test_case::test_case;

    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        0, 0, 2, 2;
        "rgb24 minimal"
    )]
    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        &[0,0,0,
          2,2,2],
        0, 0, 1, 2;
        "rgb24 right"
    )]
    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        &[0,0,0, 1,1,1],
        0, 0, 2, 1;
        "rgb24 bottom"
    )]
    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        &[0,0,0],
        0, 0, 1, 1;
        "rgb24 right and bottom"
    )]
    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        &[1,1,1,
          3,3,3],
        1, 0, 1, 2;
        "rgb24 left"
    )]
    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        &[2,2,2, 3,3,3],
        0, 1, 2, 1;
        "rgb24 top"
    )]
    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        &[3,3,3],
        1, 1, 1, 1;
        "rgb24 left and top"
    )]
    #[test_case(
        3, 3, PixelFormat::RGB24,
        &[0,0,0, 1,1,1, 2,2,2,
          3,3,3, 4,4,4, 5,5,5,
          6,6,6, 7,7,7, 8,8,8],
        &[0,0,0, 1,1,1, 2,2,2,
          3,3,3, 4,4,4, 5,5,5,
          6,6,6, 7,7,7, 8,8,8],
        0, 0, 3, 3;
        "rgb24 3x3 minimal"
    )]
    #[test_case(
        3, 3, PixelFormat::RGB24,
        &[0,0,0, 1,1,1, 2,2,2,
          3,3,3, 4,4,4, 5,5,5,
          6,6,6, 7,7,7, 8,8,8],
        &[4,4,4, 5,5,5,
          7,7,7, 8,8,8],
        1, 1, 2, 2;
        "rgb24 3x3 to 2x2"
    )]
    #[test_case(
        4, 4, PixelFormat::RGB24,
        &[0,0,0,    1,1,1,    2,2,2,    3,3,3,
          4,4,4,    5,5,5,    6,6,6,    7,7,7,
          8,8,8,    9,9,9,    10,10,10, 11,11,11,
          12,12,12, 13,13,13, 14,14,14, 15,15,15,
        ],
        &[0,0,0,    1,1,1,    2,2,2,    3,3,3,
          4,4,4,    5,5,5,    6,6,6,    7,7,7,
          8,8,8,    9,9,9,    10,10,10, 11,11,11,
          12,12,12, 13,13,13, 14,14,14, 15,15,15],
        0, 0, 4, 4;
        "rgb24 4x4 minimal"
    )]
    #[test_case(
        4, 4, PixelFormat::RGB24,
        &[0,0,0,    1,1,1,    2,2,2,    3,3,3,
          4,4,4,    5,5,5,    6,6,6,    7,7,7,
          8,8,8,    9,9,9,    10,10,10, 11,11,11,
          12,12,12, 13,13,13, 14,14,14, 15,15,15],
        &[5,5,5, 6,6,6,
          9,9,9, 10,10,10],
        1, 1, 2, 2;
        "rgb24 4x4 border"
    )]
    fn test_crop(
        src_width: u16,
        src_height: u16,
        pix_fmt: PixelFormat,
        input: &[u8],
        want: &[u8],
        x: u16,
        y: u16,
        w: u16,
        h: u16,
    ) {
        let src_frame = Frame::from_raw(
            input,
            pix_fmt,
            NonZeroU16::new(src_width).unwrap(),
            NonZeroU16::new(src_height).unwrap(),
            1,
        )
        .unwrap();

        let mut dst_frame = Frame::new();
        crop(
            &src_frame,
            &mut dst_frame,
            x,
            y,
            NonZeroU16::new(w).unwrap(),
            NonZeroU16::new(h).unwrap(),
        )
        .unwrap();

        let mut raw = Vec::new();
        dst_frame.copy_to_buffer(&mut raw, 1).unwrap();

        assert_eq!(w, dst_frame.width().get());
        assert_eq!(h, dst_frame.height().get());
        assert_eq!(pix_fmt, dst_frame.pix_fmt());
        assert_eq!(pretty_hex(&want), pretty_hex(&raw));
    }

    #[test]
    fn test_crop_twice() {
        #[rustfmt::skip]
        let frame1 = Frame::from_raw(
            &[0,0,0,    1,1,1,    2,2,2,    3,3,3,    4,4,4,
              5,5,5,    6,6,6,    7,7,7,    8,8,8,    9,9,9,
              10,10,10, 11,11,11, 12,12,12, 13,13,13, 14,14,14,
              15,15,15, 16,16,16, 17,17,17, 18,18,18, 19,19,19,
              20,20,20, 21,21,21, 22,22,22, 23,23,23, 24,24,24],
            PixelFormat::RGB24,
            NonZeroU16::new(5).unwrap(),
            NonZeroU16::new(5).unwrap(),
            1,
        )
        .unwrap();

        let mut frame2 = Frame::new();
        crop(
            &frame1,
            &mut frame2,
            1,
            1,
            NonZeroU16::new(3).unwrap(),
            NonZeroU16::new(3).unwrap(),
        )
        .unwrap();

        let mut raw = Vec::new();
        frame2.copy_to_buffer(&mut raw, 1).unwrap();

        assert_eq!(3, frame2.width().get());
        assert_eq!(3, frame2.height().get());

        #[rustfmt::skip]
        let want = &[6,6,6,    7,7,7,    8,8,8,
                     11,11,11, 12,12,12, 13,13,13,
                     16,16,16, 17,17,17, 18,18,18];
        assert_eq!(pretty_hex(&want), pretty_hex(&raw));

        let mut frame3 = Frame::new();
        crop(
            &frame2,
            &mut frame3,
            1,
            1,
            NonZeroU16::new(1).unwrap(),
            NonZeroU16::new(1).unwrap(),
        )
        .unwrap();

        let mut raw = Vec::new();
        frame3.copy_to_buffer(&mut raw, 1).unwrap();

        assert_eq!(1, frame3.width().get());
        assert_eq!(1, frame3.height().get());
        assert_eq!(pretty_hex(&[12, 12, 12]), pretty_hex(&raw));
    }

    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        1, 0, 2, 2,
        CropError::OutOfBounds(1, 2, 2);
        "width out of bounds"
    )]
    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        0, 1, 2, 2,
        CropError::OutOfBounds(1, 2, 2);
        "height out of bounds"
    )]
    #[test_case(
        2, 2, PixelFormat::RGB24,
        &[0,0,0, 1,1,1,
          2,2,2, 3,3,3],
        0, 65535, 2, 2,
        CropError::Add;
        "add"
    )]
    fn test_crop_error(
        src_width: u16,
        src_height: u16,
        pix_fmt: PixelFormat,
        input: &[u8],
        x: u16,
        y: u16,
        w: u16,
        h: u16,
        want: CropError,
    ) {
        let src_frame = Frame::from_raw(
            input,
            pix_fmt,
            NonZeroU16::new(src_width).unwrap(),
            NonZeroU16::new(src_height).unwrap(),
            1,
        )
        .unwrap();

        let result = crop(
            &src_frame,
            &mut Frame::new(),
            x,
            y,
            NonZeroU16::new(w).unwrap(),
            NonZeroU16::new(h).unwrap(),
        );
        assert_eq!(result, Err(want));
    }
}