use alloc::format;
use alloc::vec;
use alloc::vec::Vec;
use crate::color::gamut::rgb_to_luminance;
use crate::types::{ColorPrimaries, Error, GainMap, GainMapMetadata, Result};
use super::compute::GainMapConfig;
#[doc(hidden)]
#[derive(Debug)]
pub struct RowDecoder {
gainmap: GainMap,
width: u32,
height: u32,
lut: super::apply::GainMapLut,
shepards: Option<super::apply::ShepardsLut>,
base_offset: [f32; 3],
alternate_offset: [f32; 3],
current_row: u32,
gamut: ColorPrimaries,
sdr_row: Vec<[f32; 3]>,
gains_row: Vec<[f32; 3]>,
hdr_row: Vec<[f32; 3]>,
}
impl RowDecoder {
pub fn new(
gainmap: GainMap,
metadata: GainMapMetadata,
width: u32,
height: u32,
display_boost: f32,
gamut: ColorPrimaries,
) -> Result<Self> {
let weight = super::apply::calculate_weight(display_boost, &metadata);
let lut = super::apply::GainMapLut::new(&metadata, weight);
let shepards =
super::apply::ShepardsLut::try_new(width, height, gainmap.width, gainmap.height);
let base_offset = [
metadata.channels[0].base_offset as f32,
metadata.channels[1].base_offset as f32,
metadata.channels[2].base_offset as f32,
];
let alternate_offset = [
metadata.channels[0].alternate_offset as f32,
metadata.channels[1].alternate_offset as f32,
metadata.channels[2].alternate_offset as f32,
];
let row_pixels = width as usize;
Ok(Self {
gainmap,
width,
height,
lut,
shepards,
base_offset,
alternate_offset,
current_row: 0,
gamut,
sdr_row: vec![[0.0; 3]; row_pixels],
gains_row: vec![[0.0; 3]; row_pixels],
hdr_row: vec![[0.0; 3]; row_pixels],
})
}
pub fn gamut(&self) -> ColorPrimaries {
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];
let row_pixels = self.width 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, dst) in self.sdr_row.iter_mut().enumerate() {
let in_idx = input_start + x * 3;
*dst = [
sdr_linear[in_idx],
sdr_linear[in_idx + 1],
sdr_linear[in_idx + 2],
];
}
super::apply::sample_gainmap_row_lut(
&self.gainmap,
&self.lut,
self.shepards.as_ref(),
y,
self.width,
self.height,
&mut self.gains_row,
);
super::apply_simd::apply_gain_row_presampled(
&self.sdr_row,
&self.gains_row,
self.base_offset,
self.alternate_offset,
&mut self.hdr_row,
);
for x in 0..row_pixels {
let out_idx = output_start + x * 4;
output[out_idx] = self.hdr_row[x][0];
output[out_idx + 1] = self.hdr_row[x][1];
output[out_idx + 2] = self.hdr_row[x][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;
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct StreamDecoder {
sdr_width: u32,
sdr_height: u32,
gm_width: u32,
gm_height: u32,
gm_channels: u8,
lut: super::apply::GainMapLut,
base_offset: [f32; 3],
alternate_offset: [f32; 3],
current_sdr_row: u32,
current_gm_row: u32,
gamut: ColorPrimaries,
gm_buffer: GainMapRingBuffer,
sdr_row: Vec<[f32; 3]>,
gains_row: Vec<[f32; 3]>,
hdr_row: Vec<[f32; 3]>,
}
#[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: ColorPrimaries,
) -> Result<Self> {
let weight = super::apply::calculate_weight(display_boost, &metadata);
let lut = super::apply::GainMapLut::new(&metadata, weight);
let base_offset = [
metadata.channels[0].base_offset as f32,
metadata.channels[1].base_offset as f32,
metadata.channels[2].base_offset as f32,
];
let alternate_offset = [
metadata.channels[0].alternate_offset as f32,
metadata.channels[1].alternate_offset as f32,
metadata.channels[2].alternate_offset as f32,
];
let gm_buffer = GainMapRingBuffer::new(gm_width, gm_channels, 16);
let row_pixels = sdr_width as usize;
Ok(Self {
sdr_width,
sdr_height,
gm_width,
gm_height,
gm_channels,
lut,
base_offset,
alternate_offset,
current_sdr_row: 0,
current_gm_row: 0,
gamut,
gm_buffer,
sdr_row: vec![[0.0; 3]; row_pixels],
gains_row: vec![[0.0; 3]; row_pixels],
hdr_row: vec![[0.0; 3]; row_pixels],
})
}
pub fn gamut(&self) -> ColorPrimaries {
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];
let row_pixels = self.sdr_width 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, dst) in self.sdr_row.iter_mut().enumerate() {
let in_idx = input_start + x * 3;
*dst = [
sdr_linear[in_idx],
sdr_linear[in_idx + 1],
sdr_linear[in_idx + 2],
];
}
sample_gainmap_row_ring(
&self.gm_buffer,
&self.lut,
self.gm_channels,
y,
self.sdr_width,
self.sdr_height,
self.gm_width,
self.gm_height,
&mut self.gains_row,
);
super::apply_simd::apply_gain_row_presampled(
&self.sdr_row,
&self.gains_row,
self.base_offset,
self.alternate_offset,
&mut self.hdr_row,
);
for x in 0..row_pixels {
let out_idx = output_start + x * 4;
output[out_idx] = self.hdr_row[x][0];
output[out_idx + 1] = self.hdr_row[x][1];
output[out_idx + 2] = self.hdr_row[x][2];
output[out_idx + 3] = 1.0;
}
}
self.current_sdr_row += actual_rows;
Ok(output)
}
#[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
}
}
#[doc(hidden)]
#[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_data: Vec<u8>,
gm_row_stride: usize,
hdr_gamut: ColorPrimaries,
sdr_gamut: ColorPrimaries,
}
#[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: ColorPrimaries,
sdr_gamut: ColorPrimaries,
) -> 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);
let channels = if config.multi_channel { 3 } else { 1 };
let gm_row_stride = gm_width as usize * channels;
let gainmap_data = vec![0u8; gm_row_stride * gm_height as usize];
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_data,
gm_row_stride,
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 gy = self.current_gm_row as usize;
let start = gy * self.gm_row_stride;
let end = start + self.gm_row_stride;
self.compute_gainmap_row_into_buffer();
output_rows.push(self.gainmap_data[start..end].to_vec());
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 {
self.compute_gainmap_row_into_buffer();
self.current_gm_row += 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)?
};
gainmap.data = self.gainmap_data;
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 = crate::types::metadata_from_arrays(
[(actual_min as f64).log2(); 3],
[(actual_max as f64).log2(); 3],
[self.config.gamma as f64; 3],
[self.config.base_offset as f64; 3],
[self.config.alternate_offset as f64; 3],
(self.config.base_hdr_headroom as f64).log2(),
(self.config.alternate_hdr_headroom.max(actual_max) as f64).log2(),
true,
false,
);
Ok((gainmap, metadata))
}
pub fn progress(&self) -> (u32, u32) {
(self.current_input_row, self.height)
}
fn compute_gainmap_row_into_buffer(&mut self) {
let gy = self.current_gm_row;
let start = gy as usize * self.gm_row_stride;
let end = start + self.gm_row_stride;
let log_min = self.config.min_boost.ln();
let log_max = self.config.max_boost.ln();
let log_range = log_max - log_min;
let mut min_boost = self.actual_min_boost;
let mut max_boost = self.actual_max_boost;
let multi = self.config.multi_channel;
let scale = self.scale;
let width = self.width;
let height = self.height;
let gm_width = self.gm_width;
let hdr_gamut = self.hdr_gamut;
let sdr_gamut = self.sdr_gamut;
let config = &self.config;
let row = &mut self.gainmap_data[start..end];
for gx in 0..gm_width {
let x = (gx * scale + scale / 2).min(width - 1);
let y = (gy * scale + scale / 2).min(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 multi {
for c in 0..3 {
row[gx as usize * 3 + c] = super::compute::compute_and_encode_gain(
hdr_rgb[c],
sdr_rgb[c],
config,
log_min,
log_range,
&mut min_boost,
&mut max_boost,
);
}
} else {
let hdr_lum = rgb_to_luminance(hdr_rgb, hdr_gamut);
let sdr_lum = rgb_to_luminance(sdr_rgb, sdr_gamut);
row[gx as usize] = super::compute::compute_and_encode_gain(
hdr_lum,
sdr_lum,
config,
log_min,
log_range,
&mut min_boost,
&mut max_boost,
);
}
}
self.actual_min_boost = min_boost;
self.actual_max_boost = max_boost;
}
}
#[doc(hidden)]
#[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: ColorPrimaries,
sdr_gamut: ColorPrimaries,
}
#[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: ColorPrimaries,
sdr_gamut: ColorPrimaries,
) -> 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 {
row[gx as usize * 3 + c] = super::compute::compute_and_encode_gain(
hdr_rgb[c],
sdr_rgb[c],
&self.config,
log_min,
log_range,
&mut self.actual_min_boost,
&mut self.actual_max_boost,
);
}
} else {
let hdr_lum = rgb_to_luminance(hdr_rgb, self.hdr_gamut);
let sdr_lum = rgb_to_luminance(sdr_rgb, self.sdr_gamut);
row[gx as usize] = super::compute::compute_and_encode_gain(
hdr_lum,
sdr_lum,
&self.config,
log_min,
log_range,
&mut self.actual_min_boost,
&mut self.actual_max_boost,
);
}
}
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 = crate::types::metadata_from_arrays(
[(actual_min as f64).log2(); 3],
[(actual_max as f64).log2(); 3],
[self.config.gamma as f64; 3],
[self.config.base_offset as f64; 3],
[self.config.alternate_offset as f64; 3],
0.0, (actual_max as f64).log2(),
true,
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
}
}
#[allow(clippy::too_many_arguments)]
fn sample_gainmap_row_ring(
gm_buffer: &GainMapRingBuffer,
lut: &super::apply::GainMapLut,
gm_channels: u8,
y: u32,
sdr_width: u32,
sdr_height: u32,
gm_width: u32,
gm_height: u32,
out: &mut [[f32; 3]],
) {
debug_assert_eq!(out.len(), sdr_width as usize);
let gm_y = (y as f32 / sdr_height as f32) * gm_height as f32;
let y0 = (gm_y.floor() as u32).min(gm_height - 1);
let y1 = (y0 + 1).min(gm_height - 1);
let fy = gm_y - gm_y.floor();
let row0 = gm_buffer.get(y0);
let row1 = gm_buffer.get(y1);
for (x, gain_out) in out.iter_mut().enumerate() {
let gm_x = (x as f32 / sdr_width as f32) * gm_width as f32;
let x0 = (gm_x.floor() as u32).min(gm_width - 1);
let x1 = (x0 + 1).min(gm_width - 1);
let fx = gm_x - gm_x.floor();
if gm_channels == 1 {
let v00 = StreamDecoder::sample_row_gray(row0, x0);
let v10 = StreamDecoder::sample_row_gray(row0, x1);
let v01 = StreamDecoder::sample_row_gray(row1, x0);
let v11 = StreamDecoder::sample_row_gray(row1, x1);
let v = bilinear(v00, v10, v01, v11, fx, fy);
let byte = (v * 255.0).round().clamp(0.0, 255.0) as u8;
let g = lut.lookup(byte, 0);
*gain_out = [g, g, g];
} else {
for (c, dst) in gain_out.iter_mut().enumerate() {
let v00 = StreamDecoder::sample_row_rgb(row0, x0, c);
let v10 = StreamDecoder::sample_row_rgb(row0, x1, c);
let v01 = StreamDecoder::sample_row_rgb(row1, x0, c);
let v11 = StreamDecoder::sample_row_rgb(row1, x1, c);
let v = bilinear(v00, v10, v01, v11, fx, fy);
let byte = (v * 255.0).round().clamp(0.0, 255.0) as u8;
*dst = lut.lookup(byte, c);
}
}
}
}
#[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
}
#[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 = crate::types::metadata_from_arrays(
[0.0; 3],
[2.0; 3],
[1.0; 3],
[0.015625; 3],
[0.015625; 3],
0.0,
2.0,
true,
false,
);
let mut decoder =
RowDecoder::new(gainmap, metadata, 4, 4, 4.0, ColorPrimaries::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, ColorPrimaries::Bt709, ColorPrimaries::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.channels[0].max >= 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, ColorPrimaries::Bt709, ColorPrimaries::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 {
crate::types::metadata_from_arrays(
[0.0; 3],
[2.0; 3],
[1.0; 3],
[0.015625; 3],
[0.015625; 3],
0.0,
2.0,
true,
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, ColorPrimaries::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, ColorPrimaries::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, ColorPrimaries::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, ColorPrimaries::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, ColorPrimaries::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, ColorPrimaries::Bt709, ColorPrimaries::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.channels[0].max >= 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, ColorPrimaries::Bt709, ColorPrimaries::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, ColorPrimaries::Bt709, ColorPrimaries::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, ColorPrimaries::Bt709, ColorPrimaries::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, ColorPrimaries::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}"
);
}
}
}