use alloc::format;
use alloc::vec;
use alloc::vec::Vec;
use crate::color::gamut::rgb_to_luminance;
use crate::types::{ColorGamut, Error, GainMap, GainMapMetadata, Result};
use super::compute::GainMapConfig;
#[derive(Debug)]
pub struct RowDecoder {
gainmap: GainMap,
metadata: GainMapMetadata,
width: u32,
height: u32,
weight: f32,
current_row: u32,
gamut: ColorGamut,
}
impl RowDecoder {
pub fn new(
gainmap: GainMap,
metadata: GainMapMetadata,
width: u32,
height: u32,
display_boost: f32,
gamut: ColorGamut,
) -> Result<Self> {
let weight = calculate_weight(display_boost, &metadata);
Ok(Self {
gainmap,
metadata,
width,
height,
weight,
current_row: 0,
gamut,
})
}
pub fn gamut(&self) -> ColorGamut {
self.gamut
}
pub fn process_rows(&mut self, sdr_linear: &[f32], num_rows: u32) -> Result<Vec<f32>> {
let remaining = self.height - self.current_row;
let actual_rows = num_rows.min(remaining);
if actual_rows == 0 {
return Err(Error::InvalidPixelData("all rows already processed".into()));
}
let input_stride = self.width as usize * 3; let expected_len = input_stride * actual_rows as usize;
if sdr_linear.len() < expected_len {
return Err(Error::InvalidPixelData(format!(
"input data too short: {} < {} floats",
sdr_linear.len(),
expected_len
)));
}
let output_stride = self.width as usize * 4; let mut output = vec![0.0f32; output_stride * actual_rows as usize];
for row_offset in 0..actual_rows {
let y = self.current_row + row_offset;
let input_start = row_offset as usize * input_stride;
let output_start = row_offset as usize * output_stride;
for x in 0..self.width {
let in_idx = input_start + x as usize * 3;
let out_idx = output_start + x as usize * 4;
let sdr = [
sdr_linear[in_idx],
sdr_linear[in_idx + 1],
sdr_linear[in_idx + 2],
];
let gain = self.sample_gainmap(x, y);
let hdr = apply_gain(sdr, gain, &self.metadata);
output[out_idx] = hdr[0];
output[out_idx + 1] = hdr[1];
output[out_idx + 2] = hdr[2];
output[out_idx + 3] = 1.0;
}
}
self.current_row += actual_rows;
Ok(output)
}
pub fn process_row(&mut self, sdr_linear: &[f32]) -> Result<Vec<f32>> {
self.process_rows(sdr_linear, 1)
}
pub fn is_complete(&self) -> bool {
self.current_row >= self.height
}
pub fn current_row(&self) -> u32 {
self.current_row
}
pub fn total_rows(&self) -> u32 {
self.height
}
pub fn rows_remaining(&self) -> u32 {
self.height - self.current_row
}
pub fn reset(&mut self) {
self.current_row = 0;
}
fn sample_gainmap(&self, x: u32, y: u32) -> [f32; 3] {
let gm_x = (x as f32 / self.width as f32) * self.gainmap.width as f32;
let gm_y = (y as f32 / self.height as f32) * self.gainmap.height as f32;
let x0 = (gm_x.floor() as u32).min(self.gainmap.width - 1);
let y0 = (gm_y.floor() as u32).min(self.gainmap.height - 1);
let x1 = (x0 + 1).min(self.gainmap.width - 1);
let y1 = (y0 + 1).min(self.gainmap.height - 1);
let fx = gm_x - gm_x.floor();
let fy = gm_y - gm_y.floor();
if self.gainmap.channels == 1 {
let v00 = self.gainmap.data[(y0 * self.gainmap.width + x0) as usize] as f32 / 255.0;
let v10 = self.gainmap.data[(y0 * self.gainmap.width + x1) as usize] as f32 / 255.0;
let v01 = self.gainmap.data[(y1 * self.gainmap.width + x0) as usize] as f32 / 255.0;
let v11 = self.gainmap.data[(y1 * self.gainmap.width + x1) as usize] as f32 / 255.0;
let v = bilinear(v00, v10, v01, v11, fx, fy);
let gain = decode_gain(v, &self.metadata, 0, self.weight);
[gain, gain, gain]
} else {
let mut gains = [0.0f32; 3];
#[allow(clippy::needless_range_loop)]
for c in 0..3 {
let v00 = self.gainmap.data[(y0 * self.gainmap.width + x0) as usize * 3 + c] as f32
/ 255.0;
let v10 = self.gainmap.data[(y0 * self.gainmap.width + x1) as usize * 3 + c] as f32
/ 255.0;
let v01 = self.gainmap.data[(y1 * self.gainmap.width + x0) as usize * 3 + c] as f32
/ 255.0;
let v11 = self.gainmap.data[(y1 * self.gainmap.width + x1) as usize * 3 + c] as f32
/ 255.0;
let v = bilinear(v00, v10, v01, v11, fx, fy);
gains[c] = decode_gain(v, &self.metadata, c, self.weight);
}
gains
}
}
}
#[derive(Debug)]
pub struct StreamDecoder {
metadata: GainMapMetadata,
sdr_width: u32,
sdr_height: u32,
gm_width: u32,
gm_height: u32,
gm_channels: u8,
weight: f32,
current_sdr_row: u32,
current_gm_row: u32,
gamut: ColorGamut,
gm_buffer: GainMapRingBuffer,
}
#[derive(Debug)]
struct GainMapRingBuffer {
rows: Vec<Vec<u8>>,
first_row: u32,
count: u32,
row_bytes: usize,
capacity: u32,
}
impl GainMapRingBuffer {
fn new(gm_width: u32, gm_channels: u8, capacity: u32) -> Self {
let row_bytes = gm_width as usize * gm_channels as usize;
Self {
rows: vec![vec![0u8; row_bytes]; capacity as usize],
first_row: 0,
count: 0,
row_bytes,
capacity,
}
}
fn push(&mut self, row_index: u32, data: &[u8]) {
let slot = (row_index % self.capacity) as usize;
let copy_len = data.len().min(self.row_bytes);
self.rows[slot][..copy_len].copy_from_slice(&data[..copy_len]);
if self.count == 0 {
self.first_row = row_index;
}
let new_last = row_index + 1;
let new_first = new_last.saturating_sub(self.capacity);
if new_first > self.first_row {
self.first_row = new_first;
}
self.count = (new_last - self.first_row).min(self.capacity);
}
fn contains(&self, row: u32) -> bool {
if self.count == 0 {
return false;
}
row >= self.first_row && row < self.first_row + self.count
}
fn get(&self, row: u32) -> Option<&[u8]> {
if !self.contains(row) {
return None;
}
let slot = (row % self.capacity) as usize;
Some(&self.rows[slot])
}
}
impl StreamDecoder {
#[allow(clippy::too_many_arguments)]
pub fn new(
metadata: GainMapMetadata,
sdr_width: u32,
sdr_height: u32,
gm_width: u32,
gm_height: u32,
gm_channels: u8,
display_boost: f32,
gamut: ColorGamut,
) -> Result<Self> {
let weight = calculate_weight(display_boost, &metadata);
let gm_buffer = GainMapRingBuffer::new(gm_width, gm_channels, 16);
Ok(Self {
metadata,
sdr_width,
sdr_height,
gm_width,
gm_height,
gm_channels,
weight,
current_sdr_row: 0,
current_gm_row: 0,
gamut,
gm_buffer,
})
}
pub fn gamut(&self) -> ColorGamut {
self.gamut
}
pub fn push_gainmap_row(&mut self, data: &[u8]) -> Result<()> {
if self.current_gm_row >= self.gm_height {
return Err(Error::InvalidPixelData(
"all gainmap rows already received".into(),
));
}
self.gm_buffer.push(self.current_gm_row, data);
self.current_gm_row += 1;
Ok(())
}
pub fn push_gainmap_rows(&mut self, data: &[u8], num_rows: u32) -> Result<()> {
let row_bytes = self.gm_width as usize * self.gm_channels as usize;
for i in 0..num_rows {
let start = i as usize * row_bytes;
let end = start + row_bytes;
if end > data.len() {
return Err(Error::InvalidPixelData("gainmap data too short".into()));
}
self.push_gainmap_row(&data[start..end])?;
}
Ok(())
}
pub fn can_process(&self, num_sdr_rows: u32) -> bool {
if self.current_sdr_row >= self.sdr_height {
return false;
}
let last_sdr_row = (self.current_sdr_row + num_sdr_rows - 1).min(self.sdr_height - 1);
let gm_y_last = (last_sdr_row as f32 / self.sdr_height as f32) * self.gm_height as f32;
let gm_y1_needed = (gm_y_last.ceil() as u32).min(self.gm_height - 1);
self.current_gm_row > gm_y1_needed || self.gm_buffer.contains(gm_y1_needed)
}
pub fn process_sdr_rows(&mut self, sdr_linear: &[f32], num_rows: u32) -> Result<Vec<f32>> {
let remaining = self.sdr_height - self.current_sdr_row;
let actual_rows = num_rows.min(remaining);
if actual_rows == 0 {
return Err(Error::InvalidPixelData(
"all SDR rows already processed".into(),
));
}
if !self.can_process(actual_rows) {
return Err(Error::InvalidPixelData(
"insufficient gainmap data buffered".into(),
));
}
let input_stride = self.sdr_width as usize * 3;
let expected_len = input_stride * actual_rows as usize;
if sdr_linear.len() < expected_len {
return Err(Error::InvalidPixelData(format!(
"SDR data too short: {} < {} floats",
sdr_linear.len(),
expected_len
)));
}
let output_stride = self.sdr_width as usize * 4;
let mut output = vec![0.0f32; output_stride * actual_rows as usize];
for row_offset in 0..actual_rows {
let y = self.current_sdr_row + row_offset;
let input_start = row_offset as usize * input_stride;
let output_start = row_offset as usize * output_stride;
for x in 0..self.sdr_width {
let in_idx = input_start + x as usize * 3;
let out_idx = output_start + x as usize * 4;
let sdr = [
sdr_linear[in_idx],
sdr_linear[in_idx + 1],
sdr_linear[in_idx + 2],
];
let gain = self.sample_gainmap(x, y);
let hdr = apply_gain(sdr, gain, &self.metadata);
output[out_idx] = hdr[0];
output[out_idx + 1] = hdr[1];
output[out_idx + 2] = hdr[2];
output[out_idx + 3] = 1.0;
}
}
self.current_sdr_row += actual_rows;
Ok(output)
}
fn sample_gainmap(&self, x: u32, y: u32) -> [f32; 3] {
let gm_x = (x as f32 / self.sdr_width as f32) * self.gm_width as f32;
let gm_y = (y as f32 / self.sdr_height as f32) * self.gm_height as f32;
let x0 = (gm_x.floor() as u32).min(self.gm_width - 1);
let y0 = (gm_y.floor() as u32).min(self.gm_height - 1);
let x1 = (x0 + 1).min(self.gm_width - 1);
let y1 = (y0 + 1).min(self.gm_height - 1);
let fx = gm_x - gm_x.floor();
let fy = gm_y - gm_y.floor();
let row0 = self.gm_buffer.get(y0);
let row1 = self.gm_buffer.get(y1);
if self.gm_channels == 1 {
let v00 = Self::sample_row_gray(row0, x0);
let v10 = Self::sample_row_gray(row0, x1);
let v01 = Self::sample_row_gray(row1, x0);
let v11 = Self::sample_row_gray(row1, x1);
let v = bilinear(v00, v10, v01, v11, fx, fy);
let gain = decode_gain(v, &self.metadata, 0, self.weight);
[gain, gain, gain]
} else {
let mut gains = [0.0f32; 3];
#[allow(clippy::needless_range_loop)]
for c in 0..3 {
let v00 = Self::sample_row_rgb(row0, x0, c);
let v10 = Self::sample_row_rgb(row0, x1, c);
let v01 = Self::sample_row_rgb(row1, x0, c);
let v11 = Self::sample_row_rgb(row1, x1, c);
let v = bilinear(v00, v10, v01, v11, fx, fy);
gains[c] = decode_gain(v, &self.metadata, c, self.weight);
}
gains
}
}
#[inline]
fn sample_row_gray(row: Option<&[u8]>, x: u32) -> f32 {
row.and_then(|r| r.get(x as usize).copied()).unwrap_or(128) as f32 / 255.0
}
#[inline]
fn sample_row_rgb(row: Option<&[u8]>, x: u32, c: usize) -> f32 {
row.and_then(|r| r.get(x as usize * 3 + c).copied())
.unwrap_or(128) as f32
/ 255.0
}
pub fn sdr_rows_remaining(&self) -> u32 {
self.sdr_height - self.current_sdr_row
}
pub fn gainmap_rows_remaining(&self) -> u32 {
self.gm_height - self.current_gm_row
}
pub fn is_complete(&self) -> bool {
self.current_sdr_row >= self.sdr_height
}
}
#[derive(Debug)]
pub struct RowEncoder {
config: GainMapConfig,
width: u32,
height: u32,
gm_width: u32,
gm_height: u32,
scale: u32,
current_input_row: u32,
current_gm_row: u32,
hdr_buffer: LinearRowBuffer,
sdr_buffer: LinearRowBuffer,
actual_min_boost: f32,
actual_max_boost: f32,
gainmap_rows: Vec<Vec<u8>>,
hdr_gamut: ColorGamut,
sdr_gamut: ColorGamut,
}
#[derive(Debug)]
struct LinearRowBuffer {
rows: Vec<Vec<f32>>,
first_row: u32,
count: u32,
width: u32,
}
impl LinearRowBuffer {
fn new(capacity: usize, width: u32) -> Self {
let row_len = width as usize * 3; Self {
rows: vec![vec![0.0f32; row_len]; capacity],
first_row: 0,
count: 0,
width,
}
}
fn push_row(&mut self, row: u32, data: &[f32]) {
let capacity = self.rows.len() as u32;
let idx = (row % capacity) as usize;
let copy_len = data.len().min(self.rows[idx].len());
self.rows[idx][..copy_len].copy_from_slice(&data[..copy_len]);
if self.count == 0 {
self.first_row = row;
}
self.count = self.count.saturating_add(1).min(capacity);
}
fn push_rows(&mut self, start_row: u32, data: &[f32], num_rows: u32) {
let stride = self.width as usize * 3;
for i in 0..num_rows {
let row_data = &data[i as usize * stride..];
self.push_row(start_row + i, row_data);
}
}
fn get_pixel(&self, row: u32, x: u32) -> Option<[f32; 3]> {
let capacity = self.rows.len() as u32;
if row < self.first_row || row >= self.first_row + self.count {
return None;
}
let idx = (row % capacity) as usize;
let row_data = &self.rows[idx];
let px_idx = x as usize * 3;
if px_idx + 2 < row_data.len() {
Some([row_data[px_idx], row_data[px_idx + 1], row_data[px_idx + 2]])
} else {
None
}
}
}
impl RowEncoder {
pub fn new(
width: u32,
height: u32,
config: GainMapConfig,
hdr_gamut: ColorGamut,
sdr_gamut: ColorGamut,
) -> Result<Self> {
let scale = config.scale_factor.max(1) as u32;
let gm_width = width.div_ceil(scale);
let gm_height = height.div_ceil(scale);
let buffer_size = (scale as usize + 16).max(32);
Ok(Self {
config,
width,
height,
gm_width,
gm_height,
scale,
current_input_row: 0,
current_gm_row: 0,
hdr_buffer: LinearRowBuffer::new(buffer_size, width),
sdr_buffer: LinearRowBuffer::new(buffer_size, width),
actual_min_boost: f32::MAX,
actual_max_boost: f32::MIN,
gainmap_rows: Vec::new(),
hdr_gamut,
sdr_gamut,
})
}
pub fn process_rows(
&mut self,
hdr_linear: &[f32],
sdr_linear: &[f32],
num_rows: u32,
) -> Result<Vec<Vec<u8>>> {
let remaining = self.height - self.current_input_row;
let actual_rows = num_rows.min(remaining);
if actual_rows == 0 {
return Ok(Vec::new());
}
self.hdr_buffer
.push_rows(self.current_input_row, hdr_linear, actual_rows);
self.sdr_buffer
.push_rows(self.current_input_row, sdr_linear, actual_rows);
self.current_input_row += actual_rows;
let mut output_rows = Vec::new();
while self.current_gm_row < self.gm_height {
let target_y = self.current_gm_row * self.scale + self.scale / 2;
let target_y = target_y.min(self.height - 1);
if self.current_input_row > target_y {
let gm_row = self.compute_gainmap_row()?;
self.gainmap_rows.push(gm_row.clone());
output_rows.push(gm_row);
self.current_gm_row += 1;
} else {
break;
}
}
Ok(output_rows)
}
pub fn process_row(
&mut self,
hdr_linear: &[f32],
sdr_linear: &[f32],
) -> Result<Option<Vec<u8>>> {
let rows = self.process_rows(hdr_linear, sdr_linear, 1)?;
Ok(rows.into_iter().next())
}
pub fn finish(mut self) -> Result<(GainMap, GainMapMetadata)> {
while self.current_gm_row < self.gm_height {
let gm_row = self.compute_gainmap_row()?;
self.gainmap_rows.push(gm_row);
self.current_gm_row += 1;
}
let channels = if self.config.multi_channel { 3 } else { 1 };
let mut gainmap = if self.config.multi_channel {
GainMap::new_multichannel(self.gm_width, self.gm_height)?
} else {
GainMap::new(self.gm_width, self.gm_height)?
};
for (gy, row) in self.gainmap_rows.iter().enumerate() {
let start = gy * self.gm_width as usize * channels;
let end = start + row.len();
gainmap.data[start..end].copy_from_slice(row);
}
let actual_min = self.actual_min_boost.max(self.config.min_boost);
let actual_max = self.actual_max_boost.min(self.config.max_boost);
let metadata = GainMapMetadata {
gain_map_max: [(actual_max as f64).log2(); 3],
gain_map_min: [(actual_min as f64).log2(); 3],
gamma: [self.config.gamma as f64; 3],
base_offset: [self.config.base_offset as f64; 3],
alternate_offset: [self.config.alternate_offset as f64; 3],
base_hdr_headroom: (self.config.base_hdr_headroom as f64).log2(),
alternate_hdr_headroom: (self.config.alternate_hdr_headroom.max(actual_max) as f64)
.log2(),
use_base_color_space: true,
backward_direction: false,
};
Ok((gainmap, metadata))
}
pub fn progress(&self) -> (u32, u32) {
(self.current_input_row, self.height)
}
fn compute_gainmap_row(&mut self) -> Result<Vec<u8>> {
let gy = self.current_gm_row;
let channels = if self.config.multi_channel { 3 } else { 1 };
let mut row = vec![0u8; self.gm_width as usize * channels];
let log_min = self.config.min_boost.ln();
let log_max = self.config.max_boost.ln();
let log_range = log_max - log_min;
for gx in 0..self.gm_width {
let x = (gx * self.scale + self.scale / 2).min(self.width - 1);
let y = (gy * self.scale + self.scale / 2).min(self.height - 1);
let hdr_rgb = self.hdr_buffer.get_pixel(y, x).unwrap_or([0.5, 0.5, 0.5]);
let sdr_rgb = self.sdr_buffer.get_pixel(y, x).unwrap_or([0.5, 0.5, 0.5]);
if self.config.multi_channel {
for c in 0..3 {
let gain = (hdr_rgb[c] + self.config.alternate_offset)
/ (sdr_rgb[c] + self.config.base_offset).max(0.001);
self.actual_min_boost = self.actual_min_boost.min(gain);
self.actual_max_boost = self.actual_max_boost.max(gain);
let encoded = encode_gain(gain, log_min, log_range, &self.config);
row[gx as usize * 3 + c] = encoded;
}
} else {
let hdr_lum = rgb_to_luminance(hdr_rgb, self.hdr_gamut);
let sdr_lum = rgb_to_luminance(sdr_rgb, self.sdr_gamut);
let gain =
(hdr_lum + self.config.alternate_offset) / (sdr_lum + self.config.base_offset);
self.actual_min_boost = self.actual_min_boost.min(gain);
self.actual_max_boost = self.actual_max_boost.max(gain);
let encoded = encode_gain(gain, log_min, log_range, &self.config);
row[gx as usize] = encoded;
}
}
Ok(row)
}
}
#[derive(Debug)]
pub struct StreamEncoder {
config: GainMapConfig,
width: u32,
height: u32,
gm_width: u32,
gm_height: u32,
scale: u32,
hdr_rows: LinearInputRingBuffer,
sdr_rows: LinearInputRingBuffer,
next_hdr_row: u32,
next_sdr_row: u32,
next_gm_row: u32,
actual_min_boost: f32,
actual_max_boost: f32,
pending_gm_rows: Vec<Vec<u8>>,
hdr_gamut: ColorGamut,
sdr_gamut: ColorGamut,
}
#[derive(Debug)]
struct LinearInputRingBuffer {
rows: Vec<Vec<f32>>,
first_row: u32,
count: u32,
row_floats: usize,
capacity: u32,
}
impl LinearInputRingBuffer {
fn new(row_floats: usize, capacity: u32) -> Self {
Self {
rows: vec![vec![0.0f32; row_floats]; capacity as usize],
first_row: 0,
count: 0,
row_floats,
capacity,
}
}
fn push(&mut self, row_index: u32, data: &[f32]) {
let slot = (row_index % self.capacity) as usize;
let copy_len = data.len().min(self.row_floats);
self.rows[slot][..copy_len].copy_from_slice(&data[..copy_len]);
if self.count == 0 {
self.first_row = row_index;
}
let new_last = row_index + 1;
let new_first = new_last.saturating_sub(self.capacity);
if new_first > self.first_row {
self.first_row = new_first;
}
self.count = (new_last - self.first_row).min(self.capacity);
}
fn get(&self, row: u32) -> Option<&[f32]> {
if self.count == 0 || row < self.first_row || row >= self.first_row + self.count {
return None;
}
let slot = (row % self.capacity) as usize;
Some(&self.rows[slot])
}
fn has_row(&self, row: u32) -> bool {
self.count > 0 && row >= self.first_row && row < self.first_row + self.count
}
}
impl StreamEncoder {
pub fn new(
width: u32,
height: u32,
config: GainMapConfig,
hdr_gamut: ColorGamut,
sdr_gamut: ColorGamut,
) -> Result<Self> {
let scale = config.scale_factor.max(1) as u32;
let gm_width = width.div_ceil(scale);
let gm_height = height.div_ceil(scale);
let row_floats = width as usize * 3; let buffer_capacity = (scale + 16).min(32);
Ok(Self {
config,
width,
height,
gm_width,
gm_height,
scale,
hdr_rows: LinearInputRingBuffer::new(row_floats, buffer_capacity),
sdr_rows: LinearInputRingBuffer::new(row_floats, buffer_capacity),
next_hdr_row: 0,
next_sdr_row: 0,
next_gm_row: 0,
actual_min_boost: f32::MAX,
actual_max_boost: f32::MIN,
pending_gm_rows: Vec::new(),
hdr_gamut,
sdr_gamut,
})
}
pub fn push_hdr_rows(&mut self, data: &[f32], num_rows: u32) -> Result<()> {
let remaining = self.height - self.next_hdr_row;
let actual = num_rows.min(remaining);
let stride = self.width as usize * 3;
for i in 0..actual {
let start = i as usize * stride;
let end = start + stride;
if end > data.len() {
return Err(Error::InvalidPixelData("HDR data too short".into()));
}
self.hdr_rows.push(self.next_hdr_row + i, &data[start..end]);
}
self.next_hdr_row += actual;
self.try_produce_gainmap_rows();
Ok(())
}
pub fn push_hdr_row(&mut self, data: &[f32]) -> Result<()> {
if self.next_hdr_row >= self.height {
return Err(Error::InvalidPixelData(
"all HDR rows already received".into(),
));
}
self.hdr_rows.push(self.next_hdr_row, data);
self.next_hdr_row += 1;
self.try_produce_gainmap_rows();
Ok(())
}
pub fn push_sdr_rows(&mut self, data: &[f32], num_rows: u32) -> Result<()> {
let remaining = self.height - self.next_sdr_row;
let actual = num_rows.min(remaining);
let stride = self.width as usize * 3;
for i in 0..actual {
let start = i as usize * stride;
let end = start + stride;
if end > data.len() {
return Err(Error::InvalidPixelData("SDR data too short".into()));
}
self.sdr_rows.push(self.next_sdr_row + i, &data[start..end]);
}
self.next_sdr_row += actual;
self.try_produce_gainmap_rows();
Ok(())
}
pub fn push_sdr_row(&mut self, data: &[f32]) -> Result<()> {
if self.next_sdr_row >= self.height {
return Err(Error::InvalidPixelData(
"all SDR rows already received".into(),
));
}
self.sdr_rows.push(self.next_sdr_row, data);
self.next_sdr_row += 1;
self.try_produce_gainmap_rows();
Ok(())
}
pub fn take_gainmap_row(&mut self) -> Option<Vec<u8>> {
if self.pending_gm_rows.is_empty() {
None
} else {
Some(self.pending_gm_rows.remove(0))
}
}
pub fn pending_gainmap_rows(&self) -> usize {
self.pending_gm_rows.len()
}
fn try_produce_gainmap_rows(&mut self) {
while self.next_gm_row < self.gm_height {
let center_y = self.next_gm_row * self.scale + self.scale / 2;
let center_y = center_y.min(self.height - 1);
if !self.hdr_rows.has_row(center_y) || !self.sdr_rows.has_row(center_y) {
break;
}
let gm_row = self.compute_gainmap_row(self.next_gm_row);
self.pending_gm_rows.push(gm_row);
self.next_gm_row += 1;
}
}
fn compute_gainmap_row(&mut self, gm_y: u32) -> Vec<u8> {
let channels = if self.config.multi_channel { 3 } else { 1 };
let mut row = vec![0u8; self.gm_width as usize * channels];
let center_y = (gm_y * self.scale + self.scale / 2).min(self.height - 1);
let hdr_row_data = self.hdr_rows.get(center_y);
let sdr_row_data = self.sdr_rows.get(center_y);
let log_min = self.config.min_boost.ln();
let log_max = self.config.max_boost.ln();
let log_range = log_max - log_min;
for gx in 0..self.gm_width {
let center_x = (gx * self.scale + self.scale / 2).min(self.width - 1);
let hdr_rgb = self.get_pixel(hdr_row_data, center_x);
let sdr_rgb = self.get_pixel(sdr_row_data, center_x);
if self.config.multi_channel {
#[allow(clippy::needless_range_loop)]
for c in 0..3 {
let hdr_c = hdr_rgb[c] + self.config.alternate_offset;
let sdr_c = sdr_rgb[c] + self.config.base_offset;
let gain = hdr_c / sdr_c.max(1e-6);
self.actual_min_boost = self.actual_min_boost.min(gain);
self.actual_max_boost = self.actual_max_boost.max(gain);
row[gx as usize * 3 + c] = encode_gain(gain, log_min, log_range, &self.config);
}
} else {
let hdr_lum = rgb_to_luminance(hdr_rgb, self.hdr_gamut);
let sdr_lum = rgb_to_luminance(sdr_rgb, self.sdr_gamut);
let gain =
(hdr_lum + self.config.alternate_offset) / (sdr_lum + self.config.base_offset);
self.actual_min_boost = self.actual_min_boost.min(gain);
self.actual_max_boost = self.actual_max_boost.max(gain);
row[gx as usize] = encode_gain(gain, log_min, log_range, &self.config);
}
}
row
}
fn get_pixel(&self, row: Option<&[f32]>, x: u32) -> [f32; 3] {
match row {
Some(r) => {
let idx = x as usize * 3;
if idx + 2 < r.len() {
[r[idx], r[idx + 1], r[idx + 2]]
} else {
[0.5, 0.5, 0.5]
}
}
None => [0.5, 0.5, 0.5],
}
}
pub fn finish(mut self) -> Result<(GainMap, GainMapMetadata)> {
let mut all_rows = Vec::new();
all_rows.append(&mut self.pending_gm_rows);
if all_rows.len() != self.gm_height as usize {
return Err(Error::InvalidPixelData(format!(
"incomplete gainmap: {} of {} rows",
all_rows.len(),
self.gm_height
)));
}
let channels = if self.config.multi_channel { 3u8 } else { 1u8 };
let row_bytes = self.gm_width as usize * channels as usize;
let mut data = Vec::with_capacity(row_bytes * self.gm_height as usize);
for row in all_rows {
data.extend_from_slice(&row);
}
let gainmap = GainMap {
width: self.gm_width,
height: self.gm_height,
channels,
data,
};
let actual_max = if self.actual_max_boost > f32::MIN {
self.actual_max_boost
} else {
self.config.max_boost
};
let actual_min = if self.actual_min_boost < f32::MAX {
self.actual_min_boost
} else {
self.config.min_boost
};
let metadata = GainMapMetadata {
gain_map_max: [(actual_max as f64).log2(); 3],
gain_map_min: [(actual_min as f64).log2(); 3],
gamma: [self.config.gamma as f64; 3],
base_offset: [self.config.base_offset as f64; 3],
alternate_offset: [self.config.alternate_offset as f64; 3],
base_hdr_headroom: 0.0, alternate_hdr_headroom: (actual_max as f64).log2(),
use_base_color_space: true,
backward_direction: false,
};
Ok((gainmap, metadata))
}
pub fn inputs_complete(&self) -> bool {
self.next_hdr_row >= self.height && self.next_sdr_row >= self.height
}
pub fn is_complete(&self) -> bool {
self.next_gm_row >= self.gm_height && self.pending_gm_rows.is_empty()
}
pub fn hdr_rows_remaining(&self) -> u32 {
self.height - self.next_hdr_row
}
pub fn sdr_rows_remaining(&self) -> u32 {
self.height - self.next_sdr_row
}
pub fn gainmap_rows_remaining(&self) -> u32 {
self.gm_height - self.next_gm_row
}
}
fn calculate_weight(display_boost: f32, metadata: &GainMapMetadata) -> f32 {
let log_display = display_boost.max(1.0).log2() as f64;
let log_min = metadata.base_hdr_headroom.max(0.0);
let log_max = metadata.alternate_hdr_headroom.max(0.0);
if log_max <= log_min {
return 1.0;
}
((log_display - log_min) / (log_max - log_min)).clamp(0.0, 1.0) as f32
}
#[inline]
fn bilinear(v00: f32, v10: f32, v01: f32, v11: f32, fx: f32, fy: f32) -> f32 {
let top = v00 * (1.0 - fx) + v10 * fx;
let bottom = v01 * (1.0 - fx) + v11 * fx;
top * (1.0 - fy) + bottom * fy
}
fn decode_gain(normalized: f32, metadata: &GainMapMetadata, channel: usize, weight: f32) -> f32 {
let gamma = metadata.gamma[channel] as f32;
let linear = if gamma != 1.0 && gamma > 0.0 {
normalized.powf(1.0 / gamma)
} else {
normalized
};
let ln2 = core::f64::consts::LN_2;
let log_min = (metadata.gain_map_min[channel] * ln2) as f32;
let log_max = (metadata.gain_map_max[channel] * ln2) as f32;
let log_gain = log_min + linear * (log_max - log_min);
(log_gain * weight).exp()
}
fn apply_gain(sdr_linear: [f32; 3], gain: [f32; 3], metadata: &GainMapMetadata) -> [f32; 3] {
[
(sdr_linear[0] + metadata.base_offset[0] as f32) * gain[0]
- metadata.alternate_offset[0] as f32,
(sdr_linear[1] + metadata.base_offset[1] as f32) * gain[1]
- metadata.alternate_offset[1] as f32,
(sdr_linear[2] + metadata.base_offset[2] as f32) * gain[2]
- metadata.alternate_offset[2] as f32,
]
}
fn encode_gain(gain: f32, log_min: f32, log_range: f32, config: &GainMapConfig) -> u8 {
let gain_clamped = gain.clamp(config.min_boost, config.max_boost);
let log_gain = gain_clamped.ln();
let normalized = if log_range > 0.0 {
(log_gain - log_min) / log_range
} else {
0.5
};
let gamma_corrected = normalized.powf(config.gamma);
(gamma_corrected * 255.0).round().clamp(0.0, 255.0) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_row_decoder_linear_f32() {
let mut gainmap = GainMap::new(2, 2).unwrap();
for v in &mut gainmap.data {
*v = 128;
}
let metadata = GainMapMetadata {
gain_map_min: [0.0; 3], gain_map_max: [2.0; 3], gamma: [1.0; 3],
base_offset: [0.015625; 3],
alternate_offset: [0.015625; 3],
base_hdr_headroom: 0.0, alternate_hdr_headroom: 2.0, use_base_color_space: true,
backward_direction: false,
};
let mut decoder = RowDecoder::new(gainmap, metadata, 4, 4, 4.0, ColorGamut::Bt709).unwrap();
let sdr_linear = vec![0.18f32; 4 * 3]; let hdr = decoder.process_row(&sdr_linear).unwrap();
assert_eq!(hdr.len(), 4 * 4); assert!(hdr[0] > 0.0); }
#[test]
fn test_row_encoder_linear_f32() {
let config = GainMapConfig {
scale_factor: 2,
..Default::default()
};
let mut encoder =
RowEncoder::new(4, 4, config, ColorGamut::Bt709, ColorGamut::Bt709).unwrap();
let hdr_linear = vec![0.5f32; 4 * 3]; let sdr_linear = vec![0.18f32; 4 * 3];
for _ in 0..4 {
let _ = encoder.process_row(&hdr_linear, &sdr_linear).unwrap();
}
let (gainmap, metadata) = encoder.finish().unwrap();
assert_eq!(gainmap.width, 2);
assert_eq!(gainmap.height, 2);
assert!(metadata.gain_map_max[0] >= 1.0);
}
#[test]
fn test_stream_encoder_linear_f32() {
let config = GainMapConfig {
scale_factor: 2,
..Default::default()
};
let mut encoder =
StreamEncoder::new(4, 4, config, ColorGamut::Bt709, ColorGamut::Bt709).unwrap();
let hdr_linear = vec![0.5f32; 4 * 3];
let sdr_linear = vec![0.18f32; 4 * 3];
for _ in 0..4 {
encoder.push_hdr_row(&hdr_linear).unwrap();
encoder.push_sdr_row(&sdr_linear).unwrap();
}
let (gainmap, _) = encoder.finish().unwrap();
assert_eq!(gainmap.width, 2);
assert_eq!(gainmap.height, 2);
}
fn test_metadata() -> GainMapMetadata {
GainMapMetadata {
gain_map_min: [0.0; 3], gain_map_max: [2.0; 3], gamma: [1.0; 3],
base_offset: [0.015625; 3],
alternate_offset: [0.015625; 3],
base_hdr_headroom: 0.0, alternate_hdr_headroom: 2.0, use_base_color_space: true,
backward_direction: false,
}
}
fn test_gainmap(value: u8) -> GainMap {
let mut gm = GainMap::new(2, 2).unwrap();
for v in &mut gm.data {
*v = value;
}
gm
}
#[test]
fn test_row_decoder_completion() {
let gainmap = test_gainmap(128);
let metadata = test_metadata();
let mut decoder = RowDecoder::new(gainmap, metadata, 4, 4, 4.0, ColorGamut::Bt709).unwrap();
assert!(!decoder.is_complete());
assert_eq!(decoder.rows_remaining(), 4);
let sdr_row = vec![0.18f32; 4 * 3];
for i in 0..4u32 {
assert!(!decoder.is_complete());
assert_eq!(decoder.rows_remaining(), 4 - i);
assert_eq!(decoder.current_row(), i);
let hdr = decoder.process_row(&sdr_row).unwrap();
assert_eq!(hdr.len(), 4 * 4); }
assert!(decoder.is_complete());
assert_eq!(decoder.rows_remaining(), 0);
assert_eq!(decoder.current_row(), 4);
}
#[test]
fn test_row_decoder_reset() {
let gainmap = test_gainmap(128);
let metadata = test_metadata();
let mut decoder = RowDecoder::new(gainmap, metadata, 4, 4, 4.0, ColorGamut::Bt709).unwrap();
let sdr_row = vec![0.18f32; 4 * 3];
decoder.process_row(&sdr_row).unwrap();
decoder.process_row(&sdr_row).unwrap();
assert_eq!(decoder.current_row(), 2);
assert_eq!(decoder.rows_remaining(), 2);
decoder.reset();
assert_eq!(decoder.current_row(), 0);
assert_eq!(decoder.rows_remaining(), 4);
assert!(!decoder.is_complete());
let hdr = decoder.process_row(&sdr_row).unwrap();
assert_eq!(hdr.len(), 4 * 4);
assert_eq!(decoder.current_row(), 1);
}
#[test]
fn test_row_decoder_input_too_short() {
let gainmap = test_gainmap(128);
let metadata = test_metadata();
let mut decoder = RowDecoder::new(gainmap, metadata, 4, 4, 4.0, ColorGamut::Bt709).unwrap();
let short_input = vec![0.18f32; 2 * 3];
let result = decoder.process_row(&short_input);
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("too short"),
"expected 'too short' in error: {err_msg}"
);
}
#[test]
fn test_row_decoder_after_complete() {
let gainmap = test_gainmap(128);
let metadata = test_metadata();
let mut decoder = RowDecoder::new(gainmap, metadata, 4, 4, 4.0, ColorGamut::Bt709).unwrap();
let sdr_row = vec![0.18f32; 4 * 3];
for _ in 0..4 {
decoder.process_row(&sdr_row).unwrap();
}
assert!(decoder.is_complete());
let result = decoder.process_row(&sdr_row);
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(
err_msg.contains("already processed"),
"expected 'already processed' in error: {err_msg}"
);
}
#[test]
fn test_stream_decoder_basic() {
let metadata = test_metadata();
let mut decoder =
StreamDecoder::new(metadata.clone(), 4, 4, 2, 2, 1, 4.0, ColorGamut::Bt709).unwrap();
assert!(!decoder.is_complete());
assert_eq!(decoder.sdr_rows_remaining(), 4);
assert_eq!(decoder.gainmap_rows_remaining(), 2);
let gm_row = vec![128u8; 2];
decoder.push_gainmap_row(&gm_row).unwrap();
decoder.push_gainmap_row(&gm_row).unwrap();
assert_eq!(decoder.gainmap_rows_remaining(), 0);
let sdr_row = vec![0.18f32; 4 * 3];
for _ in 0..4 {
assert!(decoder.can_process(1));
let hdr = decoder.process_sdr_rows(&sdr_row, 1).unwrap();
assert_eq!(hdr.len(), 4 * 4); for &v in &hdr {
assert!(v >= 0.0, "HDR output should be non-negative, got {v}");
}
}
assert!(decoder.is_complete());
assert_eq!(decoder.sdr_rows_remaining(), 0);
}
#[test]
fn test_row_encoder_process_rows_batch() {
let config = GainMapConfig {
scale_factor: 2,
..Default::default()
};
let mut encoder =
RowEncoder::new(4, 4, config, ColorGamut::Bt709, ColorGamut::Bt709).unwrap();
let hdr_linear = vec![0.5f32; 4 * 4 * 3]; let sdr_linear = vec![0.18f32; 4 * 4 * 3];
let gm_rows = encoder.process_rows(&hdr_linear, &sdr_linear, 4).unwrap();
let (current, total) = encoder.progress();
assert_eq!(total, 4);
assert_eq!(current, 4);
let (gainmap, metadata) = encoder.finish().unwrap();
assert_eq!(gainmap.width, 2);
assert_eq!(gainmap.height, 2);
assert!(metadata.gain_map_max[0] >= 1.0);
assert_eq!(gm_rows.len(), 2);
}
#[test]
fn test_stream_encoder_status_methods() {
let config = GainMapConfig {
scale_factor: 2,
..Default::default()
};
let mut encoder =
StreamEncoder::new(4, 4, config, ColorGamut::Bt709, ColorGamut::Bt709).unwrap();
assert_eq!(encoder.hdr_rows_remaining(), 4);
assert_eq!(encoder.sdr_rows_remaining(), 4);
assert!(!encoder.inputs_complete());
assert!(!encoder.is_complete());
let hdr_row = vec![0.5f32; 4 * 3];
let sdr_row = vec![0.18f32; 4 * 3];
encoder.push_hdr_row(&hdr_row).unwrap();
encoder.push_hdr_row(&hdr_row).unwrap();
assert_eq!(encoder.hdr_rows_remaining(), 2);
assert_eq!(encoder.sdr_rows_remaining(), 4);
assert!(!encoder.inputs_complete());
encoder.push_sdr_row(&sdr_row).unwrap();
encoder.push_sdr_row(&sdr_row).unwrap();
assert_eq!(encoder.sdr_rows_remaining(), 2);
assert!(!encoder.inputs_complete());
encoder.push_hdr_row(&hdr_row).unwrap();
encoder.push_hdr_row(&hdr_row).unwrap();
encoder.push_sdr_row(&sdr_row).unwrap();
encoder.push_sdr_row(&sdr_row).unwrap();
assert_eq!(encoder.hdr_rows_remaining(), 0);
assert_eq!(encoder.sdr_rows_remaining(), 0);
assert!(encoder.inputs_complete());
assert_eq!(encoder.gainmap_rows_remaining(), 0);
}
#[test]
fn test_stream_encoder_interleaved() {
let config = GainMapConfig {
scale_factor: 2,
..Default::default()
};
let mut encoder =
StreamEncoder::new(4, 4, config, ColorGamut::Bt709, ColorGamut::Bt709).unwrap();
let hdr_row = vec![0.5f32; 4 * 3];
let sdr_row = vec![0.18f32; 4 * 3];
let mut total_gm_rows = 0;
for _ in 0..4 {
encoder.push_hdr_row(&hdr_row).unwrap();
encoder.push_sdr_row(&sdr_row).unwrap();
while let Some(gm_row) = encoder.take_gainmap_row() {
total_gm_rows += 1;
assert_eq!(gm_row.len(), 2);
}
}
assert_eq!(total_gm_rows, 2);
assert!(encoder.inputs_complete());
}
#[test]
fn test_stream_encoder_drain_pending() {
let config = GainMapConfig {
scale_factor: 2,
..Default::default()
};
let mut encoder =
StreamEncoder::new(4, 4, config, ColorGamut::Bt709, ColorGamut::Bt709).unwrap();
let hdr_row = vec![0.5f32; 4 * 3];
let sdr_row = vec![0.18f32; 4 * 3];
for _ in 0..4 {
encoder.push_hdr_row(&hdr_row).unwrap();
encoder.push_sdr_row(&sdr_row).unwrap();
}
assert!(encoder.pending_gainmap_rows() > 0);
let mut drained = Vec::new();
while let Some(row) = encoder.take_gainmap_row() {
drained.push(row);
}
assert_eq!(drained.len(), 2);
assert_eq!(encoder.pending_gainmap_rows(), 0);
for row in &drained {
assert_eq!(row.len(), 2);
}
}
#[test]
fn test_row_decoder_boost_1() {
let gainmap = test_gainmap(128);
let metadata = test_metadata();
let mut decoder = RowDecoder::new(gainmap, metadata, 4, 4, 1.0, ColorGamut::Bt709).unwrap();
let sdr_val = 0.5f32;
let sdr_row = vec![sdr_val; 4 * 3];
let hdr = decoder.process_row(&sdr_row).unwrap();
for px in 0..4 {
let r = hdr[px * 4];
let g = hdr[px * 4 + 1];
let b = hdr[px * 4 + 2];
let a = hdr[px * 4 + 3];
assert!(
(r - sdr_val).abs() < 0.01,
"R at pixel {px}: expected ~{sdr_val}, got {r}"
);
assert!(
(g - sdr_val).abs() < 0.01,
"G at pixel {px}: expected ~{sdr_val}, got {g}"
);
assert!(
(b - sdr_val).abs() < 0.01,
"B at pixel {px}: expected ~{sdr_val}, got {b}"
);
assert!(
(a - 1.0).abs() < f32::EPSILON,
"A at pixel {px}: expected 1.0, got {a}"
);
}
}
}