use core::num::{NonZeroU8, NonZeroUsize};
use crate::chroma::ChromaSubsampling;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlaneGeometry {
width: NonZeroUsize,
height: NonZeroUsize,
stride: NonZeroUsize,
pad_left: usize,
pad_right: usize,
pad_top: usize,
pad_bottom: usize,
subsampling_x: NonZeroU8,
subsampling_y: NonZeroU8,
}
impl PlaneGeometry {
#[inline]
#[must_use]
#[expect(clippy::too_many_arguments)]
pub fn new(
width: usize,
height: usize,
pad_left: usize,
pad_right: usize,
pad_top: usize,
pad_bottom: usize,
subsampling_x: u8,
subsampling_y: u8,
) -> Option<Self> {
let width = NonZeroUsize::new(width)?;
let height = NonZeroUsize::new(height)?;
let subsampling_x = NonZeroU8::new(subsampling_x)?;
let subsampling_y = NonZeroU8::new(subsampling_y)?;
let stride = width.checked_add(pad_left)?.checked_add(pad_right)?;
Some(Self {
width,
height,
stride,
pad_left,
pad_right,
pad_top,
pad_bottom,
subsampling_x,
subsampling_y,
})
}
#[inline]
#[must_use]
pub fn unpadded(
width: usize,
height: usize,
subsampling_x: u8,
subsampling_y: u8,
) -> Option<Self> {
Self::new(width, height, 0, 0, 0, 0, subsampling_x, subsampling_y)
}
#[inline]
#[must_use]
pub fn width(&self) -> usize {
self.width.get()
}
#[inline]
#[must_use]
pub fn height(&self) -> usize {
self.height.get()
}
#[inline]
#[must_use]
pub fn stride(&self) -> usize {
self.stride.get()
}
#[inline]
#[must_use]
pub fn pad_left(&self) -> usize {
self.pad_left
}
#[inline]
#[must_use]
pub fn pad_right(&self) -> usize {
self.pad_right
}
#[inline]
#[must_use]
pub fn pad_top(&self) -> usize {
self.pad_top
}
#[inline]
#[must_use]
pub fn pad_bottom(&self) -> usize {
self.pad_bottom
}
#[inline]
#[must_use]
pub fn subsampling_x(&self) -> u8 {
self.subsampling_x.get()
}
#[inline]
#[must_use]
pub fn subsampling_y(&self) -> u8 {
self.subsampling_y.get()
}
#[inline]
pub fn for_subsampling(
self,
subsampling: ChromaSubsampling,
) -> Result<Option<Self>, SubsamplingError> {
match subsampling {
ChromaSubsampling::Monochrome => Ok(None),
ChromaSubsampling::Yuv444 => Ok(Some(self)),
ChromaSubsampling::Yuv420 => self.subsampled::<2, 2>().map(Some),
ChromaSubsampling::Yuv422 => self.subsampled::<2, 1>().map(Some),
}
}
#[inline]
fn subsampled<const X: usize, const Y: usize>(self) -> Result<Self, SubsamplingError> {
let x = const { NonZeroUsize::new(X).expect("X nonzero") };
let y = const { NonZeroUsize::new(Y).expect("Y nonzero") };
if self.width.get() % x != 0
|| self.height.get() % y != 0
|| self.pad_left % x != 0
|| self.pad_right % x != 0
|| self.pad_top % y != 0
|| self.pad_bottom % y != 0
{
return Err(SubsamplingError);
}
const { assert!(X <= u8::MAX as usize && Y <= u8::MAX as usize) };
Self::new(
self.width.get() / x,
self.height.get() / y,
self.pad_left / x,
self.pad_right / x,
self.pad_top / y,
self.pad_bottom / y,
X as u8,
Y as u8,
)
.ok_or(SubsamplingError)
}
#[inline]
#[must_use]
pub fn data_origin(&self) -> usize {
self.stride() * self.pad_top + self.pad_left
}
#[inline]
#[must_use]
pub fn alloc_height(&self) -> usize {
self.height() + self.pad_top + self.pad_bottom
}
#[inline]
#[must_use]
pub fn alloc_size(&self) -> usize {
self.alloc_height() * self.stride()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubsamplingError;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unpadded() {
assert!(PlaneGeometry::unpadded(0, 1080, 1, 1).is_none());
assert!(PlaneGeometry::unpadded(1920, 0, 1, 1).is_none());
assert!(PlaneGeometry::unpadded(1920, 1080, 0, 1).is_none());
assert!(PlaneGeometry::unpadded(1920, 1080, 1, 0).is_none());
let g = PlaneGeometry::unpadded(1920, 1080, 1, 1).expect("unpadded geometry works");
assert_eq!(g.width(), 1920);
assert_eq!(g.height(), 1080);
assert_eq!(g.stride, g.width);
assert_eq!(g.pad_left(), 0);
assert_eq!(g.pad_right(), 0);
assert_eq!(g.pad_top(), 0);
assert_eq!(g.pad_bottom(), 0);
assert_eq!(g.subsampling_x(), 1);
assert_eq!(g.subsampling_y(), 1);
}
#[test]
fn new() {
assert!(PlaneGeometry::new(0, 1080, 40, 30, 20, 10, 1, 1).is_none());
assert!(PlaneGeometry::new(1920, 0, 40, 30, 20, 10, 1, 1).is_none());
assert!(PlaneGeometry::new(1920, 1080, 40, 30, 20, 10, 0, 1).is_none());
assert!(PlaneGeometry::new(1920, 1080, 40, 30, 20, 10, 1, 0).is_none());
let g = PlaneGeometry::new(1920, 1080, 40, 30, 20, 10, 1, 1).expect("new geometry works");
assert_eq!(g.width(), 1920);
assert_eq!(g.height(), 1080);
assert_eq!(g.stride(), 1920 + 40 + 30);
assert_eq!(g.pad_left(), 40);
assert_eq!(g.pad_right(), 30);
assert_eq!(g.pad_top(), 20);
assert_eq!(g.pad_bottom(), 10);
assert_eq!(g.subsampling_x(), 1);
assert_eq!(g.subsampling_y(), 1);
}
#[test]
fn new_overflow() {
assert!(PlaneGeometry::new(usize::MAX - 50, 1080, 30, 30, 0, 0, 1, 1).is_none());
}
#[test]
fn subsampled() {
let g = PlaneGeometry::new(1920, 1080, 40, 30, 20, 10, 1, 1).expect("can make geometry");
assert_eq!(g.for_subsampling(ChromaSubsampling::Monochrome), Ok(None));
let sub = g
.for_subsampling(ChromaSubsampling::Yuv444)
.expect("can 'subsample' to Yuv444")
.expect("subsampled plane exists for Yuv444");
assert_eq!(sub, g);
let sub = g
.for_subsampling(ChromaSubsampling::Yuv422)
.expect("can subsample to Yuv422")
.expect("subsampled plane exists for Yuv422");
assert_eq!(sub.width(), 960);
assert_eq!(sub.height(), 1080);
assert_eq!(sub.stride(), 960 + 20 + 15);
assert_eq!(sub.pad_left(), 20);
assert_eq!(sub.pad_right(), 15);
assert_eq!(sub.pad_top(), 20);
assert_eq!(sub.pad_bottom(), 10);
assert_eq!(sub.subsampling_x(), 2);
assert_eq!(sub.subsampling_y(), 1);
let sub = g
.for_subsampling(ChromaSubsampling::Yuv420)
.expect("can subsample to Yuv420")
.expect("subsampled plane exists for Yuv420");
assert_eq!(sub.width(), 960);
assert_eq!(sub.height(), 540);
assert_eq!(sub.stride(), 960 + 20 + 15);
assert_eq!(sub.pad_left, 20);
assert_eq!(sub.pad_right, 15);
assert_eq!(sub.pad_top, 10);
assert_eq!(sub.pad_bottom, 5);
assert_eq!(sub.subsampling_x(), 2);
assert_eq!(sub.subsampling_y(), 2);
}
#[test]
#[expect(clippy::unwrap_used)]
fn subsampled_reject() {
let g = PlaneGeometry::unpadded(1921, 1080, 1, 1).unwrap();
assert!(g.for_subsampling(ChromaSubsampling::Monochrome).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv444).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv422).is_err());
assert!(g.for_subsampling(ChromaSubsampling::Yuv420).is_err());
let g = PlaneGeometry::unpadded(1920, 1081, 1, 1).unwrap();
assert!(g.for_subsampling(ChromaSubsampling::Monochrome).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv444).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv422).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv420).is_err());
let g = PlaneGeometry::unpadded(17, 41, 1, 1).unwrap();
assert!(g.for_subsampling(ChromaSubsampling::Monochrome).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv444).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv422).is_err());
assert!(g.for_subsampling(ChromaSubsampling::Yuv420).is_err());
let g = PlaneGeometry::new(1920, 1080, 10, 7, 40, 20, 1, 1).unwrap();
assert!(g.for_subsampling(ChromaSubsampling::Monochrome).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv444).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv422).is_err());
assert!(g.for_subsampling(ChromaSubsampling::Yuv420).is_err());
let g = PlaneGeometry::new(1920, 1080, 10, 10, 40, 15, 1, 1).unwrap();
assert!(g.for_subsampling(ChromaSubsampling::Monochrome).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv444).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv422).is_ok());
assert!(g.for_subsampling(ChromaSubsampling::Yuv420).is_err());
}
}