use core::fmt;
use crate::descriptor::ChannelType;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[repr(u8)]
pub enum Subsampling {
#[default]
S444 = 0,
S422 = 1,
S420 = 2,
S411 = 3,
}
impl Subsampling {
#[inline]
#[allow(unreachable_patterns)]
pub const fn h_factor(self) -> u8 {
match self {
Self::S444 => 1,
Self::S422 | Self::S420 => 2,
Self::S411 => 4,
_ => 1,
}
}
#[inline]
#[allow(unreachable_patterns)]
pub const fn v_factor(self) -> u8 {
match self {
Self::S420 => 2,
_ => 1,
}
}
#[inline]
pub const fn from_factors(h: u8, v: u8) -> Option<Self> {
match (h, v) {
(1, 1) => Some(Self::S444),
(2, 1) => Some(Self::S422),
(2, 2) => Some(Self::S420),
(4, 1) => Some(Self::S411),
_ => None,
}
}
}
impl fmt::Display for Subsampling {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::S444 => f.write_str("4:4:4"),
Self::S422 => f.write_str("4:2:2"),
Self::S420 => f.write_str("4:2:0"),
Self::S411 => f.write_str("4:1:1"),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[repr(u8)]
pub enum YuvMatrix {
#[default]
Identity = 0,
Bt601 = 1,
Bt709 = 2,
Bt2020 = 3,
}
impl YuvMatrix {
#[allow(unreachable_patterns)]
#[inline]
pub const fn rgb_to_y_coeffs(self) -> [f64; 3] {
match self {
Self::Identity => [1.0, 0.0, 0.0],
Self::Bt601 => [0.299, 0.587, 0.114],
Self::Bt709 => [0.2126, 0.7152, 0.0722],
Self::Bt2020 => [0.2627, 0.6780, 0.0593],
_ => [0.2126, 0.7152, 0.0722],
}
}
#[inline]
pub const fn from_cicp(mc: u8) -> Option<Self> {
match mc {
0 => Some(Self::Identity),
1 => Some(Self::Bt709),
5 | 6 => Some(Self::Bt601),
9 => Some(Self::Bt2020),
_ => None,
}
}
}
impl fmt::Display for YuvMatrix {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Identity => f.write_str("Identity"),
Self::Bt601 => f.write_str("BT.601"),
Self::Bt709 => f.write_str("BT.709"),
Self::Bt2020 => f.write_str("BT.2020"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
#[repr(u8)]
pub enum PlaneSemantic {
Luma = 0,
ChromaCb = 1,
ChromaCr = 2,
Red = 3,
Green = 4,
Blue = 5,
Alpha = 6,
Depth = 7,
GainMap = 8,
Gray = 9,
OklabL = 10,
OklabA = 11,
OklabB = 12,
}
impl PlaneSemantic {
#[inline]
pub const fn is_luminance(self) -> bool {
matches!(self, Self::Luma | Self::Gray | Self::OklabL)
}
#[inline]
pub const fn is_chroma(self) -> bool {
matches!(
self,
Self::ChromaCb | Self::ChromaCr | Self::OklabA | Self::OklabB
)
}
#[inline]
pub const fn is_rgb(self) -> bool {
matches!(self, Self::Red | Self::Green | Self::Blue)
}
#[inline]
pub const fn is_alpha(self) -> bool {
matches!(self, Self::Alpha)
}
}
impl fmt::Display for PlaneSemantic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Luma => f.write_str("Luma"),
Self::ChromaCb => f.write_str("Cb"),
Self::ChromaCr => f.write_str("Cr"),
Self::Red => f.write_str("R"),
Self::Green => f.write_str("G"),
Self::Blue => f.write_str("B"),
Self::Alpha => f.write_str("A"),
Self::Depth => f.write_str("Depth"),
Self::GainMap => f.write_str("GainMap"),
Self::Gray => f.write_str("Gray"),
Self::OklabL => f.write_str("Oklab.L"),
Self::OklabA => f.write_str("Oklab.a"),
Self::OklabB => f.write_str("Oklab.b"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PlaneDescriptor {
pub semantic: PlaneSemantic,
pub channel_type: ChannelType,
pub h_subsample: u8,
pub v_subsample: u8,
}
impl PlaneDescriptor {
#[inline]
pub const fn new(semantic: PlaneSemantic, channel_type: ChannelType) -> Self {
Self {
semantic,
channel_type,
h_subsample: 1,
v_subsample: 1,
}
}
#[inline]
#[must_use]
pub const fn with_subsampling(mut self, h: u8, v: u8) -> Self {
debug_assert!(
h > 0 && h.is_power_of_two(),
"h_subsample must be a power of 2"
);
debug_assert!(
v > 0 && v.is_power_of_two(),
"v_subsample must be a power of 2"
);
self.h_subsample = h;
self.v_subsample = v;
self
}
#[inline]
pub const fn plane_width(&self, ref_width: u32) -> u32 {
ref_width.div_ceil(self.h_subsample as u32)
}
#[inline]
pub const fn plane_height(&self, ref_height: u32) -> u32 {
ref_height.div_ceil(self.v_subsample as u32)
}
#[inline]
pub const fn is_subsampled(&self) -> bool {
self.h_subsample > 1 || self.v_subsample > 1
}
#[inline]
pub const fn bytes_per_sample(&self) -> usize {
self.channel_type.byte_size()
}
}
impl fmt::Display for PlaneDescriptor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.semantic, self.channel_type)?;
if self.is_subsampled() {
write!(f, " (1/{}×1/{})", self.h_subsample, self.v_subsample)?;
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PlaneMask {
bits: u8,
}
impl PlaneMask {
pub const ALL: Self = Self { bits: 0xFF };
pub const NONE: Self = Self { bits: 0 };
pub const LUMA: Self = Self { bits: 0b0001 };
pub const CHROMA: Self = Self { bits: 0b0110 };
pub const ALPHA: Self = Self { bits: 0b1000 };
#[inline]
pub const fn single(idx: usize) -> Self {
debug_assert!(idx < 8, "PlaneMask supports at most 8 planes");
Self {
bits: 1u8 << (idx as u8),
}
}
#[inline]
pub const fn includes(&self, idx: usize) -> bool {
if idx >= 8 {
return false;
}
(self.bits >> (idx as u8)) & 1 != 0
}
#[inline]
pub const fn union(self, other: Self) -> Self {
Self {
bits: self.bits | other.bits,
}
}
#[inline]
pub const fn intersection(self, other: Self) -> Self {
Self {
bits: self.bits & other.bits,
}
}
#[inline]
pub const fn count(&self) -> u32 {
self.bits.count_ones()
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.bits == 0
}
#[inline]
pub const fn bits(&self) -> u8 {
self.bits
}
#[inline]
pub const fn from_bits(bits: u8) -> Self {
Self { bits }
}
}
impl fmt::Display for PlaneMask {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if *self == Self::ALL {
return f.write_str("ALL");
}
if self.is_empty() {
return f.write_str("NONE");
}
let mut first = true;
for i in 0..8 {
if self.includes(i) {
if !first {
f.write_str("|")?;
}
write!(f, "{i}")?;
first = false;
}
}
Ok(())
}
}
pub struct Plane {
pub buffer: crate::buffer::PixelBuffer,
pub semantic: PlaneSemantic,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum PlaneRelationship {
Independent,
YCbCr {
matrix: YuvMatrix,
},
Oklab,
GainMap,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PlaneLayout {
Interleaved {
channels: u8,
},
Planar {
planes: alloc::vec::Vec<PlaneDescriptor>,
relationship: PlaneRelationship,
},
}
impl PlaneLayout {
pub fn ycbcr_444(ct: ChannelType) -> Self {
Self::Planar {
planes: alloc::vec![
PlaneDescriptor::new(PlaneSemantic::Luma, ct),
PlaneDescriptor::new(PlaneSemantic::ChromaCb, ct),
PlaneDescriptor::new(PlaneSemantic::ChromaCr, ct),
],
relationship: PlaneRelationship::YCbCr {
matrix: YuvMatrix::Bt601,
},
}
}
pub fn ycbcr_444_matrix(ct: ChannelType, matrix: YuvMatrix) -> Self {
Self::Planar {
planes: alloc::vec![
PlaneDescriptor::new(PlaneSemantic::Luma, ct),
PlaneDescriptor::new(PlaneSemantic::ChromaCb, ct),
PlaneDescriptor::new(PlaneSemantic::ChromaCr, ct),
],
relationship: PlaneRelationship::YCbCr { matrix },
}
}
pub fn ycbcr_422(ct: ChannelType) -> Self {
Self::Planar {
planes: alloc::vec![
PlaneDescriptor::new(PlaneSemantic::Luma, ct),
PlaneDescriptor::new(PlaneSemantic::ChromaCb, ct).with_subsampling(2, 1),
PlaneDescriptor::new(PlaneSemantic::ChromaCr, ct).with_subsampling(2, 1),
],
relationship: PlaneRelationship::YCbCr {
matrix: YuvMatrix::Bt601,
},
}
}
pub fn ycbcr_420(ct: ChannelType) -> Self {
Self::Planar {
planes: alloc::vec![
PlaneDescriptor::new(PlaneSemantic::Luma, ct),
PlaneDescriptor::new(PlaneSemantic::ChromaCb, ct).with_subsampling(2, 2),
PlaneDescriptor::new(PlaneSemantic::ChromaCr, ct).with_subsampling(2, 2),
],
relationship: PlaneRelationship::YCbCr {
matrix: YuvMatrix::Bt601,
},
}
}
pub fn rgb(ct: ChannelType) -> Self {
Self::Planar {
planes: alloc::vec![
PlaneDescriptor::new(PlaneSemantic::Red, ct),
PlaneDescriptor::new(PlaneSemantic::Green, ct),
PlaneDescriptor::new(PlaneSemantic::Blue, ct),
],
relationship: PlaneRelationship::Independent,
}
}
pub fn rgba(ct: ChannelType) -> Self {
Self::Planar {
planes: alloc::vec![
PlaneDescriptor::new(PlaneSemantic::Red, ct),
PlaneDescriptor::new(PlaneSemantic::Green, ct),
PlaneDescriptor::new(PlaneSemantic::Blue, ct),
PlaneDescriptor::new(PlaneSemantic::Alpha, ct),
],
relationship: PlaneRelationship::Independent,
}
}
pub fn oklab(ct: ChannelType) -> Self {
Self::Planar {
planes: alloc::vec![
PlaneDescriptor::new(PlaneSemantic::OklabL, ct),
PlaneDescriptor::new(PlaneSemantic::OklabA, ct),
PlaneDescriptor::new(PlaneSemantic::OklabB, ct),
],
relationship: PlaneRelationship::Oklab,
}
}
pub fn oklab_alpha(ct: ChannelType) -> Self {
Self::Planar {
planes: alloc::vec![
PlaneDescriptor::new(PlaneSemantic::OklabL, ct),
PlaneDescriptor::new(PlaneSemantic::OklabA, ct),
PlaneDescriptor::new(PlaneSemantic::OklabB, ct),
PlaneDescriptor::new(PlaneSemantic::Alpha, ct),
],
relationship: PlaneRelationship::Oklab,
}
}
pub fn gray(ct: ChannelType) -> Self {
Self::Planar {
planes: alloc::vec![PlaneDescriptor::new(PlaneSemantic::Gray, ct)],
relationship: PlaneRelationship::Independent,
}
}
#[inline]
pub fn plane_count(&self) -> usize {
match self {
Self::Interleaved { channels } => *channels as usize,
Self::Planar { planes, .. } => planes.len(),
}
}
#[inline]
pub fn planes(&self) -> &[PlaneDescriptor] {
match self {
Self::Interleaved { .. } => &[],
Self::Planar { planes, .. } => planes,
}
}
pub fn luma_plane_index(&self) -> Option<usize> {
match self {
Self::Interleaved { .. } => None,
Self::Planar { planes, .. } => planes.iter().position(|p| p.semantic.is_luminance()),
}
}
pub fn has_subsampling(&self) -> bool {
match self {
Self::Interleaved { .. } => false,
Self::Planar { planes, .. } => planes.iter().any(|p| p.is_subsampled()),
}
}
pub fn is_ycbcr(&self) -> bool {
matches!(
self,
Self::Planar {
relationship: PlaneRelationship::YCbCr { .. },
..
}
)
}
pub fn is_oklab(&self) -> bool {
matches!(
self,
Self::Planar {
relationship: PlaneRelationship::Oklab,
..
}
)
}
#[inline]
pub fn is_planar(&self) -> bool {
matches!(self, Self::Planar { .. })
}
pub fn relationship(&self) -> Option<PlaneRelationship> {
match self {
Self::Interleaved { .. } => None,
Self::Planar { relationship, .. } => Some(*relationship),
}
}
pub fn max_v_subsample(&self) -> u8 {
match self {
Self::Interleaved { .. } => 1,
Self::Planar { planes, .. } => planes.iter().map(|p| p.v_subsample).max().unwrap_or(1),
}
}
pub fn max_h_subsample(&self) -> u8 {
match self {
Self::Interleaved { .. } => 1,
Self::Planar { planes, .. } => planes.iter().map(|p| p.h_subsample).max().unwrap_or(1),
}
}
pub fn mask_where(&self, f: impl Fn(PlaneSemantic) -> bool) -> PlaneMask {
match self {
Self::Interleaved { .. } => PlaneMask::NONE,
Self::Planar { planes, .. } => {
let mut bits = 0u8;
for (i, p) in planes.iter().enumerate() {
if i < 8 && f(p.semantic) {
bits |= 1 << (i as u8);
}
}
PlaneMask::from_bits(bits)
}
}
}
pub fn luma_mask(&self) -> PlaneMask {
self.mask_where(|s| s.is_luminance())
}
pub fn chroma_mask(&self) -> PlaneMask {
self.mask_where(|s| s.is_chroma())
}
pub fn alpha_mask(&self) -> PlaneMask {
self.mask_where(|s| s.is_alpha())
}
}
impl fmt::Display for PlaneLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Interleaved { channels } => write!(f, "Interleaved({channels}ch)"),
Self::Planar {
planes,
relationship,
} => {
write!(f, "{relationship:?}[")?;
for (i, p) in planes.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{p}")?;
}
f.write_str("]")
}
}
}
}
pub struct MultiPlaneImage {
layout: PlaneLayout,
buffers: alloc::vec::Vec<crate::buffer::PixelBuffer>,
origin: Option<alloc::sync::Arc<crate::color::ColorContext>>,
}
impl MultiPlaneImage {
pub fn new(layout: PlaneLayout, buffers: alloc::vec::Vec<crate::buffer::PixelBuffer>) -> Self {
debug_assert!(
layout.is_planar(),
"MultiPlaneImage requires a Planar layout"
);
debug_assert_eq!(
layout.plane_count(),
buffers.len(),
"buffer count ({}) must match plane count ({})",
buffers.len(),
layout.plane_count(),
);
Self {
layout,
buffers,
origin: None,
}
}
pub fn with_origin(mut self, ctx: alloc::sync::Arc<crate::color::ColorContext>) -> Self {
self.origin = Some(ctx);
self
}
#[inline]
pub fn layout(&self) -> &PlaneLayout {
&self.layout
}
#[inline]
pub fn plane_count(&self) -> usize {
self.buffers.len()
}
#[inline]
pub fn buffer(&self, idx: usize) -> Option<&crate::buffer::PixelBuffer> {
self.buffers.get(idx)
}
#[inline]
pub fn buffer_mut(&mut self, idx: usize) -> Option<&mut crate::buffer::PixelBuffer> {
self.buffers.get_mut(idx)
}
#[inline]
pub fn buffers(&self) -> &[crate::buffer::PixelBuffer] {
&self.buffers
}
#[inline]
pub fn buffers_mut(&mut self) -> &mut [crate::buffer::PixelBuffer] {
&mut self.buffers
}
#[inline]
pub fn origin(&self) -> Option<&alloc::sync::Arc<crate::color::ColorContext>> {
self.origin.as_ref()
}
}
#[cfg(test)]
mod tests {
use alloc::format;
use core::mem::size_of;
use super::*;
use crate::descriptor::PixelDescriptor;
#[test]
fn subsampling_factors() {
assert_eq!(Subsampling::S444.h_factor(), 1);
assert_eq!(Subsampling::S444.v_factor(), 1);
assert_eq!(Subsampling::S422.h_factor(), 2);
assert_eq!(Subsampling::S422.v_factor(), 1);
assert_eq!(Subsampling::S420.h_factor(), 2);
assert_eq!(Subsampling::S420.v_factor(), 2);
assert_eq!(Subsampling::S411.h_factor(), 4);
assert_eq!(Subsampling::S411.v_factor(), 1);
}
#[test]
fn yuv_matrix_cicp() {
assert_eq!(YuvMatrix::from_cicp(1), Some(YuvMatrix::Bt709));
assert_eq!(YuvMatrix::from_cicp(5), Some(YuvMatrix::Bt601));
assert_eq!(YuvMatrix::from_cicp(9), Some(YuvMatrix::Bt2020));
assert_eq!(YuvMatrix::from_cicp(99), None);
}
#[test]
fn subsampling_from_factors() {
assert_eq!(Subsampling::from_factors(1, 1), Some(Subsampling::S444));
assert_eq!(Subsampling::from_factors(2, 1), Some(Subsampling::S422));
assert_eq!(Subsampling::from_factors(2, 2), Some(Subsampling::S420));
assert_eq!(Subsampling::from_factors(4, 1), Some(Subsampling::S411));
assert_eq!(Subsampling::from_factors(3, 1), None);
assert_eq!(Subsampling::from_factors(1, 2), None);
}
#[test]
fn plane_semantic_classification() {
assert!(PlaneSemantic::Luma.is_luminance());
assert!(PlaneSemantic::Gray.is_luminance());
assert!(PlaneSemantic::OklabL.is_luminance());
assert!(!PlaneSemantic::Red.is_luminance());
assert!(PlaneSemantic::ChromaCb.is_chroma());
assert!(PlaneSemantic::ChromaCr.is_chroma());
assert!(PlaneSemantic::OklabA.is_chroma());
assert!(PlaneSemantic::OklabB.is_chroma());
assert!(!PlaneSemantic::Luma.is_chroma());
assert!(PlaneSemantic::Red.is_rgb());
assert!(PlaneSemantic::Green.is_rgb());
assert!(PlaneSemantic::Blue.is_rgb());
assert!(!PlaneSemantic::Luma.is_rgb());
assert!(PlaneSemantic::Alpha.is_alpha());
assert!(!PlaneSemantic::Luma.is_alpha());
assert!(!PlaneSemantic::Depth.is_alpha());
}
#[test]
fn plane_semantic_display() {
assert_eq!(format!("{}", PlaneSemantic::Luma), "Luma");
assert_eq!(format!("{}", PlaneSemantic::ChromaCb), "Cb");
assert_eq!(format!("{}", PlaneSemantic::Gray), "Gray");
assert_eq!(format!("{}", PlaneSemantic::OklabL), "Oklab.L");
}
#[test]
fn plane_descriptor_full_resolution() {
let d = PlaneDescriptor::new(PlaneSemantic::Luma, ChannelType::F32);
assert_eq!(d.semantic, PlaneSemantic::Luma);
assert_eq!(d.channel_type, ChannelType::F32);
assert!(!d.is_subsampled());
assert_eq!(d.h_subsample, 1);
assert_eq!(d.v_subsample, 1);
assert_eq!(d.plane_width(1920), 1920);
assert_eq!(d.plane_height(1080), 1080);
assert_eq!(d.bytes_per_sample(), 4);
}
#[test]
fn plane_descriptor_subsampled() {
let d =
PlaneDescriptor::new(PlaneSemantic::ChromaCb, ChannelType::U8).with_subsampling(2, 2);
assert!(d.is_subsampled());
assert_eq!(d.plane_width(1920), 960);
assert_eq!(d.plane_height(1080), 540);
assert_eq!(d.plane_width(1921), 961);
assert_eq!(d.plane_height(1081), 541);
assert_eq!(d.bytes_per_sample(), 1);
}
#[test]
fn plane_descriptor_quarter_h() {
let d =
PlaneDescriptor::new(PlaneSemantic::ChromaCr, ChannelType::U16).with_subsampling(4, 1);
assert!(d.is_subsampled());
assert_eq!(d.plane_width(1920), 480);
assert_eq!(d.plane_height(1080), 1080);
assert_eq!(d.bytes_per_sample(), 2);
}
#[test]
fn plane_descriptor_display() {
let d = PlaneDescriptor::new(PlaneSemantic::Luma, ChannelType::F32);
assert_eq!(format!("{d}"), "Luma:F32");
let d =
PlaneDescriptor::new(PlaneSemantic::ChromaCb, ChannelType::U8).with_subsampling(2, 2);
assert_eq!(format!("{d}"), "Cb:U8 (1/2\u{00d7}1/2)");
}
#[test]
fn plane_descriptor_size() {
assert!(size_of::<PlaneDescriptor>() <= 4);
}
#[test]
fn plane_mask_constants() {
assert!(PlaneMask::ALL.includes(0));
assert!(PlaneMask::ALL.includes(7));
assert!(!PlaneMask::NONE.includes(0));
assert!(PlaneMask::NONE.is_empty());
assert!(PlaneMask::LUMA.includes(0));
assert!(!PlaneMask::LUMA.includes(1));
assert!(PlaneMask::CHROMA.includes(1));
assert!(PlaneMask::CHROMA.includes(2));
assert!(!PlaneMask::CHROMA.includes(0));
assert!(PlaneMask::ALPHA.includes(3));
assert!(!PlaneMask::ALPHA.includes(0));
}
#[test]
fn plane_mask_single() {
let m = PlaneMask::single(5);
assert!(m.includes(5));
assert!(!m.includes(4));
assert_eq!(m.count(), 1);
}
#[test]
fn plane_mask_union_intersection() {
let luma_alpha = PlaneMask::LUMA.union(PlaneMask::ALPHA);
assert!(luma_alpha.includes(0));
assert!(luma_alpha.includes(3));
assert!(!luma_alpha.includes(1));
assert_eq!(luma_alpha.count(), 2);
let intersect = luma_alpha.intersection(PlaneMask::LUMA);
assert!(intersect.includes(0));
assert!(!intersect.includes(3));
assert_eq!(intersect.count(), 1);
}
#[test]
fn plane_mask_out_of_range() {
assert!(!PlaneMask::ALL.includes(8));
assert!(!PlaneMask::ALL.includes(100));
}
#[test]
fn plane_mask_bits_roundtrip() {
let m = PlaneMask::LUMA.union(PlaneMask::CHROMA);
let bits = m.bits();
assert_eq!(PlaneMask::from_bits(bits), m);
}
#[test]
fn plane_mask_display() {
assert_eq!(format!("{}", PlaneMask::ALL), "ALL");
assert_eq!(format!("{}", PlaneMask::NONE), "NONE");
assert_eq!(format!("{}", PlaneMask::LUMA), "0");
assert_eq!(
format!("{}", PlaneMask::LUMA.union(PlaneMask::ALPHA)),
"0|3"
);
}
#[test]
fn plane_layout_interleaved() {
let layout = PlaneLayout::Interleaved { channels: 4 };
assert!(!layout.is_planar());
assert_eq!(layout.plane_count(), 4);
assert!(layout.planes().is_empty());
assert!(!layout.has_subsampling());
assert!(!layout.is_ycbcr());
assert!(!layout.is_oklab());
assert_eq!(layout.luma_plane_index(), None);
}
#[test]
fn plane_layout_ycbcr_444() {
let layout = PlaneLayout::ycbcr_444(ChannelType::F32);
assert!(layout.is_planar());
assert!(layout.is_ycbcr());
assert!(!layout.is_oklab());
assert_eq!(layout.plane_count(), 3);
assert!(!layout.has_subsampling());
assert_eq!(layout.luma_plane_index(), Some(0));
assert_eq!(layout.max_v_subsample(), 1);
assert_eq!(layout.max_h_subsample(), 1);
let planes = layout.planes();
assert_eq!(planes[0].semantic, PlaneSemantic::Luma);
assert_eq!(planes[1].semantic, PlaneSemantic::ChromaCb);
assert_eq!(planes[2].semantic, PlaneSemantic::ChromaCr);
assert_eq!(planes[0].channel_type, ChannelType::F32);
}
#[test]
fn plane_layout_ycbcr_420() {
let layout = PlaneLayout::ycbcr_420(ChannelType::U8);
assert!(layout.is_planar());
assert!(layout.is_ycbcr());
assert!(layout.has_subsampling());
assert_eq!(layout.max_v_subsample(), 2);
assert_eq!(layout.max_h_subsample(), 2);
let planes = layout.planes();
assert!(!planes[0].is_subsampled());
assert!(planes[1].is_subsampled());
assert_eq!(planes[1].h_subsample, 2);
assert_eq!(planes[1].v_subsample, 2);
}
#[test]
fn plane_layout_ycbcr_422() {
let layout = PlaneLayout::ycbcr_422(ChannelType::U8);
assert!(layout.has_subsampling());
assert_eq!(layout.max_v_subsample(), 1);
assert_eq!(layout.max_h_subsample(), 2);
let planes = layout.planes();
assert_eq!(planes[1].h_subsample, 2);
assert_eq!(planes[1].v_subsample, 1);
}
#[test]
fn plane_layout_ycbcr_444_matrix() {
let layout = PlaneLayout::ycbcr_444_matrix(ChannelType::U8, YuvMatrix::Bt709);
assert_eq!(
layout.relationship(),
Some(PlaneRelationship::YCbCr {
matrix: YuvMatrix::Bt709
})
);
}
#[test]
fn plane_layout_oklab() {
let layout = PlaneLayout::oklab(ChannelType::F32);
assert!(layout.is_planar());
assert!(layout.is_oklab());
assert!(!layout.is_ycbcr());
assert_eq!(layout.plane_count(), 3);
assert!(!layout.has_subsampling());
assert_eq!(layout.luma_plane_index(), Some(0));
let planes = layout.planes();
assert_eq!(planes[0].semantic, PlaneSemantic::OklabL);
assert_eq!(planes[1].semantic, PlaneSemantic::OklabA);
assert_eq!(planes[2].semantic, PlaneSemantic::OklabB);
assert_eq!(planes[0].channel_type, ChannelType::F32);
}
#[test]
fn plane_layout_oklab_alpha() {
let layout = PlaneLayout::oklab_alpha(ChannelType::F32);
assert!(layout.is_oklab());
assert_eq!(layout.plane_count(), 4);
assert_eq!(layout.planes()[3].semantic, PlaneSemantic::Alpha);
}
#[test]
fn plane_layout_rgb() {
let layout = PlaneLayout::rgb(ChannelType::U8);
assert!(layout.is_planar());
assert!(!layout.is_ycbcr());
assert!(!layout.is_oklab());
assert_eq!(layout.plane_count(), 3);
assert_eq!(layout.relationship(), Some(PlaneRelationship::Independent));
let planes = layout.planes();
assert_eq!(planes[0].semantic, PlaneSemantic::Red);
assert_eq!(planes[1].semantic, PlaneSemantic::Green);
assert_eq!(planes[2].semantic, PlaneSemantic::Blue);
}
#[test]
fn plane_layout_rgba() {
let layout = PlaneLayout::rgba(ChannelType::U8);
assert_eq!(layout.plane_count(), 4);
assert_eq!(layout.planes()[3].semantic, PlaneSemantic::Alpha);
}
#[test]
fn plane_layout_gray() {
let layout = PlaneLayout::gray(ChannelType::U8);
assert!(layout.is_planar());
assert_eq!(layout.plane_count(), 1);
assert_eq!(layout.planes()[0].semantic, PlaneSemantic::Gray);
assert_eq!(layout.luma_plane_index(), Some(0));
}
#[test]
fn plane_layout_display() {
let layout = PlaneLayout::Interleaved { channels: 3 };
assert_eq!(format!("{layout}"), "Interleaved(3ch)");
let layout = PlaneLayout::oklab(ChannelType::F32);
let s = format!("{layout}");
assert!(s.starts_with("Oklab["), "got: {s}");
assert!(s.contains("Oklab.L:F32"), "got: {s}");
}
#[test]
fn multi_plane_image_basic() {
let layout = PlaneLayout::ycbcr_444(ChannelType::U8);
let y = crate::buffer::PixelBuffer::new(64, 64, PixelDescriptor::GRAY8);
let cb = crate::buffer::PixelBuffer::new(64, 64, PixelDescriptor::GRAY8);
let cr = crate::buffer::PixelBuffer::new(64, 64, PixelDescriptor::GRAY8);
let img = MultiPlaneImage::new(layout, alloc::vec![y, cb, cr]);
assert_eq!(img.plane_count(), 3);
assert!(img.layout().is_ycbcr());
assert!(img.buffer(0).is_some());
assert!(img.buffer(2).is_some());
assert!(img.buffer(3).is_none());
assert!(img.origin().is_none());
}
#[test]
fn multi_plane_image_with_origin() {
let layout = PlaneLayout::gray(ChannelType::U8);
let buf = crate::buffer::PixelBuffer::new(32, 32, PixelDescriptor::GRAY8);
let ctx = alloc::sync::Arc::new(crate::color::ColorContext::from_cicp(
crate::cicp::Cicp::new(1, 13, 0, false),
));
let img = MultiPlaneImage::new(layout, alloc::vec![buf]).with_origin(ctx.clone());
assert!(img.origin().is_some());
}
#[test]
fn plane_relationship_variants() {
let r = PlaneRelationship::Oklab;
assert_eq!(r, PlaneRelationship::Oklab);
let r = PlaneRelationship::YCbCr {
matrix: YuvMatrix::Bt709,
};
assert_ne!(r, PlaneRelationship::Independent);
let r2 = r;
assert_eq!(r, r2);
}
#[test]
fn mask_where_oklab() {
let layout = PlaneLayout::oklab_alpha(ChannelType::F32);
let luma = layout.luma_mask();
assert!(luma.includes(0));
assert!(!luma.includes(1));
assert_eq!(luma.count(), 1);
let chroma = layout.chroma_mask();
assert!(chroma.includes(1));
assert!(chroma.includes(2));
assert!(!chroma.includes(0));
assert_eq!(chroma.count(), 2);
let alpha = layout.alpha_mask();
assert!(alpha.includes(3));
assert_eq!(alpha.count(), 1);
}
#[test]
fn mask_where_ycbcr_420() {
let layout = PlaneLayout::ycbcr_420(ChannelType::U8);
let luma = layout.luma_mask();
assert!(luma.includes(0));
assert_eq!(luma.count(), 1);
let chroma = layout.chroma_mask();
assert!(chroma.includes(1));
assert!(chroma.includes(2));
assert_eq!(chroma.count(), 2);
let alpha = layout.alpha_mask();
assert!(alpha.is_empty());
}
#[test]
fn mask_where_gray() {
let layout = PlaneLayout::gray(ChannelType::U8);
let luma = layout.luma_mask();
assert!(luma.includes(0));
assert_eq!(luma.count(), 1);
assert!(layout.chroma_mask().is_empty());
assert!(layout.alpha_mask().is_empty());
}
#[test]
fn mask_where_interleaved_returns_none() {
let layout = PlaneLayout::Interleaved { channels: 4 };
assert!(layout.luma_mask().is_empty());
assert!(layout.chroma_mask().is_empty());
assert!(layout.alpha_mask().is_empty());
}
}