use crate::{ErrorCategory, ErrorCode, PixelFlowError, Result};
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorRange {
Full,
Limited,
}
impl ColorRange {
pub fn parse(value: &str) -> Result<Self> {
match value {
"full" => Ok(Self::Full),
"limited" => Ok(Self::Limited),
_ => Err(PixelFlowError::new(
ErrorCategory::Format,
ErrorCode::new("format.unsupported_range"),
format!("unsupported color range '{value}'"),
)),
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Full => "full",
Self::Limited => "limited",
}
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorMatrix {
Identity,
Bt709,
Bt470M,
Bt470Bg,
Smpte170M,
Smpte240M,
Ycgco,
Bt2020Ncl,
Bt2020Cl,
Smpte2085,
ChromaticityDerivedNcl,
ChromaticityDerivedCl,
Ictcp,
}
impl ColorMatrix {
pub fn parse(value: &str) -> Result<Self> {
match value {
"identity" => Ok(Self::Identity),
"bt709" => Ok(Self::Bt709),
"bt470m" => Ok(Self::Bt470M),
"bt470bg" => Ok(Self::Bt470Bg),
"smpte170m" => Ok(Self::Smpte170M),
"smpte240m" => Ok(Self::Smpte240M),
"ycgco" => Ok(Self::Ycgco),
"bt2020_ncl" => Ok(Self::Bt2020Ncl),
"bt2020_cl" => Ok(Self::Bt2020Cl),
"smpte2085" => Ok(Self::Smpte2085),
"chromaticity_derived_ncl" => Ok(Self::ChromaticityDerivedNcl),
"chromaticity_derived_cl" => Ok(Self::ChromaticityDerivedCl),
"ictcp" => Ok(Self::Ictcp),
_ => Err(color_metadata_error(
"format.unsupported_matrix",
format!("unsupported color matrix '{value}'"),
)),
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Identity => "identity",
Self::Bt709 => "bt709",
Self::Bt470M => "bt470m",
Self::Bt470Bg => "bt470bg",
Self::Smpte170M => "smpte170m",
Self::Smpte240M => "smpte240m",
Self::Ycgco => "ycgco",
Self::Bt2020Ncl => "bt2020_ncl",
Self::Bt2020Cl => "bt2020_cl",
Self::Smpte2085 => "smpte2085",
Self::ChromaticityDerivedNcl => "chromaticity_derived_ncl",
Self::ChromaticityDerivedCl => "chromaticity_derived_cl",
Self::Ictcp => "ictcp",
}
}
#[must_use]
pub const fn luma_coefficients(self) -> (f32, f32, f32) {
match self {
Self::Identity | Self::Ycgco | Self::Ictcp => (1.0, 1.0, 1.0),
Self::Bt709 => (0.2126, 0.7152, 0.0722),
Self::Bt470M => (0.30, 0.59, 0.11),
Self::Bt470Bg | Self::Smpte170M => (0.299, 0.587, 0.114),
Self::Smpte240M => (0.212, 0.701, 0.087),
Self::Bt2020Ncl | Self::Bt2020Cl | Self::Smpte2085 => (0.2627, 0.6780, 0.0593),
Self::ChromaticityDerivedNcl | Self::ChromaticityDerivedCl => (0.0, 0.0, 0.0),
}
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorTransfer {
Bt1886,
Bt470M,
Bt470Bg,
Smpte170M,
Smpte240M,
Linear,
Log100,
Log316,
Xvycc,
Bt1361E,
Srgb,
Bt2020_10,
Bt2020_12,
Pq,
Smpte428,
Hlg,
}
impl ColorTransfer {
pub fn parse(value: &str) -> Result<Self> {
match value {
"bt1886" => Ok(Self::Bt1886),
"bt470m" => Ok(Self::Bt470M),
"bt470bg" => Ok(Self::Bt470Bg),
"smpte170m" => Ok(Self::Smpte170M),
"smpte240m" => Ok(Self::Smpte240M),
"linear" => Ok(Self::Linear),
"log100" => Ok(Self::Log100),
"log316" => Ok(Self::Log316),
"xvycc" => Ok(Self::Xvycc),
"bt1361e" => Ok(Self::Bt1361E),
"srgb" => Ok(Self::Srgb),
"bt2020_10" => Ok(Self::Bt2020_10),
"bt2020_12" => Ok(Self::Bt2020_12),
"pq" => Ok(Self::Pq),
"smpte428" => Ok(Self::Smpte428),
"hlg" => Ok(Self::Hlg),
_ => Err(color_metadata_error(
"format.unsupported_transfer",
format!("unsupported color transfer '{value}'"),
)),
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Bt1886 => "bt1886",
Self::Bt470M => "bt470m",
Self::Bt470Bg => "bt470bg",
Self::Smpte170M => "smpte170m",
Self::Smpte240M => "smpte240m",
Self::Linear => "linear",
Self::Log100 => "log100",
Self::Log316 => "log316",
Self::Xvycc => "xvycc",
Self::Bt1361E => "bt1361e",
Self::Srgb => "srgb",
Self::Bt2020_10 => "bt2020_10",
Self::Bt2020_12 => "bt2020_12",
Self::Pq => "pq",
Self::Smpte428 => "smpte428",
Self::Hlg => "hlg",
}
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ColorPrimaries {
Bt709,
Bt470M,
Bt470Bg,
Smpte170M,
Smpte240M,
Film,
Bt2020,
Smpte428,
DciP3,
DisplayP3,
}
impl ColorPrimaries {
pub fn parse(value: &str) -> Result<Self> {
match value {
"bt709" => Ok(Self::Bt709),
"bt470m" => Ok(Self::Bt470M),
"bt470bg" => Ok(Self::Bt470Bg),
"smpte170m" => Ok(Self::Smpte170M),
"smpte240m" => Ok(Self::Smpte240M),
"film" => Ok(Self::Film),
"bt2020" => Ok(Self::Bt2020),
"smpte428" => Ok(Self::Smpte428),
"dci_p3" => Ok(Self::DciP3),
"display_p3" => Ok(Self::DisplayP3),
_ => Err(color_metadata_error(
"format.unsupported_primaries",
format!("unsupported color primaries '{value}'"),
)),
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Bt709 => "bt709",
Self::Bt470M => "bt470m",
Self::Bt470Bg => "bt470bg",
Self::Smpte170M => "smpte170m",
Self::Smpte240M => "smpte240m",
Self::Film => "film",
Self::Bt2020 => "bt2020",
Self::Smpte428 => "smpte428",
Self::DciP3 => "dci_p3",
Self::DisplayP3 => "display_p3",
}
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChromaSiting {
Left,
Center,
TopLeft,
Top,
BottomLeft,
Bottom,
}
impl ChromaSiting {
pub fn parse(value: &str) -> Result<Self> {
match value {
"left" => Ok(Self::Left),
"center" => Ok(Self::Center),
"top_left" => Ok(Self::TopLeft),
"top" => Ok(Self::Top),
"bottom_left" => Ok(Self::BottomLeft),
"bottom" => Ok(Self::Bottom),
_ => Err(color_metadata_error(
"format.unsupported_chroma_siting",
format!("unsupported chroma siting '{value}'"),
)),
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Left => "left",
Self::Center => "center",
Self::TopLeft => "top_left",
Self::Top => "top",
Self::BottomLeft => "bottom_left",
Self::Bottom => "bottom",
}
}
#[must_use]
pub const fn offsets(self) -> (f32, f32) {
match self {
Self::Left => (0.0, 0.5),
Self::Center => (0.5, 0.5),
Self::TopLeft => (0.0, 0.0),
Self::Top => (0.5, 0.0),
Self::BottomLeft => (0.0, 1.0),
Self::Bottom => (0.5, 1.0),
}
}
}
fn color_metadata_error(code: &'static str, message: impl Into<String>) -> PixelFlowError {
PixelFlowError::new(ErrorCategory::Format, ErrorCode::new(code), message)
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SampleType {
U8,
U16,
F32,
}
impl SampleType {
#[must_use]
pub const fn bytes_per_sample(self) -> usize {
match self {
Self::U8 => 1,
Self::U16 => 2,
Self::F32 => 4,
}
}
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FormatFamily {
Gray,
Yuv,
PlanarRgb,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ChromaSubsampling {
Cs444,
Cs422,
Cs420,
}
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PlaneRole {
Gray,
Y,
U,
V,
R,
G,
B,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PlaneDescriptor {
pub role: PlaneRole,
pub width_divisor: usize,
pub height_divisor: usize,
pub sample_type: SampleType,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FormatDescriptor {
name: String,
family: FormatFamily,
subsampling: Option<ChromaSubsampling>,
bits_per_sample: u8,
sample_type: SampleType,
planes: Vec<PlaneDescriptor>,
}
impl FormatDescriptor {
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn family(&self) -> FormatFamily {
self.family
}
#[must_use]
pub const fn subsampling(&self) -> Option<ChromaSubsampling> {
self.subsampling
}
#[must_use]
pub const fn bits_per_sample(&self) -> u8 {
self.bits_per_sample
}
#[must_use]
pub const fn sample_type(&self) -> SampleType {
self.sample_type
}
#[must_use]
pub fn planes(&self) -> &[PlaneDescriptor] {
&self.planes
}
}
pub fn format_with_bit_depth(format: &FormatDescriptor, bits: u8) -> Result<FormatDescriptor> {
let suffix = match bits {
8 => "8",
10 => "10",
12 => "12",
16 => "16",
32 => "f32",
_ => {
return Err(PixelFlowError::new(
ErrorCategory::Format,
ErrorCode::new("format.unsupported_bit_depth"),
format!("unsupported bit depth '{bits}'"),
));
}
};
let alias = match (format.family(), format.subsampling()) {
(FormatFamily::Gray, None) => format!("gray{suffix}"),
(FormatFamily::Yuv, Some(ChromaSubsampling::Cs420)) => format!("yuv420p{suffix}"),
(FormatFamily::Yuv, Some(ChromaSubsampling::Cs422)) => format!("yuv422p{suffix}"),
(FormatFamily::Yuv, Some(ChromaSubsampling::Cs444)) => format!("yuv444p{suffix}"),
(FormatFamily::PlanarRgb, None) => format!("rgbp{suffix}"),
_ => {
return Err(PixelFlowError::new(
ErrorCategory::Format,
ErrorCode::new("format.unsupported_bit_depth"),
format!("cannot derive bit depth format from '{}'", format.name()),
));
}
};
resolve_format_alias(&alias)
}
pub fn resolve_format_alias(alias: &str) -> Result<FormatDescriptor> {
let normalized = alias.to_ascii_lowercase();
if let Some(descriptor) = resolve_gray(&normalized) {
return Ok(descriptor);
}
if let Some(descriptor) = resolve_yuv(&normalized) {
return Ok(descriptor);
}
if let Some(descriptor) = resolve_planar_rgb(&normalized) {
return Ok(descriptor);
}
Err(PixelFlowError::new(
ErrorCategory::Format,
ErrorCode::new("format.unsupported_alias"),
format!("unsupported format alias '{alias}'"),
))
}
fn resolve_gray(alias: &str) -> Option<FormatDescriptor> {
let suffix = alias.strip_prefix("gray")?;
let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;
Some(FormatDescriptor {
name: format!("gray{canonical_suffix}"),
family: FormatFamily::Gray,
subsampling: None,
bits_per_sample: bits,
sample_type: sample,
planes: vec![PlaneDescriptor {
role: PlaneRole::Gray,
width_divisor: 1,
height_divisor: 1,
sample_type: sample,
}],
})
}
fn resolve_yuv(alias: &str) -> Option<FormatDescriptor> {
let (prefix, subsampling) = [
("yuv420p", ChromaSubsampling::Cs420),
("yuv422p", ChromaSubsampling::Cs422),
("yuv444p", ChromaSubsampling::Cs444),
]
.into_iter()
.find(|(prefix, _)| alias.starts_with(prefix))?;
let suffix = alias.strip_prefix(prefix)?;
let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;
let (chroma_w_div, chroma_h_div) = match subsampling {
ChromaSubsampling::Cs444 => (1, 1),
ChromaSubsampling::Cs422 => (2, 1),
ChromaSubsampling::Cs420 => (2, 2),
};
Some(FormatDescriptor {
name: format!("{prefix}{canonical_suffix}"),
family: FormatFamily::Yuv,
subsampling: Some(subsampling),
bits_per_sample: bits,
sample_type: sample,
planes: vec![
PlaneDescriptor {
role: PlaneRole::Y,
width_divisor: 1,
height_divisor: 1,
sample_type: sample,
},
PlaneDescriptor {
role: PlaneRole::U,
width_divisor: chroma_w_div,
height_divisor: chroma_h_div,
sample_type: sample,
},
PlaneDescriptor {
role: PlaneRole::V,
width_divisor: chroma_w_div,
height_divisor: chroma_h_div,
sample_type: sample,
},
],
})
}
fn resolve_planar_rgb(alias: &str) -> Option<FormatDescriptor> {
let suffix = alias
.strip_prefix("rgbp")
.or_else(|| alias.strip_prefix("gbrp"))?;
let (bits, sample, canonical_suffix) = parse_sample_suffix(suffix)?;
Some(FormatDescriptor {
name: format!("rgbp{canonical_suffix}"),
family: FormatFamily::PlanarRgb,
subsampling: None,
bits_per_sample: bits,
sample_type: sample,
planes: vec![
PlaneDescriptor {
role: PlaneRole::R,
width_divisor: 1,
height_divisor: 1,
sample_type: sample,
},
PlaneDescriptor {
role: PlaneRole::G,
width_divisor: 1,
height_divisor: 1,
sample_type: sample,
},
PlaneDescriptor {
role: PlaneRole::B,
width_divisor: 1,
height_divisor: 1,
sample_type: sample,
},
],
})
}
fn parse_sample_suffix(suffix: &str) -> Option<(u8, SampleType, &'static str)> {
match suffix {
"" | "8" => Some((8, SampleType::U8, "8")),
"10" => Some((10, SampleType::U16, "10")),
"12" => Some((12, SampleType::U16, "12")),
"16" => Some((16, SampleType::U16, "16")),
"f32" => Some((32, SampleType::F32, "f32")),
_ => None,
}
}
#[cfg(test)]
mod tests {
#![expect(clippy::indexing_slicing, reason = "allow in tests")]
use crate::{ErrorCategory, ErrorCode};
use super::{
ChromaSiting, ChromaSubsampling, ColorMatrix, ColorPrimaries, ColorRange, ColorTransfer,
FormatFamily, PlaneRole, SampleType, format_with_bit_depth, resolve_format_alias,
};
#[test]
fn color_metadata_parse_accepts_canonical_names() {
assert_eq!(
ColorRange::parse("full").expect("full range"),
ColorRange::Full
);
assert_eq!(
ColorMatrix::parse("identity").expect("identity matrix"),
ColorMatrix::Identity
);
assert_eq!(
ColorMatrix::parse("bt709").expect("bt709 matrix"),
ColorMatrix::Bt709
);
assert_eq!(
ColorMatrix::parse("bt470m").expect("bt470m matrix"),
ColorMatrix::Bt470M
);
assert_eq!(
ColorMatrix::parse("bt470bg").expect("bt470bg matrix"),
ColorMatrix::Bt470Bg
);
assert_eq!(
ColorMatrix::parse("smpte170m").expect("smpte170m matrix"),
ColorMatrix::Smpte170M
);
assert_eq!(
ColorMatrix::parse("smpte240m").expect("smpte240m matrix"),
ColorMatrix::Smpte240M
);
assert_eq!(
ColorMatrix::parse("ycgco").expect("ycgco matrix"),
ColorMatrix::Ycgco
);
assert_eq!(
ColorMatrix::parse("bt2020_ncl").expect("bt2020 matrix"),
ColorMatrix::Bt2020Ncl
);
assert_eq!(
ColorMatrix::parse("bt2020_cl").expect("bt2020 CL matrix"),
ColorMatrix::Bt2020Cl
);
assert_eq!(
ColorMatrix::parse("smpte2085").expect("smpte2085 matrix"),
ColorMatrix::Smpte2085
);
assert_eq!(
ColorMatrix::parse("chromaticity_derived_ncl").expect("chromaticity NCL matrix"),
ColorMatrix::ChromaticityDerivedNcl
);
assert_eq!(
ColorMatrix::parse("chromaticity_derived_cl").expect("chromaticity CL matrix"),
ColorMatrix::ChromaticityDerivedCl
);
assert_eq!(
ColorMatrix::parse("ictcp").expect("ICtCp matrix"),
ColorMatrix::Ictcp
);
assert_eq!(
ColorTransfer::parse("bt1886").expect("BT.1886 transfer"),
ColorTransfer::Bt1886
);
assert_eq!(
ColorTransfer::parse("bt470m").expect("BT.470M transfer"),
ColorTransfer::Bt470M
);
assert_eq!(
ColorTransfer::parse("bt470bg").expect("BT.470BG transfer"),
ColorTransfer::Bt470Bg
);
assert_eq!(
ColorTransfer::parse("smpte170m").expect("SMPTE 170M transfer"),
ColorTransfer::Smpte170M
);
assert_eq!(
ColorTransfer::parse("smpte240m").expect("SMPTE 240M transfer"),
ColorTransfer::Smpte240M
);
assert_eq!(
ColorTransfer::parse("linear").expect("linear transfer"),
ColorTransfer::Linear
);
assert_eq!(
ColorTransfer::parse("log100").expect("Log100 transfer"),
ColorTransfer::Log100
);
assert_eq!(
ColorTransfer::parse("log316").expect("Log316 transfer"),
ColorTransfer::Log316
);
assert_eq!(
ColorTransfer::parse("xvycc").expect("xvYCC transfer"),
ColorTransfer::Xvycc
);
assert_eq!(
ColorTransfer::parse("bt1361e").expect("BT.1361E transfer"),
ColorTransfer::Bt1361E
);
assert_eq!(
ColorTransfer::parse("srgb").expect("sRGB transfer"),
ColorTransfer::Srgb
);
assert_eq!(
ColorTransfer::parse("bt2020_10").expect("bt2020 transfer"),
ColorTransfer::Bt2020_10
);
assert_eq!(
ColorTransfer::parse("bt2020_12").expect("bt2020 transfer"),
ColorTransfer::Bt2020_12
);
assert_eq!(
ColorTransfer::parse("pq").expect("PQ transfer"),
ColorTransfer::Pq
);
assert_eq!(
ColorTransfer::parse("smpte428").expect("SMPTE 428 transfer"),
ColorTransfer::Smpte428
);
assert_eq!(
ColorTransfer::parse("hlg").expect("HLG transfer"),
ColorTransfer::Hlg
);
assert_eq!(
ColorPrimaries::parse("bt709").expect("BT.709 primaries"),
ColorPrimaries::Bt709
);
assert_eq!(
ColorPrimaries::parse("bt470m").expect("BT.470M primaries"),
ColorPrimaries::Bt470M
);
assert_eq!(
ColorPrimaries::parse("bt470bg").expect("BT.470BG primaries"),
ColorPrimaries::Bt470Bg
);
assert_eq!(
ColorPrimaries::parse("smpte170m").expect("SMPTE 170M primaries"),
ColorPrimaries::Smpte170M
);
assert_eq!(
ColorPrimaries::parse("smpte240m").expect("SMPTE 240M primaries"),
ColorPrimaries::Smpte240M
);
assert_eq!(
ColorPrimaries::parse("film").expect("film primaries"),
ColorPrimaries::Film
);
assert_eq!(
ColorPrimaries::parse("bt2020").expect("BT.2020 primaries"),
ColorPrimaries::Bt2020
);
assert_eq!(
ColorPrimaries::parse("smpte428").expect("SMPTE 428 primaries"),
ColorPrimaries::Smpte428
);
assert_eq!(
ColorPrimaries::parse("dci_p3").expect("DCI-P3 primaries"),
ColorPrimaries::DciP3
);
assert_eq!(
ColorPrimaries::parse("display_p3").expect("Display P3 primaries"),
ColorPrimaries::DisplayP3
);
assert_eq!(
ChromaSiting::parse("top_left").expect("top-left siting"),
ChromaSiting::TopLeft
);
assert_eq!(ColorMatrix::Ictcp.as_str(), "ictcp");
assert_eq!(ColorTransfer::Pq.as_str(), "pq");
assert_eq!(ColorPrimaries::DisplayP3.as_str(), "display_p3");
assert_eq!(ChromaSiting::Bottom.offsets(), (0.5, 1.0));
}
#[test]
fn color_metadata_parse_rejects_noncanonical_names() {
for (label, error) in [
(
"matrix",
ColorMatrix::parse("rec709").expect_err("alias should fail"),
),
(
"transfer",
ColorTransfer::parse("bt.1886").expect_err("punctuated name should fail"),
),
(
"primaries",
ColorPrimaries::parse("p3").expect_err("alias should fail"),
),
(
"siting",
ChromaSiting::parse("mpeg2").expect_err("alias should fail"),
),
] {
assert_eq!(error.category(), ErrorCategory::Format, "{label}");
}
}
#[test]
fn color_metadata_matrix_luma_coefficients_are_stable() {
assert_eq!(
ColorMatrix::Bt709.luma_coefficients(),
(0.2126, 0.7152, 0.0722)
);
assert_eq!(ColorMatrix::Bt470M.luma_coefficients(), (0.30, 0.59, 0.11));
assert_eq!(
ColorMatrix::Bt470Bg.luma_coefficients(),
(0.299, 0.587, 0.114)
);
assert_eq!(
ColorMatrix::Smpte170M.luma_coefficients(),
(0.299, 0.587, 0.114)
);
assert_eq!(
ColorMatrix::Smpte240M.luma_coefficients(),
(0.212, 0.701, 0.087)
);
assert_eq!(
ColorMatrix::Bt2020Ncl.luma_coefficients(),
(0.2627, 0.6780, 0.0593)
);
}
#[test]
fn color_range_parse_accepts_official_names() {
assert_eq!(
ColorRange::parse("full").expect("full should parse"),
ColorRange::Full
);
assert_eq!(
ColorRange::parse("limited").expect("limited should parse"),
ColorRange::Limited
);
assert_eq!(ColorRange::Full.as_str(), "full");
assert_eq!(ColorRange::Limited.as_str(), "limited");
}
#[test]
fn color_range_parse_rejects_unknown_names() {
let error = ColorRange::parse("tv").expect_err("unknown range should fail");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("format.unsupported_range"));
}
#[test]
fn format_with_bit_depth_preserves_family_and_subsampling() {
let yuv = resolve_format_alias("yuv420p8").expect("alias should resolve");
let rgb = resolve_format_alias("rgbp16").expect("alias should resolve");
let gray = resolve_format_alias("grayf32").expect("alias should resolve");
assert_eq!(
format_with_bit_depth(&yuv, 10)
.expect("10-bit YUV should derive")
.name(),
"yuv420p10"
);
assert_eq!(
format_with_bit_depth(&rgb, 32)
.expect("f32 RGB should derive")
.name(),
"rgbpf32"
);
assert_eq!(
format_with_bit_depth(&gray, 8)
.expect("8-bit gray should derive")
.name(),
"gray8"
);
}
#[test]
fn format_with_bit_depth_rejects_unsupported_depth() {
let format = resolve_format_alias("gray8").expect("alias should resolve");
let error = format_with_bit_depth(&format, 14).expect_err("14-bit output should fail");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("format.unsupported_bit_depth"));
}
#[test]
fn resolves_yuv420p10_to_stable_descriptor() {
let descriptor = resolve_format_alias("yuv420p10").expect("alias should resolve");
assert_eq!(descriptor.name(), "yuv420p10");
assert_eq!(descriptor.family(), FormatFamily::Yuv);
assert_eq!(descriptor.subsampling(), Some(ChromaSubsampling::Cs420));
assert_eq!(descriptor.bits_per_sample(), 10);
assert_eq!(descriptor.sample_type(), SampleType::U16);
assert_eq!(descriptor.planes().len(), 3);
assert_eq!(descriptor.planes()[0].role, PlaneRole::Y);
assert_eq!(descriptor.planes()[1].role, PlaneRole::U);
assert_eq!(descriptor.planes()[1].width_divisor, 2);
assert_eq!(descriptor.planes()[1].height_divisor, 2);
}
#[test]
fn resolves_planar_rgb_aliases_to_same_descriptor() {
let rgb = resolve_format_alias("rgbp10").expect("alias should resolve");
let gbr = resolve_format_alias("gbrp10").expect("alias should resolve");
assert_eq!(rgb, gbr);
assert_eq!(rgb.family(), FormatFamily::PlanarRgb);
assert_eq!(rgb.planes()[0].role, PlaneRole::R);
assert_eq!(rgb.planes()[1].role, PlaneRole::G);
assert_eq!(rgb.planes()[2].role, PlaneRole::B);
}
#[test]
fn resolves_float_gray_descriptor() {
let descriptor = resolve_format_alias("grayf32").expect("alias should resolve");
assert_eq!(descriptor.name(), "grayf32");
assert_eq!(descriptor.family(), FormatFamily::Gray);
assert_eq!(descriptor.bits_per_sample(), 32);
assert_eq!(descriptor.sample_type(), SampleType::F32);
assert_eq!(descriptor.planes()[0].role, PlaneRole::Gray);
}
#[test]
fn unsupported_alias_returns_structured_format_error() {
let error = resolve_format_alias("yuv420p14").expect_err("unsupported alias should fail");
assert_eq!(error.category(), ErrorCategory::Format);
assert_eq!(error.code(), ErrorCode::new("format.unsupported_alias"));
assert!(error.message().contains("yuv420p14"));
}
}