use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::color::{ColorSpace, Primaries, TransferFunction, decode_transfer, primaries_matrix};
use crate::tree::Rect;
fn mat3_mul_vec3(m: [[f32; 3]; 3], v: [f32; 3]) -> [f32; 3] {
[
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
]
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PixelFormat {
Rgba8,
Rgba16,
RgbaF16,
RgbaF32,
}
impl PixelFormat {
pub const fn bytes_per_pixel(self) -> usize {
match self {
PixelFormat::Rgba8 => 4,
PixelFormat::Rgba16 | PixelFormat::RgbaF16 => 8,
PixelFormat::RgbaF32 => 16,
}
}
}
#[derive(Clone)]
pub struct Image {
inner: Arc<ImageInner>,
}
struct ImageInner {
pixels: Vec<u8>,
width: u32,
height: u32,
format: PixelFormat,
color_space: ColorSpace,
content_hash: u64,
}
impl Image {
pub fn from_rgba8(width: u32, height: u32, pixels: Vec<u8>) -> Self {
Self::from_rgba8_in(ColorSpace::SRGB, width, height, pixels)
}
pub fn from_rgba8_in(space: ColorSpace, width: u32, height: u32, pixels: Vec<u8>) -> Self {
Self::new_raw(PixelFormat::Rgba8, space, width, height, pixels)
}
pub fn from_rgba16_in(space: ColorSpace, width: u32, height: u32, pixels: Vec<u16>) -> Self {
Self::check_channel_count("from_rgba16_in", "u16", width, height, pixels.len());
let bytes = pixels.iter().flat_map(|v| v.to_ne_bytes()).collect();
Self::new_raw(PixelFormat::Rgba16, space, width, height, bytes)
}
pub fn from_rgba_f16_bits_in(
space: ColorSpace,
width: u32,
height: u32,
bits: Vec<u16>,
) -> Self {
Self::check_channel_count(
"from_rgba_f16_bits_in",
"f16-bit",
width,
height,
bits.len(),
);
let bytes = bits.iter().flat_map(|v| v.to_ne_bytes()).collect();
Self::new_raw(PixelFormat::RgbaF16, space, width, height, bytes)
}
pub fn from_rgba_f32_in(space: ColorSpace, width: u32, height: u32, pixels: Vec<f32>) -> Self {
Self::check_channel_count("from_rgba_f32_in", "f32", width, height, pixels.len());
let bytes = pixels.iter().flat_map(|v| v.to_ne_bytes()).collect();
Self::new_raw(PixelFormat::RgbaF32, space, width, height, bytes)
}
fn check_channel_count(ctor: &str, unit: &str, width: u32, height: u32, got: usize) {
let expected = (width as usize) * (height as usize) * 4;
assert_eq!(
got, expected,
"Image::{ctor}: expected {expected} {unit} channel values ({width}x{height} RGBA), got {got}",
);
}
fn new_raw(
format: PixelFormat,
space: ColorSpace,
width: u32,
height: u32,
pixels: Vec<u8>,
) -> Self {
let expected = (width as usize) * (height as usize) * format.bytes_per_pixel();
assert_eq!(
pixels.len(),
expected,
"Image: expected {expected} bytes ({width}x{height} {format:?}), got {}",
pixels.len(),
);
let mut h = DefaultHasher::new();
width.hash(&mut h);
height.hash(&mut h);
format.hash(&mut h);
space.hash(&mut h);
pixels.hash(&mut h);
let content_hash = h.finish();
Self {
inner: Arc::new(ImageInner {
pixels,
width,
height,
format,
color_space: space,
content_hash,
}),
}
}
pub fn width(&self) -> u32 {
self.inner.width
}
pub fn height(&self) -> u32 {
self.inner.height
}
pub fn format(&self) -> PixelFormat {
self.inner.format
}
pub fn color_space(&self) -> ColorSpace {
self.inner.color_space
}
pub fn pixels(&self) -> &[u8] {
&self.inner.pixels
}
pub fn is_srgb8(&self) -> bool {
self.inner.format == PixelFormat::Rgba8 && self.inner.color_space == ColorSpace::SRGB
}
pub fn to_scrgb_f16(&self) -> Vec<u16> {
self.to_scrgb_f16_with_peak().0
}
pub fn to_scrgb_f16_with_peak(&self) -> (Vec<u16>, f32) {
let inner = &*self.inner;
let tf = inner.color_space.transfer;
let matrix = (inner.color_space.primaries != Primaries::Srgb)
.then(|| primaries_matrix(inner.color_space.primaries, Primaries::Srgb));
let lum_scale = match tf {
TransferFunction::Pq => {
let r = inner.color_space.reference_luminance_nits;
debug_assert!(
r > 0.0,
"Image::to_scrgb_f16: PQ source tagged with \
non-positive reference_luminance_nits ({r}); the \
reference white anchors absolute PQ luminance into \
the working space"
);
10_000.0 / r
}
_ => 1.0,
};
let px = (inner.width as usize) * (inner.height as usize);
let mut out = Vec::with_capacity(px * 4);
let mut peak = 0.0f32;
let mut push = |rgba: [f32; 4]| {
let lin = match matrix {
Some(m) => mat3_mul_vec3(m, [rgba[0], rgba[1], rgba[2]]),
None => [rgba[0], rgba[1], rgba[2]],
};
let lin = [lin[0] * lum_scale, lin[1] * lum_scale, lin[2] * lum_scale];
for c in lin {
if c.is_finite() {
peak = peak.max(c);
}
}
out.push(half::f16::from_f32(lin[0]).to_bits());
out.push(half::f16::from_f32(lin[1]).to_bits());
out.push(half::f16::from_f32(lin[2]).to_bits());
out.push(half::f16::from_f32(rgba[3]).to_bits());
};
match inner.format {
PixelFormat::Rgba8 => {
let lut: Vec<f32> = (0..=255u32)
.map(|v| decode_transfer(v as f32 / 255.0, tf))
.collect();
for p in inner.pixels.chunks_exact(4) {
push([
lut[p[0] as usize],
lut[p[1] as usize],
lut[p[2] as usize],
p[3] as f32 / 255.0,
]);
}
}
PixelFormat::Rgba16 => {
let lut: Vec<f32> = (0..=65535u32)
.map(|v| decode_transfer(v as f32 / 65535.0, tf))
.collect();
for p in inner.pixels.chunks_exact(8) {
let ch = |i: usize| u16::from_ne_bytes([p[i * 2], p[i * 2 + 1]]) as usize;
push([lut[ch(0)], lut[ch(1)], lut[ch(2)], ch(3) as f32 / 65535.0]);
}
}
PixelFormat::RgbaF16 => {
for p in inner.pixels.chunks_exact(8) {
let ch = |i: usize| {
half::f16::from_bits(u16::from_ne_bytes([p[i * 2], p[i * 2 + 1]])).to_f32()
};
push([
decode_transfer(ch(0), tf),
decode_transfer(ch(1), tf),
decode_transfer(ch(2), tf),
ch(3),
]);
}
}
PixelFormat::RgbaF32 => {
for p in inner.pixels.chunks_exact(16) {
let ch = |i: usize| {
f32::from_ne_bytes([p[i * 4], p[i * 4 + 1], p[i * 4 + 2], p[i * 4 + 3]])
};
push([
decode_transfer(ch(0), tf),
decode_transfer(ch(1), tf),
decode_transfer(ch(2), tf),
ch(3),
]);
}
}
}
(out, peak)
}
pub fn content_hash(&self) -> u64 {
self.inner.content_hash
}
pub fn label(&self) -> String {
format!("image:{:08x}", self.inner.content_hash as u32)
}
}
impl PartialEq for Image {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.inner, &other.inner)
|| self.inner.content_hash == other.inner.content_hash
}
}
impl Eq for Image {}
impl std::fmt::Debug for Image {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Image")
.field("width", &self.inner.width)
.field("height", &self.inner.height)
.field("format", &self.inner.format)
.field("color_space", &self.inner.color_space)
.field(
"content_hash",
&format_args!("{:016x}", self.inner.content_hash),
)
.finish()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ImageFit {
#[default]
Contain,
Cover,
Fill,
None,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum DynamicRangeLimit {
Standard,
ConstrainedHigh,
#[default]
NoLimit,
}
impl DynamicRangeLimit {
pub const CONSTRAINED_HIGH_HEADROOM: f32 = 2.0;
pub fn resolve(self, headroom: f32) -> f32 {
let headroom = headroom.max(1.0);
match self {
DynamicRangeLimit::Standard => 1.0,
DynamicRangeLimit::ConstrainedHigh => headroom.min(Self::CONSTRAINED_HIGH_HEADROOM),
DynamicRangeLimit::NoLimit => headroom,
}
}
}
impl ImageFit {
pub fn project(self, nw: u32, nh: u32, rect: Rect) -> Rect {
let nw = (nw as f32).max(1.0);
let nh = (nh as f32).max(1.0);
match self {
ImageFit::Fill => rect,
ImageFit::None => Rect::new(rect.x, rect.y, nw, nh),
ImageFit::Contain => {
let scale = (rect.w / nw).min(rect.h / nh).max(0.0);
let w = nw * scale;
let h = nh * scale;
Rect::new(
rect.x + (rect.w - w) * 0.5,
rect.y + (rect.h - h) * 0.5,
w,
h,
)
}
ImageFit::Cover => {
let scale = (rect.w / nw).max(rect.h / nh).max(0.0);
let w = nw * scale;
let h = nh * scale;
Rect::new(
rect.x + (rect.w - w) * 0.5,
rect.y + (rect.h - h) * 0.5,
w,
h,
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rgba(w: u32, h: u32, byte: u8) -> Vec<u8> {
vec![byte; (w as usize) * (h as usize) * 4]
}
#[test]
fn from_rgba8_validates_buffer_length() {
let _ = Image::from_rgba8(2, 2, rgba(2, 2, 0));
}
#[test]
#[should_panic(expected = "expected 16 bytes")]
fn from_rgba8_panics_on_size_mismatch() {
let _ = Image::from_rgba8(2, 2, vec![0; 12]);
}
#[test]
fn equal_pixels_share_content_hash() {
let a = Image::from_rgba8(4, 4, rgba(4, 4, 0xab));
let b = Image::from_rgba8(4, 4, rgba(4, 4, 0xab));
assert_eq!(a.content_hash(), b.content_hash());
assert_eq!(a, b);
}
#[test]
fn different_pixels_get_distinct_hash() {
let a = Image::from_rgba8(2, 2, rgba(2, 2, 0x00));
let b = Image::from_rgba8(2, 2, rgba(2, 2, 0xff));
assert_ne!(a.content_hash(), b.content_hash());
}
#[test]
fn same_pixels_different_space_get_distinct_hash() {
let a = Image::from_rgba8(2, 2, rgba(2, 2, 0xab));
let b = Image::from_rgba8_in(ColorSpace::DISPLAY_P3, 2, 2, rgba(2, 2, 0xab));
assert_ne!(a.content_hash(), b.content_hash());
assert_ne!(a, b);
}
#[test]
fn srgb8_fast_path_predicate() {
assert!(Image::from_rgba8(1, 1, rgba(1, 1, 0)).is_srgb8());
assert!(!Image::from_rgba8_in(ColorSpace::DISPLAY_P3, 1, 1, rgba(1, 1, 0)).is_srgb8());
assert!(!Image::from_rgba_f32_in(ColorSpace::SCRGB_LINEAR, 1, 1, vec![0.0; 4]).is_srgb8());
}
fn f16_val(bits: u16) -> f32 {
half::f16::from_bits(bits).to_f32()
}
#[test]
fn scrgb_conversion_decodes_srgb_tf() {
let img = Image::from_rgba8(1, 1, vec![188, 188, 188, 255]);
let out = img.to_scrgb_f16();
assert_eq!(out.len(), 4);
for c in &out[..3] {
assert!((f16_val(*c) - 0.5).abs() < 0.01, "got {}", f16_val(*c));
}
assert!((f16_val(out[3]) - 1.0).abs() < 1e-3);
}
#[test]
fn scrgb_conversion_preserves_white_across_primaries() {
let img = Image::from_rgba8_in(ColorSpace::DISPLAY_P3, 1, 1, vec![255, 255, 255, 255]);
let out = img.to_scrgb_f16();
for c in &out[..3] {
assert!((f16_val(*c) - 1.0).abs() < 0.01, "got {}", f16_val(*c));
}
}
#[test]
fn scrgb_conversion_maps_p3_red_out_of_gamut() {
let img = Image::from_rgba8_in(ColorSpace::DISPLAY_P3, 1, 1, vec![255, 0, 0, 255]);
let out = img.to_scrgb_f16();
let (r, g) = (f16_val(out[0]), f16_val(out[1]));
assert!(r > 1.0, "P3 red r = {r}, expected > 1");
assert!(g < 0.0, "P3 red g = {g}, expected < 0");
}
#[test]
fn scrgb_conversion_passes_linear_floats_through() {
let img =
Image::from_rgba_f32_in(ColorSpace::SCRGB_LINEAR, 1, 1, vec![4.0, 0.25, 1.0, 0.5]);
let out = img.to_scrgb_f16();
assert!((f16_val(out[0]) - 4.0).abs() < 0.01);
assert!((f16_val(out[1]) - 0.25).abs() < 0.001);
assert!((f16_val(out[2]) - 1.0).abs() < 0.001);
assert!((f16_val(out[3]) - 0.5).abs() < 0.001);
}
#[test]
fn scrgb_conversion_handles_rgba16_and_f16_bits() {
let half_u16 = (0.5f32 * 65535.0) as u16;
let img = Image::from_rgba16_in(
ColorSpace::SRGB_LINEAR,
1,
1,
vec![half_u16, half_u16, half_u16, 65535],
);
let out = img.to_scrgb_f16();
assert!(
(f16_val(out[0]) - 0.5).abs() < 0.001,
"got {}",
f16_val(out[0])
);
let bits = half::f16::from_f32(2.5).to_bits();
let img = Image::from_rgba_f16_bits_in(
ColorSpace::SCRGB_LINEAR,
1,
1,
vec![bits, bits, bits, half::f16::from_f32(1.0).to_bits()],
);
let out = img.to_scrgb_f16();
assert!((f16_val(out[0]) - 2.5).abs() < 0.01);
}
#[test]
fn pq_anchors_reference_white_to_working_one() {
let img = Image::from_rgba_f32_in(
ColorSpace::BT2020_PQ,
1,
1,
vec![0.5807, 0.5807, 0.5807, 1.0],
);
let (out, peak) = img.to_scrgb_f16_with_peak();
for c in &out[..3] {
assert!((f16_val(*c) - 1.0).abs() < 0.02, "got {}", f16_val(*c));
}
assert!((peak - 1.0).abs() < 0.02, "got {peak}");
}
#[test]
fn pq_peak_signal_lands_at_headroom_above_reference() {
let img = Image::from_rgba_f32_in(ColorSpace::BT2020_PQ, 1, 1, vec![1.0, 1.0, 1.0, 1.0]);
let (out, peak) = img.to_scrgb_f16_with_peak();
let expected = 10_000.0 / 203.0;
assert!(
(f16_val(out[0]) - expected).abs() / expected < 0.01,
"got {}",
f16_val(out[0])
);
assert!((peak - expected).abs() / expected < 0.01, "got {peak}");
}
#[test]
fn pq_anchor_honors_overridden_reference_white() {
let space = ColorSpace {
reference_luminance_nits: 100.0,
..ColorSpace::BT2020_PQ
};
let img = Image::from_rgba_f32_in(space, 1, 1, vec![0.5081, 0.5081, 0.5081, 1.0]);
let (out, _) = img.to_scrgb_f16_with_peak();
assert!(
(f16_val(out[0]) - 1.0).abs() < 0.02,
"got {}",
f16_val(out[0])
);
}
#[test]
fn measured_peak_is_max_linear_channel() {
let (_, peak) = Image::from_rgba8(1, 1, vec![255, 128, 0, 255]).to_scrgb_f16_with_peak();
assert!((peak - 1.0).abs() < 1e-3, "got {peak}");
let img = Image::from_rgba_f32_in(
ColorSpace::SCRGB_LINEAR,
2,
1,
vec![0.5, 0.5, 0.5, 1.0, 3.75, 0.25, 1.0, 0.5],
);
let (_, peak) = img.to_scrgb_f16_with_peak();
assert!((peak - 3.75).abs() < 0.01, "got {peak}");
}
#[test]
fn measured_peak_skips_non_finite() {
let img = Image::from_rgba_f32_in(
ColorSpace::SCRGB_LINEAR,
1,
1,
vec![f32::NAN, f32::INFINITY, 2.0, 1.0],
);
let (_, peak) = img.to_scrgb_f16_with_peak();
assert!((peak - 2.0).abs() < 0.01, "got {peak}");
}
#[test]
fn dynamic_range_limit_resolves_against_headroom() {
use DynamicRangeLimit::*;
let h = 1000.0 / 203.0;
assert_eq!(Standard.resolve(h), 1.0);
assert_eq!(ConstrainedHigh.resolve(h), 2.0);
assert_eq!(NoLimit.resolve(h), h);
assert_eq!(NoLimit.resolve(1.0), 1.0);
assert_eq!(ConstrainedHigh.resolve(1.0), 1.0);
assert_eq!(NoLimit.resolve(f32::INFINITY), f32::INFINITY);
assert_eq!(NoLimit.resolve(0.5), 1.0);
}
#[test]
fn fit_contain_letterboxes_horizontally() {
let r = ImageFit::Contain.project(200, 100, Rect::new(0.0, 0.0, 400.0, 400.0));
assert!((r.w - 400.0).abs() < 0.01);
assert!((r.h - 200.0).abs() < 0.01);
assert!((r.x - 0.0).abs() < 0.01);
assert!((r.y - 100.0).abs() < 0.01);
}
#[test]
fn fit_cover_overflows_horizontally() {
let r = ImageFit::Cover.project(100, 200, Rect::new(0.0, 0.0, 400.0, 400.0));
assert!((r.w - 400.0).abs() < 0.01);
assert!((r.h - 800.0).abs() < 0.01);
assert!((r.y + 200.0).abs() < 0.01);
}
#[test]
fn fit_fill_stretches() {
let r = ImageFit::Fill.project(100, 200, Rect::new(10.0, 20.0, 300.0, 50.0));
assert_eq!(r, Rect::new(10.0, 20.0, 300.0, 50.0));
}
#[test]
fn fit_none_uses_natural_size() {
let r = ImageFit::None.project(64, 32, Rect::new(10.0, 20.0, 400.0, 400.0));
assert_eq!(r, Rect::new(10.0, 20.0, 64.0, 32.0));
}
}