use zenpixels::{ColorPrimaries, TransferFunction};
use crate::convert::{hlg_eotf, hlg_oetf, pq_eotf, pq_oetf};
use crate::gamut::GamutMatrix;
pub trait TransferFunctionExt {
#[must_use]
fn linearize(&self, v: f32) -> f32;
#[must_use]
fn delinearize(&self, v: f32) -> f32;
}
impl TransferFunctionExt for TransferFunction {
#[allow(unreachable_patterns)]
fn linearize(&self, v: f32) -> f32 {
match self {
Self::Linear | Self::Unknown => v,
Self::Srgb => linear_srgb::precise::srgb_to_linear(v),
Self::Bt709 => linear_srgb::tf::bt709_to_linear(v),
Self::Pq => pq_eotf(v),
Self::Hlg => hlg_eotf(v),
_ => v,
}
}
#[allow(unreachable_patterns)]
fn delinearize(&self, v: f32) -> f32 {
match self {
Self::Linear | Self::Unknown => v,
Self::Srgb => linear_srgb::precise::linear_to_srgb(v),
Self::Bt709 => linear_srgb::tf::linear_to_bt709(v),
Self::Pq => pq_oetf(v),
Self::Hlg => hlg_oetf(v),
_ => v,
}
}
}
#[allow(clippy::wrong_self_convention)]
pub trait ColorPrimariesExt {
fn to_xyz_matrix(&self) -> Option<&'static GamutMatrix>;
fn from_xyz_matrix(&self) -> Option<&'static GamutMatrix>;
}
impl ColorPrimariesExt for ColorPrimaries {
#[allow(unreachable_patterns)]
fn to_xyz_matrix(&self) -> Option<&'static GamutMatrix> {
match self {
Self::Bt709 => Some(&crate::gamut::BT709_TO_XYZ),
Self::DisplayP3 => Some(&crate::gamut::DISPLAY_P3_TO_XYZ),
Self::Bt2020 => Some(&crate::gamut::BT2020_TO_XYZ),
_ => None,
}
}
#[allow(unreachable_patterns)]
fn from_xyz_matrix(&self) -> Option<&'static GamutMatrix> {
match self {
Self::Bt709 => Some(&crate::gamut::XYZ_TO_BT709),
Self::DisplayP3 => Some(&crate::gamut::XYZ_TO_DISPLAY_P3),
Self::Bt2020 => Some(&crate::gamut::XYZ_TO_BT2020),
_ => None,
}
}
}
use alloc::sync::Arc;
use whereat::{At, ResultAtExt};
use zenpixels::PixelDescriptor;
use zenpixels::buffer::PixelBuffer;
use zenpixels::descriptor::{AlphaMode, ChannelLayout, ChannelType};
pub trait PixelBufferConvertExt {
fn convert_to(&self, target: PixelDescriptor) -> Result<PixelBuffer, At<crate::ConvertError>>;
fn try_add_alpha(&self) -> Result<PixelBuffer, At<crate::ConvertError>>;
fn try_widen_to_u16(&self) -> Result<PixelBuffer, At<crate::ConvertError>>;
fn try_narrow_to_u8(&self) -> Result<PixelBuffer, At<crate::ConvertError>>;
fn linearize(&self) -> Result<PixelBuffer, At<crate::ConvertError>>;
fn delinearize(
&self,
transfer: TransferFunction,
) -> Result<PixelBuffer, At<crate::ConvertError>>;
}
#[cfg(feature = "rgb")]
pub trait PixelBufferConvertTypedExt: PixelBufferConvertExt {
fn to_rgb8(&self) -> PixelBuffer<rgb::Rgb<u8>>;
fn to_rgba8(&self) -> PixelBuffer<rgb::Rgba<u8>>;
fn to_gray8(&self) -> PixelBuffer<rgb::Gray<u8>>;
fn to_bgra8(&self) -> PixelBuffer<rgb::alt::BGRA<u8>>;
}
fn assert_not_cmyk(desc: &PixelDescriptor) {
assert!(
desc.color_model() != crate::ColorModel::Cmyk,
"CMYK pixel data cannot be processed by zenpixels-convert. \
Use a CMS (e.g., moxcms) with an ICC profile for CMYK↔RGB conversion."
);
}
impl PixelBufferConvertExt for PixelBuffer {
#[track_caller]
fn convert_to(&self, target: PixelDescriptor) -> Result<PixelBuffer, At<crate::ConvertError>> {
let src_desc = self.descriptor();
assert_not_cmyk(&src_desc);
assert_not_cmyk(&target);
if src_desc == target {
let dst_stride = target.aligned_stride(self.width());
let total = dst_stride
.checked_mul(self.height() as usize)
.ok_or_else(|| whereat::at!(crate::ConvertError::AllocationFailed))?;
let mut out = alloc::vec![0u8; total];
let src_slice = self.as_slice();
for y in 0..self.height() {
let src_row = src_slice.row(y);
let dst_start = y as usize * dst_stride;
out[dst_start..dst_start + src_row.len()].copy_from_slice(src_row);
}
let mut buf = PixelBuffer::from_vec(out, self.width(), self.height(), target)
.map_err(|_| whereat::at!(crate::ConvertError::AllocationFailed))?;
if let Some(ctx) = self.color_context() {
buf = buf.with_color_context(Arc::clone(ctx));
}
return Ok(buf);
}
let mut converter = crate::RowConverter::new(src_desc, target).at()?;
let dst_stride = target.aligned_stride(self.width());
let total = dst_stride
.checked_mul(self.height() as usize)
.ok_or_else(|| whereat::at!(crate::ConvertError::AllocationFailed))?;
let mut out = alloc::vec![0u8; total];
let src_slice = self.as_slice();
for y in 0..self.height() {
let src_row = src_slice.row(y);
let dst_start = y as usize * dst_stride;
let dst_end = dst_start + dst_stride;
converter.convert_row(src_row, &mut out[dst_start..dst_end], self.width());
}
let mut buf = PixelBuffer::from_vec(out, self.width(), self.height(), target)
.map_err(|_| whereat::at!(crate::ConvertError::AllocationFailed))?;
if let Some(ctx) = self.color_context() {
buf = buf.with_color_context(Arc::clone(ctx));
}
Ok(buf)
}
#[track_caller]
fn try_add_alpha(&self) -> Result<PixelBuffer, At<crate::ConvertError>> {
let desc = self.descriptor();
let target_layout = match desc.layout() {
ChannelLayout::Gray => ChannelLayout::GrayAlpha,
ChannelLayout::Rgb => ChannelLayout::Rgba,
other => other,
};
let alpha = if target_layout.has_alpha() && desc.alpha().is_none() {
Some(AlphaMode::Straight)
} else {
desc.alpha()
};
let target =
PixelDescriptor::new(desc.channel_type(), target_layout, alpha, desc.transfer());
self.convert_to(target)
}
#[track_caller]
fn try_widen_to_u16(&self) -> Result<PixelBuffer, At<crate::ConvertError>> {
let desc = self.descriptor();
let target = PixelDescriptor::new(
ChannelType::U16,
desc.layout(),
desc.alpha(),
desc.transfer(),
);
self.convert_to(target)
}
#[track_caller]
fn try_narrow_to_u8(&self) -> Result<PixelBuffer, At<crate::ConvertError>> {
let desc = self.descriptor();
let target = PixelDescriptor::new(
ChannelType::U8,
desc.layout(),
desc.alpha(),
desc.transfer(),
);
self.convert_to(target)
}
#[track_caller]
fn linearize(&self) -> Result<PixelBuffer, At<crate::ConvertError>> {
let desc = self.descriptor();
let target = PixelDescriptor::new_full(
ChannelType::F32,
desc.layout(),
desc.alpha(),
TransferFunction::Linear,
desc.primaries,
);
self.convert_to(target)
}
#[track_caller]
fn delinearize(
&self,
transfer: TransferFunction,
) -> Result<PixelBuffer, At<crate::ConvertError>> {
let target = self.descriptor().with_transfer(transfer);
self.convert_to(target)
}
}
#[cfg(feature = "rgb")]
use zenpixels::buffer::Pixel;
#[cfg(feature = "rgb")]
impl PixelBufferConvertTypedExt for PixelBuffer {
fn to_rgb8(&self) -> PixelBuffer<rgb::Rgb<u8>> {
convert_to_typed(self, PixelDescriptor::RGB8_SRGB)
}
fn to_rgba8(&self) -> PixelBuffer<rgb::Rgba<u8>> {
convert_to_typed(self, PixelDescriptor::RGBA8_SRGB)
}
fn to_gray8(&self) -> PixelBuffer<rgb::Gray<u8>> {
convert_to_typed(self, PixelDescriptor::GRAY8_SRGB)
}
fn to_bgra8(&self) -> PixelBuffer<rgb::alt::BGRA<u8>> {
convert_to_typed(self, PixelDescriptor::BGRA8_SRGB)
}
}
#[cfg(feature = "rgb")]
fn convert_to_typed<Q: Pixel>(buf: &PixelBuffer, target: PixelDescriptor) -> PixelBuffer<Q> {
use alloc::vec;
let mut conv = crate::RowConverter::new(buf.descriptor(), target)
.expect("RowConverter: no conversion path");
let dst_bpp = target.bytes_per_pixel();
let dst_stride = target.aligned_stride(buf.width());
let total = dst_stride * buf.height() as usize;
let mut out = vec![0u8; total];
let src_slice = buf.as_slice();
for y in 0..buf.height() {
let src_row = src_slice.row(y);
let dst_start = y as usize * dst_stride;
let dst_end = dst_start + buf.width() as usize * dst_bpp;
conv.convert_row(src_row, &mut out[dst_start..dst_end], buf.width());
}
let erased = PixelBuffer::from_vec(out, buf.width(), buf.height(), target)
.expect("convert_to_typed: buffer construction failed");
let erased = if let Some(ctx) = buf.color_context() {
erased.with_color_context(Arc::clone(ctx))
} else {
erased
};
erased
.try_typed::<Q>()
.expect("convert_to_typed: type mismatch after conversion")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "CMYK pixel data cannot be processed")]
fn cmyk_rejected_by_convert_to() {
let cmyk_data = vec![0u8; 4 * 4]; let buf = PixelBuffer::from_vec(cmyk_data, 2, 2, PixelDescriptor::CMYK8).unwrap();
let _ = buf.convert_to(PixelDescriptor::RGB8_SRGB);
}
#[test]
#[should_panic(expected = "CMYK pixel data cannot be processed")]
fn cmyk_rejected_as_convert_target() {
let rgb_data = vec![0u8; 3 * 4]; let buf = PixelBuffer::from_vec(rgb_data, 2, 2, PixelDescriptor::RGB8_SRGB).unwrap();
let _ = buf.convert_to(PixelDescriptor::CMYK8);
}
#[test]
fn srgb_linearize_roundtrip() {
let tf = TransferFunction::Srgb;
for &v in &[0.0, 0.04045, 0.1, 0.5, 0.73, 1.0] {
let lin = tf.linearize(v);
let back = tf.delinearize(lin);
assert!(
(v - back).abs() < 1e-5,
"sRGB roundtrip failed for {v}: linearize={lin}, delinearize={back}"
);
}
}
#[test]
fn pq_linearize_roundtrip() {
let tf = TransferFunction::Pq;
for &v in &[0.0, 0.1, 0.5, 0.75, 1.0] {
let lin = tf.linearize(v);
let back = tf.delinearize(lin);
assert!(
(v - back).abs() < 5e-4,
"PQ roundtrip failed for {v}: linearize={lin}, delinearize={back}"
);
}
}
#[test]
fn hlg_linearize_roundtrip() {
let tf = TransferFunction::Hlg;
for &v in &[0.0, 0.1, 0.3, 0.5, 0.8, 1.0] {
let lin = tf.linearize(v);
let back = tf.delinearize(lin);
assert!(
(v - back).abs() < 1e-4,
"HLG roundtrip failed for {v}: linearize={lin}, delinearize={back}"
);
}
}
#[test]
fn linear_identity() {
let tf = TransferFunction::Linear;
for &v in &[0.0, 0.5, 1.0] {
assert_eq!(tf.linearize(v), v);
assert_eq!(tf.delinearize(v), v);
}
}
#[test]
fn xyz_matrix_availability() {
assert!(ColorPrimaries::Bt709.to_xyz_matrix().is_some());
assert!(ColorPrimaries::Bt709.from_xyz_matrix().is_some());
assert!(ColorPrimaries::DisplayP3.to_xyz_matrix().is_some());
assert!(ColorPrimaries::Bt2020.to_xyz_matrix().is_some());
assert!(ColorPrimaries::Unknown.to_xyz_matrix().is_none());
assert!(ColorPrimaries::Unknown.from_xyz_matrix().is_none());
}
#[test]
fn xyz_roundtrip_bt709() {
let to = ColorPrimaries::Bt709.to_xyz_matrix().unwrap();
let from = ColorPrimaries::Bt709.from_xyz_matrix().unwrap();
let rgb = [0.5f32, 0.3, 0.8];
let mut v = rgb;
crate::gamut::apply_matrix_f32(&mut v, to);
crate::gamut::apply_matrix_f32(&mut v, from);
for c in 0..3 {
assert!(
(v[c] - rgb[c]).abs() < 1e-4,
"XYZ roundtrip BT.709 ch{c}: {:.6} vs {:.6}",
v[c],
rgb[c]
);
}
}
#[test]
fn bt709_linearize_roundtrip() {
let tf = TransferFunction::Bt709;
for &v in &[0.0, 0.04045, 0.1, 0.5, 0.73, 1.0] {
let lin = tf.linearize(v);
let back = tf.delinearize(lin);
assert!(
(v - back).abs() < 1e-5,
"BT.709 roundtrip failed for {v}: linearize={lin}, delinearize={back}"
);
}
}
#[test]
fn unknown_transfer_identity() {
let tf = TransferFunction::Unknown;
for &v in &[0.0, 0.25, 0.5, 0.75, 1.0] {
assert_eq!(
tf.linearize(v),
v,
"Unknown linearize should be identity for {v}"
);
assert_eq!(
tf.delinearize(v),
v,
"Unknown delinearize should be identity for {v}"
);
}
}
use super::PixelBufferConvertExt;
#[test]
fn convert_to_identity() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let buf = PixelBuffer::from_vec(data.clone(), 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let out = buf.convert_to(PixelDescriptor::RGB8_SRGB).unwrap();
assert_eq!(out.descriptor(), PixelDescriptor::RGB8_SRGB);
assert_eq!(out.width(), 2);
assert_eq!(out.height(), 1);
assert_eq!(&out.as_slice().row(0)[..6], &data[..]);
}
#[test]
fn convert_to_rgba8() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let out = buf.convert_to(PixelDescriptor::RGBA8_SRGB).unwrap();
assert_eq!(out.descriptor(), PixelDescriptor::RGBA8_SRGB);
let slice = out.as_slice();
let row = slice.row(0);
assert_eq!(row[0], 100);
assert_eq!(row[1], 150);
assert_eq!(row[2], 200);
assert_eq!(row[3], 255);
assert_eq!(row[4], 50);
assert_eq!(row[5], 100);
assert_eq!(row[6], 150);
assert_eq!(row[7], 255);
}
#[test]
fn try_add_alpha_rgb() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let out = buf.try_add_alpha().unwrap();
assert_eq!(
out.descriptor().layout(),
zenpixels::descriptor::ChannelLayout::Rgba
);
let slice = out.as_slice();
let row = slice.row(0);
assert_eq!(row[3], 255);
assert_eq!(row[7], 255);
}
#[test]
fn try_widen_to_u16() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let out = buf.try_widen_to_u16().unwrap();
assert_eq!(
out.descriptor().channel_type(),
zenpixels::descriptor::ChannelType::U16
);
let slice = out.as_slice();
let row = slice.row(0);
for (i, &expected_u8) in [100u8, 150, 200, 50, 100, 150].iter().enumerate() {
let lo = row[i * 2];
let hi = row[i * 2 + 1];
let val = u16::from_le_bytes([lo, hi]);
let expected = expected_u8 as u16 * 257;
assert_eq!(
val, expected,
"channel {i}: expected {expected} (u8={expected_u8}*257), got {val}"
);
}
}
#[test]
fn linearize_srgb_to_linear_f32() {
let data = vec![128u8, 128, 128, 64, 64, 64];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let lin = buf.linearize().unwrap();
assert_eq!(lin.descriptor().transfer(), TransferFunction::Linear);
assert_eq!(
lin.descriptor().channel_type(),
zenpixels::descriptor::ChannelType::F32
);
assert_eq!(lin.descriptor().primaries, ColorPrimaries::Bt709);
let slice = lin.as_slice();
let row = slice.row(0);
let r = f32::from_le_bytes([row[0], row[1], row[2], row[3]]);
assert!(
(r - 0.216).abs() < 0.01,
"sRGB 128 should linearize to ~0.216, got {r}"
);
}
#[test]
fn delinearize_linear_to_srgb() {
let linear_val: f32 = 0.216;
let mut data = vec![0u8; 24]; for i in 0..6 {
let bytes = linear_val.to_le_bytes();
data[i * 4..i * 4 + 4].copy_from_slice(&bytes);
}
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGBF32_LINEAR).unwrap();
let srgb = buf.delinearize(TransferFunction::Srgb).unwrap();
assert_eq!(srgb.descriptor().transfer(), TransferFunction::Srgb);
let slice = srgb.as_slice();
let row = slice.row(0);
let r = f32::from_le_bytes([row[0], row[1], row[2], row[3]]);
assert!(
(r - 0.502).abs() < 0.01,
"linear 0.216 should delinearize to ~0.502, got {r}"
);
}
#[test]
fn linearize_delinearize_roundtrip() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let buf = PixelBuffer::from_vec(data.clone(), 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let lin = buf.linearize().unwrap();
let back = lin.delinearize(TransferFunction::Srgb).unwrap();
let slice = back.as_slice();
let row = slice.row(0);
let r = f32::from_le_bytes([row[0], row[1], row[2], row[3]]);
let expected = 100.0 / 255.0;
assert!(
(r - expected).abs() < 0.005,
"roundtrip pixel 0 R: expected ~{expected}, got {r}"
);
}
#[test]
fn linearize_preserves_alpha() {
let data = vec![100u8, 150, 200, 128, 50, 100, 150, 64];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGBA8_SRGB).unwrap();
let lin = buf.linearize().unwrap();
assert_eq!(
lin.descriptor().layout(),
zenpixels::descriptor::ChannelLayout::Rgba
);
assert!(lin.descriptor().alpha().is_some());
}
#[test]
fn linearize_preserves_primaries() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let desc = PixelDescriptor::RGB8_SRGB.with_primaries(ColorPrimaries::DisplayP3);
let buf = PixelBuffer::from_vec(data, 2, 1, desc).unwrap();
let lin = buf.linearize().unwrap();
assert_eq!(lin.descriptor().primaries, ColorPrimaries::DisplayP3);
}
#[test]
fn linearize_already_linear_is_identity() {
let val: f32 = 0.5;
let mut data = vec![0u8; 12]; for i in 0..3 {
data[i * 4..i * 4 + 4].copy_from_slice(&val.to_le_bytes());
}
let buf = PixelBuffer::from_vec(data, 1, 1, PixelDescriptor::RGBF32_LINEAR).unwrap();
let lin = buf.linearize().unwrap();
let slice = lin.as_slice();
let row = slice.row(0);
let r = f32::from_le_bytes([row[0], row[1], row[2], row[3]]);
assert!(
(r - val).abs() < 1e-6,
"already-linear should be identity, got {r}"
);
}
#[test]
fn try_narrow_to_u8() {
let values: [u16; 6] = [
100 * 257,
150 * 257,
200 * 257,
50 * 257,
100 * 257,
150 * 257,
];
let mut data = vec![0u8; 12];
for (i, &v) in values.iter().enumerate() {
let bytes = v.to_le_bytes();
data[i * 2] = bytes[0];
data[i * 2 + 1] = bytes[1];
}
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB16_SRGB).unwrap();
let out = buf.try_narrow_to_u8().unwrap();
assert_eq!(
out.descriptor().channel_type(),
zenpixels::descriptor::ChannelType::U8
);
let slice = out.as_slice();
let row = slice.row(0);
assert_eq!(row[0], 100);
assert_eq!(row[1], 150);
assert_eq!(row[2], 200);
assert_eq!(row[3], 50);
assert_eq!(row[4], 100);
assert_eq!(row[5], 150);
}
#[test]
#[cfg(feature = "rgb")]
fn to_rgb8() {
let data = vec![100u8, 150, 200, 255, 50, 100, 150, 255];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGBA8_SRGB).unwrap();
let typed: PixelBuffer<rgb::Rgb<u8>> = buf.to_rgb8();
assert_eq!(typed.width(), 2);
assert_eq!(typed.height(), 1);
let slice = typed.as_slice();
let row = slice.row(0);
assert_eq!(row[0], 100);
assert_eq!(row[1], 150);
assert_eq!(row[2], 200);
assert_eq!(row[3], 50);
assert_eq!(row[4], 100);
assert_eq!(row[5], 150);
}
#[test]
#[cfg(feature = "rgb")]
fn to_rgba8() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let typed: PixelBuffer<rgb::Rgba<u8>> = buf.to_rgba8();
assert_eq!(typed.width(), 2);
assert_eq!(typed.height(), 1);
let slice = typed.as_slice();
let row = slice.row(0);
assert_eq!(row[0], 100);
assert_eq!(row[1], 150);
assert_eq!(row[2], 200);
assert_eq!(row[3], 255);
assert_eq!(row[4], 50);
assert_eq!(row[5], 100);
assert_eq!(row[6], 150);
assert_eq!(row[7], 255);
}
#[test]
#[cfg(feature = "rgb")]
fn to_gray8() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let typed: PixelBuffer<rgb::Gray<u8>> = buf.to_gray8();
assert_eq!(typed.width(), 2);
assert_eq!(typed.height(), 1);
let slice = typed.as_slice();
let row = slice.row(0);
assert!(row[0] > 0, "gray pixel 0 should be non-zero");
assert!(row[1] > 0, "gray pixel 1 should be non-zero");
}
#[test]
#[cfg(feature = "rgb")]
fn to_bgra8() {
let data = vec![100u8, 150, 200, 50, 100, 150];
let buf = PixelBuffer::from_vec(data, 2, 1, PixelDescriptor::RGB8_SRGB).unwrap();
let typed: PixelBuffer<rgb::alt::BGRA<u8>> = buf.to_bgra8();
assert_eq!(typed.width(), 2);
assert_eq!(typed.height(), 1);
let slice = typed.as_slice();
let row = slice.row(0);
assert_eq!(row[0], 200);
assert_eq!(row[1], 150);
assert_eq!(row[2], 100);
assert_eq!(row[3], 255);
assert_eq!(row[4], 150);
assert_eq!(row[5], 100);
assert_eq!(row[6], 50);
assert_eq!(row[7], 255);
}
}