use crate::{PixelFormat, PixelSlice, TransferFunction};
#[derive(Clone, Copy, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DiffuseWhite(f32);
impl PartialEq for DiffuseWhite {
fn eq(&self, other: &Self) -> bool {
self.0.to_bits() == other.0.to_bits()
}
}
impl Eq for DiffuseWhite {}
impl DiffuseWhite {
pub const BT2408: Self = Self(203.0);
#[must_use]
pub const fn new(nits: f32) -> Self {
Self(nits)
}
#[must_use]
pub const fn nits(self) -> f32 {
self.0
}
}
impl Default for DiffuseWhite {
fn default() -> Self {
Self::BT2408
}
}
#[inline]
fn nits_to_u16(nits: f64) -> u16 {
(nits + 0.5) as u16
}
#[inline]
fn row_max_sum<const N: usize>(row: &[f32]) -> (f32, f64) {
let mut row_max = 0.0f32;
let mut row_sum = 0.0f64;
for chunk in row.chunks_exact(N) {
let px: &[f32; N] = chunk.try_into().unwrap();
let m = 0.0f32.max(px[0]).max(px[1]).max(px[2]);
row_max = row_max.max(m);
row_sum += f64::from(m);
}
(row_max, row_sum)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContentLightLevel {
pub max_content_light_level: u16,
pub max_frame_average_light_level: u16,
}
impl ContentLightLevel {
pub const fn new(max_content_light_level: u16, max_frame_average_light_level: u16) -> Self {
Self {
max_content_light_level,
max_frame_average_light_level,
}
}
#[must_use]
pub fn measure(px: PixelSlice<'_>, white: DiffuseWhite) -> Option<Self> {
let desc = px.descriptor();
let channels = match desc.pixel_format() {
PixelFormat::RgbF32 => 3,
PixelFormat::RgbaF32 => 4,
_ => return None,
};
if desc.transfer != TransferFunction::Linear {
return None;
}
let w = px.width() as usize;
let h = px.rows() as usize;
if w == 0 || h == 0 {
return Some(Self::new(0, 0));
}
let stride = px.stride();
let bytes = px.as_strided_bytes();
let row_len = w * channels * 4;
let mut max_lin = 0.0f32;
let mut sum_lin = 0.0f64;
for row in 0..h {
let row_bytes = &bytes[row * stride..row * stride + row_len];
let floats: &[f32] = bytemuck::cast_slice(row_bytes);
let (row_max, row_sum) = if channels == 3 {
row_max_sum::<3>(floats)
} else {
row_max_sum::<4>(floats)
};
max_lin = max_lin.max(row_max);
sum_lin += row_sum;
}
let wn = f64::from(white.nits());
let max_nits = f64::from(max_lin) * wn;
let fall = sum_lin / (w as f64 * h as f64) * wn;
Some(Self::new(nits_to_u16(max_nits), nits_to_u16(fall)))
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MasteringDisplay {
pub primaries_xy: [[f32; 2]; 3],
pub white_point_xy: [f32; 2],
pub max_luminance: f32,
pub min_luminance: f32,
}
impl MasteringDisplay {
pub const fn new(
primaries_xy: [[f32; 2]; 3],
white_point_xy: [f32; 2],
max_luminance: f32,
min_luminance: f32,
) -> Self {
Self {
primaries_xy,
white_point_xy,
max_luminance,
min_luminance,
}
}
pub const HDR10_REFERENCE: Self = Self {
primaries_xy: [[0.708, 0.292], [0.170, 0.797], [0.131, 0.046]],
white_point_xy: [0.3127, 0.3290],
max_luminance: 10000.0,
min_luminance: 0.0001,
};
pub const DISPLAY_P3_1000: Self = Self {
primaries_xy: [[0.680, 0.320], [0.265, 0.690], [0.150, 0.060]],
white_point_xy: [0.3127, 0.3290],
max_luminance: 1000.0,
min_luminance: 0.0001,
};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PixelBuffer, PixelDescriptor};
use alloc::vec::Vec;
fn rgbf32(pixels: &[[f32; 3]], w: u32, h: u32) -> PixelBuffer {
let mut data = Vec::with_capacity(pixels.len() * 12);
for p in pixels {
for c in p {
data.extend_from_slice(&c.to_ne_bytes());
}
}
PixelBuffer::from_vec(data, w, h, PixelDescriptor::RGBF32_LINEAR).unwrap()
}
#[test]
fn diffuse_white_defaults_to_bt2408() {
assert_eq!(DiffuseWhite::default(), DiffuseWhite::BT2408);
assert_eq!(DiffuseWhite::BT2408.nits(), 203.0);
assert_eq!(DiffuseWhite::new(100.0).nits(), 100.0);
}
#[test]
fn measure_two_grays_cta_stills_semantics() {
let buf = rgbf32(&[[1.0; 3], [2.0; 3]], 2, 1);
let cll = ContentLightLevel::measure(buf.as_slice(), DiffuseWhite::BT2408).unwrap();
assert_eq!(cll.max_content_light_level, 406);
assert_eq!(cll.max_frame_average_light_level, 305);
}
#[test]
fn measure_handles_stride_and_ignores_padding() {
use crate::PixelSlice;
let (w, h, row_floats) = (2u32, 2u32, 9usize);
let mut data = alloc::vec![1.0e9f32; row_floats * h as usize];
let pixels = [[0.5f32; 3], [1.0; 3], [2.0; 3], [0.25; 3]];
for (i, p) in pixels.iter().enumerate() {
let base = (i / w as usize) * row_floats + (i % w as usize) * 3;
data[base..base + 3].copy_from_slice(p);
}
let bytes: &[u8] = bytemuck::cast_slice(&data);
let px =
PixelSlice::new(bytes, w, h, row_floats * 4, PixelDescriptor::RGBF32_LINEAR).unwrap();
let cll = ContentLightLevel::measure(px, DiffuseWhite::BT2408).unwrap();
assert_eq!(cll.max_content_light_level, 406);
assert_eq!(cll.max_frame_average_light_level, 190);
}
#[test]
fn measure_clamps_nan_and_negative() {
let buf = rgbf32(&[[-1.0, f32::NAN, 0.5]], 1, 1);
let cll = ContentLightLevel::measure(buf.as_slice(), DiffuseWhite::BT2408).unwrap();
assert_eq!(cll.max_content_light_level, 102);
assert_eq!(cll.max_frame_average_light_level, 102);
}
#[test]
fn measure_ignores_alpha_and_custom_white() {
let mut data = Vec::new();
for c in [0.5f32, 0.5, 0.5, 7.0] {
data.extend_from_slice(&c.to_ne_bytes());
}
let buf = PixelBuffer::from_vec(data, 1, 1, PixelDescriptor::RGBAF32_LINEAR).unwrap();
let cll = ContentLightLevel::measure(buf.as_slice(), DiffuseWhite::new(100.0)).unwrap();
assert_eq!(cll.max_content_light_level, 50);
}
#[test]
fn measure_rejects_non_linear_and_non_f32() {
let u8buf =
PixelBuffer::from_vec(alloc::vec![0u8; 3], 1, 1, PixelDescriptor::RGB8_SRGB).unwrap();
assert!(ContentLightLevel::measure(u8buf.as_slice(), DiffuseWhite::BT2408).is_none());
let nonlinear = PixelDescriptor::RGBF32_LINEAR.with_transfer(TransferFunction::Srgb);
let mut data = Vec::new();
for c in [0.5f32; 3] {
data.extend_from_slice(&c.to_ne_bytes());
}
let buf = PixelBuffer::from_vec(data, 1, 1, nonlinear).unwrap();
assert!(ContentLightLevel::measure(buf.as_slice(), DiffuseWhite::BT2408).is_none());
}
#[test]
fn content_light_level_new() {
let cll = ContentLightLevel::new(1000, 500);
assert_eq!(cll.max_content_light_level, 1000);
assert_eq!(cll.max_frame_average_light_level, 500);
}
#[test]
fn content_light_level_default() {
let cll = ContentLightLevel::default();
assert_eq!(cll.max_content_light_level, 0);
assert_eq!(cll.max_frame_average_light_level, 0);
}
#[test]
fn mastering_display_new() {
let md = MasteringDisplay::new(
[[0.68, 0.32], [0.265, 0.69], [0.15, 0.06]],
[0.3127, 0.329],
1000.0,
0.001,
);
assert_eq!(md.max_luminance, 1000.0);
assert_eq!(md.min_luminance, 0.001);
}
#[test]
fn mastering_display_constants() {
assert_eq!(MasteringDisplay::HDR10_REFERENCE.max_luminance, 10000.0);
assert_eq!(MasteringDisplay::DISPLAY_P3_1000.max_luminance, 1000.0);
}
#[test]
fn content_light_level_clone_eq() {
let a = ContentLightLevel::new(100, 50);
let b = a;
assert_eq!(a, b);
}
#[test]
#[cfg(feature = "std")]
fn content_light_level_hash() {
use core::hash::{Hash, Hasher};
let a = ContentLightLevel::new(100, 50);
let b = a;
let mut h1 = std::hash::DefaultHasher::new();
a.hash(&mut h1);
let mut h2 = std::hash::DefaultHasher::new();
b.hash(&mut h2);
assert_eq!(h1.finish(), h2.finish());
}
}