use derive_more::{Display, IsVariant};
use thiserror::Error;
pub use mediatime::{TimeRange, Timebase, Timestamp};
#[derive(Debug, Clone, Copy)]
pub struct LumaFrame<'a> {
data: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
}
impl<'a> LumaFrame<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(
data: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
) -> Self {
match Self::try_new(data, width, height, stride, timestamp) {
Ok(f) => f,
Err(_) => panic!("invalid LumaFrame dimensions or data length"),
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn try_new(
data: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
) -> Result<Self, LumaFrameError> {
if stride < width {
return Err(LumaFrameError::StrideTooSmall { width, stride });
}
let expected = match (stride as usize).checked_mul(height as usize) {
Some(v) => v,
None => return Err(LumaFrameError::DimensionsOverflow { stride, height }),
};
if data.len() < expected {
return Err(LumaFrameError::DataTooShort {
expected,
actual: data.len(),
});
}
Ok(Self {
data,
width,
height,
stride,
timestamp,
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn data(&self) -> &'a [u8] {
self.data
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn width(&self) -> u32 {
self.width
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn height(&self) -> u32 {
self.height
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stride(&self) -> u32 {
self.stride
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn timestamp(&self) -> Timestamp {
self.timestamp
}
}
#[derive(Debug, Clone, Copy)]
pub struct RgbFrame<'a> {
data: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
}
impl<'a> RgbFrame<'a> {
pub const BYTES_PER_PIXEL: u32 = 3;
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(
data: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
) -> Self {
match Self::try_new(data, width, height, stride, timestamp) {
Ok(f) => f,
Err(_) => panic!("invalid RgbFrame dimensions or data length"),
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn try_new(
data: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
) -> Result<Self, RgbFrameError> {
let min_stride = match width.checked_mul(Self::BYTES_PER_PIXEL) {
Some(v) => v,
None => return Err(RgbFrameError::WidthOverflow { width }),
};
if stride < min_stride {
return Err(RgbFrameError::StrideTooSmall {
width,
stride,
min_stride,
});
}
let expected = match (stride as usize).checked_mul(height as usize) {
Some(v) => v,
None => return Err(RgbFrameError::DimensionsOverflow { stride, height }),
};
if data.len() < expected {
return Err(RgbFrameError::DataTooShort {
expected,
actual: data.len(),
});
}
Ok(Self {
data,
width,
height,
stride,
timestamp,
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn data(&self) -> &'a [u8] {
self.data
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn width(&self) -> u32 {
self.width
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn height(&self) -> u32 {
self.height
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stride(&self) -> u32 {
self.stride
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn timestamp(&self) -> Timestamp {
self.timestamp
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Error)]
#[non_exhaustive]
pub enum RgbFrameError {
#[error("stride ({stride}) is smaller than width*3 ({min_stride})")]
StrideTooSmall {
width: u32,
stride: u32,
min_stride: u32,
},
#[error("data length {actual} is less than required {expected} bytes")]
DataTooShort {
expected: usize,
actual: usize,
},
#[error("width ({width}) * 3 overflows u32")]
WidthOverflow {
width: u32,
},
#[error("frame dimensions overflow usize: stride ({stride}) * height ({height})")]
DimensionsOverflow {
stride: u32,
height: u32,
},
}
#[derive(Debug, Clone, Copy)]
pub struct HsvFrame<'a> {
h: &'a [u8],
s: &'a [u8],
v: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
}
impl<'a> HsvFrame<'a> {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn new(
h: &'a [u8],
s: &'a [u8],
v: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
) -> Self {
match Self::try_new(h, s, v, width, height, stride, timestamp) {
Ok(f) => f,
Err(_) => panic!("invalid HsvFrame dimensions or data length"),
}
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn try_new(
h: &'a [u8],
s: &'a [u8],
v: &'a [u8],
width: u32,
height: u32,
stride: u32,
timestamp: Timestamp,
) -> Result<Self, HsvFrameError> {
if stride < width {
return Err(HsvFrameError::StrideTooSmall { width, stride });
}
let expected = match (stride as usize).checked_mul(height as usize) {
Some(v) => v,
None => return Err(HsvFrameError::DimensionsOverflow { stride, height }),
};
if h.len() < expected {
return Err(HsvFrameError::PlaneTooShort {
plane: HsvPlane::Hue,
expected,
actual: h.len(),
});
}
if s.len() < expected {
return Err(HsvFrameError::PlaneTooShort {
plane: HsvPlane::Saturation,
expected,
actual: s.len(),
});
}
if v.len() < expected {
return Err(HsvFrameError::PlaneTooShort {
plane: HsvPlane::Value,
expected,
actual: v.len(),
});
}
Ok(Self {
h,
s,
v,
width,
height,
stride,
timestamp,
})
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn hue(&self) -> &'a [u8] {
self.h
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn saturation(&self) -> &'a [u8] {
self.s
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn value(&self) -> &'a [u8] {
self.v
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn width(&self) -> u32 {
self.width
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn height(&self) -> u32 {
self.height
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn stride(&self) -> u32 {
self.stride
}
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn timestamp(&self) -> Timestamp {
self.timestamp
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Display)]
#[display("{}", self.as_str())]
pub enum HsvPlane {
Hue,
Saturation,
Value,
}
impl HsvPlane {
#[cfg_attr(not(tarpaulin), inline(always))]
pub const fn as_str(&self) -> &'static str {
match self {
Self::Hue => "hue",
Self::Saturation => "saturation",
Self::Value => "value",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Error)]
#[non_exhaustive]
pub enum HsvFrameError {
#[error("stride ({stride}) is smaller than width ({width})")]
StrideTooSmall {
width: u32,
stride: u32,
},
#[error("{plane} plane has length {actual} but at least {expected} are required")]
PlaneTooShort {
plane: HsvPlane,
expected: usize,
actual: usize,
},
#[error("frame dimensions overflow usize: stride ({stride}) * height ({height})")]
DimensionsOverflow {
stride: u32,
height: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Error)]
#[non_exhaustive]
pub enum LumaFrameError {
#[error("stride ({stride}) is smaller than width ({width})")]
StrideTooSmall {
width: u32,
stride: u32,
},
#[error("data length {actual} is less than required {expected} bytes")]
DataTooShort {
expected: usize,
actual: usize,
},
#[error("frame dimensions overflow usize: stride ({stride}) * height ({height})")]
DimensionsOverflow {
stride: u32,
height: u32,
},
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use core::num::NonZeroU32;
const fn nz(n: u32) -> NonZeroU32 {
match NonZeroU32::new(n) {
Some(v) => v,
None => panic!("zero"),
}
}
#[test]
fn luma_frame_basic() {
let buf = [0u8; 64 * 48];
let tb = Timebase::new(1, nz(1000));
let f = LumaFrame::new(&buf, 64, 48, 64, Timestamp::new(0, tb));
assert_eq!(f.width(), 64);
assert_eq!(f.height(), 48);
assert_eq!(f.stride(), 64);
assert_eq!(f.data().len(), 64 * 48);
}
#[test]
fn luma_frame_with_padding() {
let buf = [0u8; 80 * 48];
let tb = Timebase::new(1, nz(1000));
let f = LumaFrame::new(&buf, 64, 48, 80, Timestamp::new(0, tb));
assert_eq!(f.width(), 64);
assert_eq!(f.stride(), 80);
}
#[test]
#[should_panic(expected = "invalid LumaFrame")]
fn luma_frame_new_panics_on_stride_less_than_width() {
let buf = [0u8; 64 * 48];
let tb = Timebase::new(1, nz(1000));
let _ = LumaFrame::new(&buf, 64, 48, 32, Timestamp::new(0, tb));
}
#[test]
#[should_panic(expected = "invalid LumaFrame")]
fn luma_frame_new_panics_on_short_data() {
let buf = [0u8; 10];
let tb = Timebase::new(1, nz(1000));
let _ = LumaFrame::new(&buf, 64, 48, 64, Timestamp::new(0, tb));
}
#[test]
fn try_new_success() {
let buf = [0u8; 80 * 48];
let tb = Timebase::new(1, nz(1000));
let f = LumaFrame::try_new(&buf, 64, 48, 80, Timestamp::new(0, tb)).expect("valid frame");
assert_eq!(f.width(), 64);
assert_eq!(f.stride(), 80);
}
#[test]
fn try_new_rejects_stride_less_than_width() {
let buf = [0u8; 64 * 48];
let tb = Timebase::new(1, nz(1000));
let err = LumaFrame::try_new(&buf, 64, 48, 32, Timestamp::new(0, tb)).expect_err("should fail");
assert_eq!(
err,
LumaFrameError::StrideTooSmall {
width: 64,
stride: 32,
},
);
}
#[test]
fn try_new_rejects_short_data() {
let buf = [0u8; 10];
let tb = Timebase::new(1, nz(1000));
let err = LumaFrame::try_new(&buf, 64, 48, 64, Timestamp::new(0, tb)).expect_err("should fail");
assert_eq!(
err,
LumaFrameError::DataTooShort {
expected: 64 * 48,
actual: 10,
},
);
}
#[test]
fn luma_frame_error_display() {
let e = LumaFrameError::StrideTooSmall {
width: 64,
stride: 32,
};
assert_eq!(format!("{e}"), "stride (32) is smaller than width (64)");
}
#[test]
fn rgb_frame_basic() {
let buf = [0u8; 4 * 3 * 2];
let tb = Timebase::new(1, nz(1000));
let f = RgbFrame::new(&buf, 4, 2, 12, Timestamp::new(0, tb));
assert_eq!(f.width(), 4);
assert_eq!(f.height(), 2);
assert_eq!(f.stride(), 12);
assert_eq!(f.data().len(), 24);
}
#[test]
fn rgb_frame_with_padding() {
let buf = [0u8; 16 * 2];
let tb = Timebase::new(1, nz(1000));
let f = RgbFrame::new(&buf, 4, 2, 16, Timestamp::new(0, tb));
assert_eq!(f.stride(), 16);
}
#[test]
fn try_new_rgb_rejects_stride_less_than_width_times_3() {
let buf = [0u8; 12 * 2];
let tb = Timebase::new(1, nz(1000));
let err =
RgbFrame::try_new(&buf, 4, 2, 8, Timestamp::new(0, tb)).expect_err("stride 8 < 4*3 = 12");
assert_eq!(
err,
RgbFrameError::StrideTooSmall {
width: 4,
stride: 8,
min_stride: 12,
},
);
}
#[test]
fn try_new_rgb_rejects_short_data() {
let buf = [0u8; 10];
let tb = Timebase::new(1, nz(1000));
let err = RgbFrame::try_new(&buf, 4, 2, 12, Timestamp::new(0, tb)).expect_err("should fail");
assert_eq!(
err,
RgbFrameError::DataTooShort {
expected: 24,
actual: 10,
},
);
}
#[test]
#[should_panic(expected = "invalid RgbFrame")]
fn rgb_frame_new_panics_on_invalid() {
let buf = [0u8; 10];
let tb = Timebase::new(1, nz(1000));
let _ = RgbFrame::new(&buf, 4, 2, 12, Timestamp::new(0, tb));
}
#[test]
fn rgb_frame_try_new_rejects_width_times_three_overflow() {
let buf = [0u8; 0];
let tb = Timebase::new(1, nz(1000));
let bad_w = u32::MAX / 3 + 1;
let err = RgbFrame::try_new(&buf, bad_w, 1, u32::MAX, Timestamp::new(0, tb))
.expect_err("width*3 should overflow");
assert_eq!(err, RgbFrameError::WidthOverflow { width: bad_w });
}
#[test]
fn hsv_frame_basic_accessors() {
let h = vec![10u8; 64 * 48];
let s = vec![20u8; 64 * 48];
let v = vec![30u8; 64 * 48];
let tb = Timebase::new(1, nz(1000));
let ts = Timestamp::new(42, tb);
let f = HsvFrame::new(&h, &s, &v, 64, 48, 64, ts);
assert_eq!(f.width(), 64);
assert_eq!(f.height(), 48);
assert_eq!(f.stride(), 64);
assert_eq!(f.timestamp(), ts);
assert_eq!(f.hue().len(), 64 * 48);
assert_eq!(f.saturation().len(), 64 * 48);
assert_eq!(f.value().len(), 64 * 48);
assert_eq!(f.hue()[0], 10);
assert_eq!(f.saturation()[0], 20);
assert_eq!(f.value()[0], 30);
}
#[test]
fn hsv_frame_try_new_rejects_stride_less_than_width() {
let h = vec![0u8; 16];
let tb = Timebase::new(1, nz(1000));
let err =
HsvFrame::try_new(&h, &h, &h, 64, 1, 32, Timestamp::new(0, tb)).expect_err("should fail");
assert_eq!(
err,
HsvFrameError::StrideTooSmall {
width: 64,
stride: 32
}
);
}
#[test]
fn hsv_frame_try_new_reports_which_plane_is_short() {
let full = vec![0u8; 64 * 48];
let short = vec![0u8; 10];
let tb = Timebase::new(1, nz(1000));
let ts = Timestamp::new(0, tb);
let err = HsvFrame::try_new(&short, &full, &full, 64, 48, 64, ts).expect_err("h too short");
assert_eq!(
err,
HsvFrameError::PlaneTooShort {
plane: HsvPlane::Hue,
expected: 64 * 48,
actual: 10,
},
);
let err = HsvFrame::try_new(&full, &short, &full, 64, 48, 64, ts).expect_err("s too short");
assert_eq!(
err,
HsvFrameError::PlaneTooShort {
plane: HsvPlane::Saturation,
expected: 64 * 48,
actual: 10,
},
);
let err = HsvFrame::try_new(&full, &full, &short, 64, 48, 64, ts).expect_err("v too short");
assert_eq!(
err,
HsvFrameError::PlaneTooShort {
plane: HsvPlane::Value,
expected: 64 * 48,
actual: 10,
},
);
}
#[test]
#[should_panic(expected = "invalid HsvFrame")]
fn hsv_frame_new_panics_on_invalid() {
let h = vec![0u8; 10];
let tb = Timebase::new(1, nz(1000));
let _ = HsvFrame::new(&h, &h, &h, 64, 48, 64, Timestamp::new(0, tb));
}
#[test]
fn hsv_plane_display_and_as_str() {
assert_eq!(HsvPlane::Hue.as_str(), "hue");
assert_eq!(HsvPlane::Saturation.as_str(), "saturation");
assert_eq!(HsvPlane::Value.as_str(), "value");
assert_eq!(format!("{}", HsvPlane::Hue), "hue");
assert_eq!(format!("{}", HsvPlane::Saturation), "saturation");
assert_eq!(format!("{}", HsvPlane::Value), "value");
}
#[test]
fn hsv_frame_error_display_variants() {
let e = HsvFrameError::StrideTooSmall {
width: 10,
stride: 5,
};
assert!(format!("{e}").contains("smaller than width"));
let e = HsvFrameError::PlaneTooShort {
plane: HsvPlane::Saturation,
expected: 100,
actual: 50,
};
let s = format!("{e}");
assert!(s.contains("saturation"));
assert!(s.contains("100"));
assert!(s.contains("50"));
}
#[test]
fn frame_error_displays_include_key_fields() {
let e = RgbFrameError::StrideTooSmall {
width: 4,
stride: 8,
min_stride: 12,
};
assert!(format!("{e}").contains("12"));
let e = RgbFrameError::DataTooShort {
expected: 24,
actual: 10,
};
assert!(format!("{e}").contains("24"));
let e = RgbFrameError::DimensionsOverflow {
stride: 1,
height: 1,
};
assert!(format!("{e}").contains("overflow"));
let e = LumaFrameError::DataTooShort {
expected: 24,
actual: 10,
};
assert!(format!("{e}").contains("24"));
let e = LumaFrameError::DimensionsOverflow {
stride: 1,
height: 1,
};
assert!(format!("{e}").contains("overflow"));
let e = HsvFrameError::DimensionsOverflow {
stride: 1,
height: 1,
};
assert!(format!("{e}").contains("overflow"));
}
}