use serde::{Deserialize, Serialize};
use std::fmt;
crate::ef_vocabulary! {
#[derive(Serialize, Deserialize)]
#[non_exhaustive]
pub enum PixelFormat {
Rgb = 1, "rgb8", RGB,
Rgba = 2, "rgba8", RGBA,
Bgra = 3, "bgra8", BGRA,
Grey = 4, "mono8", GREY,
Yuyv = 5, "YUYV", YUYV,
Vyuy = 6, "VYUY", VYUY,
Nv12 = 7, "NV12", NV12,
Nv16 = 8, "NV16", NV16,
PlanarRgb = 9, "rgb8_planar", PLANAR_RGB,
PlanarRgba = 10, "rgba8_planar", PLANAR_RGBA,
Nv24 = 11, "NV24", NV24,
}
#[doc(hidden)]
pub mod pixel_format_wire;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PixelLayout {
Packed,
Planar,
SemiPlanar,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChromaLayout {
pub shift_x: u32,
pub shift_y: u32,
pub uv_rows_per_luma: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PlaneGeometry {
pub offset: u64,
pub stride: u64,
pub size: u64,
}
const FOURCC_RGB: u32 = u32::from_le_bytes(*b"RGB ");
const FOURCC_RGBA: u32 = u32::from_le_bytes(*b"RGBA");
const FOURCC_BGRA: u32 = u32::from_le_bytes(*b"BGRA");
const FOURCC_GREY: u32 = u32::from_le_bytes(*b"Y800");
const FOURCC_YUYV: u32 = u32::from_le_bytes(*b"YUYV");
const FOURCC_VYUY: u32 = u32::from_le_bytes(*b"VYUY");
const FOURCC_NV12: u32 = u32::from_le_bytes(*b"NV12");
const FOURCC_NV16: u32 = u32::from_le_bytes(*b"NV16");
const FOURCC_NV24: u32 = u32::from_le_bytes(*b"NV24");
impl PixelFormat {
pub const fn channels(&self) -> usize {
match self {
Self::Rgb | Self::PlanarRgb => 3,
Self::Rgba | Self::Bgra | Self::PlanarRgba => 4,
Self::Grey | Self::Nv12 | Self::Nv16 | Self::Nv24 => 1,
Self::Yuyv | Self::Vyuy => 2,
}
}
pub const fn layout(&self) -> PixelLayout {
match self {
Self::Rgb | Self::Rgba | Self::Bgra | Self::Grey | Self::Yuyv | Self::Vyuy => {
PixelLayout::Packed
}
Self::PlanarRgb | Self::PlanarRgba => PixelLayout::Planar,
Self::Nv12 | Self::Nv16 | Self::Nv24 => PixelLayout::SemiPlanar,
}
}
pub fn allocation_shape(&self, width: usize, height: usize) -> Option<Vec<usize>> {
match self.layout() {
PixelLayout::Packed => Some(vec![height, width, self.channels()]),
PixelLayout::Planar => Some(vec![self.channels(), height, width]),
PixelLayout::SemiPlanar => {
Some(vec![self.combined_plane_height(height)?, width])
}
}
}
pub fn addressing_shape(&self, width: usize, height: usize) -> Option<Vec<usize>> {
match self.layout() {
PixelLayout::Packed if self.channels() == 1 => Some(vec![height, width]),
PixelLayout::Packed => Some(vec![height, width, self.channels()]),
PixelLayout::Planar => Some(vec![self.channels(), height, width]),
PixelLayout::SemiPlanar => {
self.combined_plane_height(height)?;
Some(vec![height, width])
}
}
}
pub fn plane_table(
&self,
width: usize,
height: usize,
row_stride: usize,
) -> Option<Vec<PlaneGeometry>> {
let _ = width; let stride = u64::try_from(row_stride).ok()?;
let rows = u64::try_from(height).ok()?;
let plane_bytes = stride.checked_mul(rows)?;
match self.layout() {
PixelLayout::Packed => Some(vec![PlaneGeometry {
offset: 0,
stride,
size: plane_bytes,
}]),
PixelLayout::Planar => {
let n = u64::try_from(self.channels()).ok()?;
(0..n)
.map(|i| {
Some(PlaneGeometry {
offset: i.checked_mul(plane_bytes)?,
stride,
size: plane_bytes,
})
})
.collect()
}
PixelLayout::SemiPlanar => {
let total_rows = u64::try_from(self.combined_plane_height(height)?).ok()?;
let chroma_rows = total_rows.checked_sub(rows)?;
Some(vec![
PlaneGeometry {
offset: 0,
stride,
size: plane_bytes,
},
PlaneGeometry {
offset: plane_bytes,
stride,
size: stride.checked_mul(chroma_rows)?,
},
])
}
}
}
pub const fn combined_plane_height(&self, height: usize) -> Option<usize> {
match self {
PixelFormat::Nv12 => Some(height + height.div_ceil(2)),
PixelFormat::Nv16 => Some(height * 2),
PixelFormat::Nv24 => Some(height * 3),
_ => None,
}
}
pub fn image_height_from_combined(&self, combined: usize) -> Option<usize> {
let candidate = match self {
PixelFormat::Nv12 => combined.checked_mul(2)? / 3,
PixelFormat::Nv16 => combined / 2,
PixelFormat::Nv24 => combined / 3,
_ => return None,
};
(self.combined_plane_height(candidate) == Some(combined)).then_some(candidate)
}
pub const fn chroma_layout(&self) -> Option<ChromaLayout> {
match self {
PixelFormat::Nv12 => Some(ChromaLayout {
shift_x: 1,
shift_y: 1,
uv_rows_per_luma: 1,
}),
PixelFormat::Nv16 => Some(ChromaLayout {
shift_x: 1,
shift_y: 0,
uv_rows_per_luma: 1,
}),
PixelFormat::Nv24 => Some(ChromaLayout {
shift_x: 0,
shift_y: 0,
uv_rows_per_luma: 2,
}),
_ => None,
}
}
pub fn semi_planar_surface_dims(
&self,
width: usize,
height: usize,
bpe: usize,
) -> Option<(usize, usize)> {
let total_h = self.combined_plane_height(height)?;
let pitch_width = (width * bpe).next_multiple_of(64) / bpe;
Some((pitch_width, total_h))
}
pub const fn is_yuv(&self) -> bool {
matches!(
self,
Self::Yuyv | Self::Vyuy | Self::Nv12 | Self::Nv16 | Self::Nv24
)
}
pub const fn has_alpha(&self) -> bool {
matches!(self, Self::Rgba | Self::Bgra | Self::PlanarRgba)
}
pub const fn to_fourcc(&self) -> u32 {
match self {
Self::Rgb => FOURCC_RGB,
Self::Rgba => FOURCC_RGBA,
Self::Bgra => FOURCC_BGRA,
Self::Grey => FOURCC_GREY,
Self::Yuyv => FOURCC_YUYV,
Self::Vyuy => FOURCC_VYUY,
Self::Nv12 => FOURCC_NV12,
Self::Nv16 => FOURCC_NV16,
Self::Nv24 => FOURCC_NV24,
Self::PlanarRgb | Self::PlanarRgba => 0,
}
}
pub const fn from_fourcc(fourcc: u32) -> Option<Self> {
match fourcc {
FOURCC_RGB => Some(Self::Rgb),
FOURCC_RGBA => Some(Self::Rgba),
FOURCC_BGRA => Some(Self::Bgra),
FOURCC_GREY => Some(Self::Grey),
FOURCC_YUYV => Some(Self::Yuyv),
FOURCC_VYUY => Some(Self::Vyuy),
FOURCC_NV12 => Some(Self::Nv12),
FOURCC_NV16 => Some(Self::Nv16),
FOURCC_NV24 => Some(Self::Nv24),
_ => None,
}
}
}
impl fmt::Display for PixelFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fcc = self.to_fourcc();
if fcc != 0 {
let bytes = fcc.to_le_bytes();
for &b in &bytes {
if b == b' ' {
break;
}
write!(f, "{}", b as char)?;
}
Ok(())
} else {
write!(f, "{self:?}")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn addressing_shape_is_the_grid_not_the_allocation() {
assert_eq!(
PixelFormat::Nv12.addressing_shape(640, 480).unwrap(),
vec![480, 640]
);
assert_eq!(
PixelFormat::Rgb.addressing_shape(640, 480).unwrap(),
vec![480, 640, 3]
);
assert_eq!(
PixelFormat::PlanarRgb.addressing_shape(640, 480).unwrap(),
vec![3, 480, 640]
);
assert_eq!(
PixelFormat::Grey.addressing_shape(4, 2).unwrap(),
vec![2, 4]
);
}
#[test]
fn allocation_shape_still_sizes_the_buffer() {
assert_eq!(
PixelFormat::Nv12.allocation_shape(640, 480).unwrap(),
vec![720, 640]
);
assert_eq!(
PixelFormat::Nv12.allocation_shape(640, 483).unwrap(),
vec![725, 640]
);
for f in [PixelFormat::Rgb, PixelFormat::PlanarRgb, PixelFormat::Rgba] {
assert_eq!(
f.allocation_shape(64, 48),
f.addressing_shape(64, 48),
"{f:?}: allocation and grid coincide when nothing is subsampled"
);
}
assert_eq!(
PixelFormat::Grey.allocation_shape(64, 48).unwrap(),
vec![48, 64, 1]
);
assert_eq!(
PixelFormat::Grey.addressing_shape(64, 48).unwrap(),
vec![48, 64]
);
let a: usize = PixelFormat::Grey
.allocation_shape(64, 48)
.unwrap()
.iter()
.product();
let g: usize = PixelFormat::Grey
.addressing_shape(64, 48)
.unwrap()
.iter()
.product();
assert_eq!(
a, g,
"dropping a 1-sized dimension cannot change the element count"
);
}
#[test]
fn the_grid_is_not_the_buffer_size_for_subsampled_formats() {
let grid: usize = PixelFormat::Nv12
.addressing_shape(640, 480)
.unwrap()
.iter()
.product();
let alloc: usize = PixelFormat::Nv12
.allocation_shape(640, 480)
.unwrap()
.iter()
.product();
assert_eq!(grid, 640 * 480, "the grid addresses luma samples only");
assert_eq!(alloc, 640 * 480 * 3 / 2, "the buffer holds luma + chroma");
assert!(
grid < alloc,
"product(shape) * dtype_size is NOT the buffer size"
);
}
#[test]
fn nv12_plane_table_matches_the_schemas_golden() {
let planes = PixelFormat::Nv12.plane_table(640, 480, 640).unwrap();
assert_eq!(planes.len(), 2);
assert_eq!(planes[0].offset, 0);
assert_eq!(planes[0].stride, 640);
assert_eq!(planes[0].size, 640 * 480);
assert_eq!(planes[1].offset, 640 * 480);
assert_eq!(planes[1].stride, 640);
assert_eq!(planes[1].size, 640 * 480 / 2);
}
#[test]
fn plane_counts_follow_the_spec_table() {
assert_eq!(PixelFormat::Rgb.plane_table(64, 48, 192).unwrap().len(), 1);
assert_eq!(PixelFormat::Grey.plane_table(64, 48, 64).unwrap().len(), 1);
assert_eq!(
PixelFormat::PlanarRgb
.plane_table(64, 48, 64)
.unwrap()
.len(),
3
);
assert_eq!(PixelFormat::Nv12.plane_table(64, 48, 64).unwrap().len(), 2);
}
#[test]
fn plane_table_honours_a_padded_luma_stride() {
let planes = PixelFormat::Nv12.plane_table(640, 480, 704).unwrap();
assert_eq!(planes[0].stride, 704);
assert_eq!(
planes[1].offset,
704 * 480,
"chroma starts after the PADDED luma plane, not after w*h"
);
assert_eq!(planes[1].size, 704 * 240);
}
#[test]
fn plane_table_extent_agrees_with_allocation_shape() {
for (w, h) in [(640, 480), (789, 383), (64, 1)] {
let stride = w; let planes = PixelFormat::Nv12.plane_table(w, h, stride).unwrap();
let total: u64 = planes.iter().map(|p| p.size).sum();
let rows = PixelFormat::Nv12.combined_plane_height(h).unwrap();
assert_eq!(
total,
(stride * rows) as u64,
"{w}x{h}: plane bytes must equal combined_plane_height * stride"
);
}
}
#[test]
fn plane_table_rejects_overflow_rather_than_wrapping() {
assert!(PixelFormat::Nv12
.plane_table(usize::MAX, usize::MAX, usize::MAX)
.is_none());
}
#[test]
fn channels() {
assert_eq!(PixelFormat::Rgb.channels(), 3);
assert_eq!(PixelFormat::Rgba.channels(), 4);
assert_eq!(PixelFormat::Bgra.channels(), 4);
assert_eq!(PixelFormat::Grey.channels(), 1);
assert_eq!(PixelFormat::Yuyv.channels(), 2);
assert_eq!(PixelFormat::Vyuy.channels(), 2);
assert_eq!(PixelFormat::Nv12.channels(), 1);
assert_eq!(PixelFormat::Nv16.channels(), 1);
assert_eq!(PixelFormat::Nv24.channels(), 1);
assert_eq!(PixelFormat::PlanarRgb.channels(), 3);
assert_eq!(PixelFormat::PlanarRgba.channels(), 4);
}
#[test]
fn layout() {
assert_eq!(PixelFormat::Rgb.layout(), PixelLayout::Packed);
assert_eq!(PixelFormat::Rgba.layout(), PixelLayout::Packed);
assert_eq!(PixelFormat::Bgra.layout(), PixelLayout::Packed);
assert_eq!(PixelFormat::Grey.layout(), PixelLayout::Packed);
assert_eq!(PixelFormat::Yuyv.layout(), PixelLayout::Packed);
assert_eq!(PixelFormat::Vyuy.layout(), PixelLayout::Packed);
assert_eq!(PixelFormat::Nv12.layout(), PixelLayout::SemiPlanar);
assert_eq!(PixelFormat::Nv16.layout(), PixelLayout::SemiPlanar);
assert_eq!(PixelFormat::Nv24.layout(), PixelLayout::SemiPlanar);
assert_eq!(PixelFormat::PlanarRgb.layout(), PixelLayout::Planar);
assert_eq!(PixelFormat::PlanarRgba.layout(), PixelLayout::Planar);
}
#[test]
fn is_yuv() {
assert!(!PixelFormat::Rgb.is_yuv());
assert!(!PixelFormat::Rgba.is_yuv());
assert!(PixelFormat::Yuyv.is_yuv());
assert!(PixelFormat::Vyuy.is_yuv());
assert!(PixelFormat::Nv12.is_yuv());
assert!(PixelFormat::Nv16.is_yuv());
assert!(PixelFormat::Nv24.is_yuv());
assert!(!PixelFormat::PlanarRgb.is_yuv());
}
#[test]
fn has_alpha() {
assert!(!PixelFormat::Rgb.has_alpha());
assert!(PixelFormat::Rgba.has_alpha());
assert!(PixelFormat::Bgra.has_alpha());
assert!(!PixelFormat::Grey.has_alpha());
assert!(!PixelFormat::Yuyv.has_alpha());
assert!(!PixelFormat::PlanarRgb.has_alpha());
assert!(PixelFormat::PlanarRgba.has_alpha());
}
#[test]
fn fourcc_roundtrip() {
for fmt in [
PixelFormat::Rgb,
PixelFormat::Rgba,
PixelFormat::Bgra,
PixelFormat::Grey,
PixelFormat::Yuyv,
PixelFormat::Vyuy,
PixelFormat::Nv12,
PixelFormat::Nv16,
PixelFormat::Nv24,
] {
let fcc = fmt.to_fourcc();
assert_ne!(fcc, 0, "{fmt:?} should have a fourcc code");
assert_eq!(
PixelFormat::from_fourcc(fcc),
Some(fmt),
"roundtrip failed for {fmt:?}"
);
}
}
#[test]
fn combined_plane_height_inverts_for_every_semi_planar_height() {
for fmt in [PixelFormat::Nv12, PixelFormat::Nv16, PixelFormat::Nv24] {
for h in 1..=64usize {
let combined = fmt.combined_plane_height(h).unwrap();
assert_eq!(
fmt.image_height_from_combined(combined),
Some(h),
"{fmt:?} height {h} -> combined {combined}"
);
}
}
}
#[test]
fn image_height_from_combined_rejects_unproduced_heights() {
assert_eq!(PixelFormat::Nv12.image_height_from_combined(721), None);
assert_eq!(PixelFormat::Nv16.image_height_from_combined(7), None);
assert_eq!(PixelFormat::Nv24.image_height_from_combined(8), None);
}
#[test]
fn image_height_from_combined_is_none_for_non_semi_planar() {
for fmt in [
PixelFormat::Rgba,
PixelFormat::Rgb,
PixelFormat::Grey,
PixelFormat::Yuyv,
PixelFormat::PlanarRgb,
] {
assert_eq!(fmt.image_height_from_combined(12), None, "{fmt:?}");
}
}
#[test]
fn fourcc_planar_returns_zero() {
assert_eq!(PixelFormat::PlanarRgb.to_fourcc(), 0);
assert_eq!(PixelFormat::PlanarRgba.to_fourcc(), 0);
}
#[test]
fn from_fourcc_unknown() {
assert_eq!(PixelFormat::from_fourcc(0), None);
assert_eq!(PixelFormat::from_fourcc(0xDEADBEEF), None);
}
#[test]
fn display_fourcc_formats() {
assert_eq!(format!("{}", PixelFormat::Rgba), "RGBA");
assert_eq!(format!("{}", PixelFormat::Nv12), "NV12");
assert_eq!(format!("{}", PixelFormat::Yuyv), "YUYV");
assert_eq!(format!("{}", PixelFormat::Grey), "Y800");
}
#[test]
fn display_planar_formats() {
assert_eq!(format!("{}", PixelFormat::PlanarRgb), "PlanarRgb");
assert_eq!(format!("{}", PixelFormat::PlanarRgba), "PlanarRgba");
}
#[test]
fn repr_starts_at_one() {
assert_eq!(PixelFormat::Rgb as u8, 1);
}
#[test]
fn serde_roundtrip() {
let fmt = PixelFormat::Nv12;
let json = serde_json::to_string(&fmt).unwrap();
let back: PixelFormat = serde_json::from_str(&json).unwrap();
assert_eq!(fmt, back);
}
}