use crate::decode::{DecodedExtras, ScanlineReader};
use crate::error::{Error, Result};
use crate::types::Dimensions;
#[cfg(feature = "ultrahdr")]
use ultrahdr_core::{
ColorGamut, GainMap, GainMapMetadata,
gainmap::{RowDecoder, StreamDecoder},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum UltraHdrMode {
SdrOnly,
#[default]
Hdr,
SdrAndHdr,
SdrAndGainMap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum GainMapMemory {
#[default]
Full,
Streaming,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct UltraHdrReaderConfig {
pub mode: UltraHdrMode,
pub display_boost: f32,
pub memory_strategy: GainMapMemory,
pub preserve_metadata: bool,
}
impl Default for UltraHdrReaderConfig {
fn default() -> Self {
Self {
mode: UltraHdrMode::Hdr,
display_boost: 1.0,
memory_strategy: GainMapMemory::Full,
preserve_metadata: false,
}
}
}
impl UltraHdrReaderConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn mode(mut self, mode: UltraHdrMode) -> Self {
self.mode = mode;
self
}
#[must_use]
pub fn display_boost(mut self, boost: f32) -> Self {
self.display_boost = boost;
self
}
#[must_use]
pub fn memory_strategy(mut self, strategy: GainMapMemory) -> Self {
self.memory_strategy = strategy;
self
}
#[must_use]
pub fn preserve_metadata(mut self, preserve: bool) -> Self {
self.preserve_metadata = preserve;
self
}
#[must_use]
pub fn sdr_only() -> Self {
Self::new().mode(UltraHdrMode::SdrOnly)
}
#[must_use]
pub fn hdr_default() -> Self {
Self::new().mode(UltraHdrMode::Hdr).display_boost(4.0)
}
#[must_use]
pub fn editing() -> Self {
Self::new()
.mode(UltraHdrMode::SdrAndGainMap)
.preserve_metadata(true)
}
}
#[cfg(feature = "ultrahdr")]
pub struct UltraHdrReader<'a> {
config: UltraHdrReaderConfig,
base_reader: ScanlineReader<'a>,
data: &'a [u8],
is_ultrahdr: bool,
metadata: Option<GainMapMetadata>,
extras: Option<DecodedExtras>,
hdr_state: Option<HdrDecoderState<'a>>,
gainmap_range: Option<(usize, usize)>,
}
#[cfg(feature = "ultrahdr")]
enum HdrDecoderState<'a> {
RowDecoder(Box<RowDecoder>),
StreamDecoder {
decoder: Box<StreamDecoder>,
gainmap_reader: Box<ScanlineReader<'a>>,
},
}
#[cfg(feature = "ultrahdr")]
impl<'a> UltraHdrReader<'a> {
pub(crate) fn new(
data: &'a [u8],
config: UltraHdrReaderConfig,
base_reader: ScanlineReader<'a>,
extras: Option<DecodedExtras>,
gainmap_range: Option<(usize, usize)>,
metadata: Option<GainMapMetadata>,
) -> Result<Self> {
let is_ultrahdr = metadata.is_some() && gainmap_range.is_some();
let mut reader = Self {
config,
base_reader,
data,
is_ultrahdr,
metadata,
extras,
hdr_state: None,
gainmap_range,
};
if reader.needs_hdr_processing() && reader.is_ultrahdr {
reader.init_hdr_state()?;
}
Ok(reader)
}
fn needs_hdr_processing(&self) -> bool {
matches!(
self.config.mode,
UltraHdrMode::Hdr | UltraHdrMode::SdrAndHdr
)
}
fn init_hdr_state(&mut self) -> Result<()> {
let metadata = self.metadata.as_ref().ok_or_else(|| {
Error::decode_error("Missing gain map metadata for HDR decode".to_string())
})?;
let (gm_start, gm_end) = self.gainmap_range.ok_or_else(|| {
Error::decode_error("Missing gain map data for HDR decode".to_string())
})?;
let gainmap_data = &self.data[gm_start..gm_end];
match self.config.memory_strategy {
GainMapMemory::Full => {
let gainmap = decode_gainmap_jpeg(gainmap_data)?;
let width = self.base_reader.width();
let height = self.base_reader.height();
let row_decoder = RowDecoder::new(
gainmap,
metadata.clone(),
width,
height,
self.config.display_boost,
ColorGamut::Bt709,
)
.map_err(|e| Error::decode_error(e.to_string()))?;
self.hdr_state = Some(HdrDecoderState::RowDecoder(Box::new(row_decoder)));
}
GainMapMemory::Streaming => {
let gm_info = crate::decode::Decoder::new()
.read_info(gainmap_data)
.map_err(|e| {
Error::decode_error(format!("Failed to read gainmap info: {}", e))
})?;
let gm_width = gm_info.dimensions.width;
let gm_height = gm_info.dimensions.height;
let gm_channels = if gm_info.num_components == 1 { 1 } else { 3 };
let sdr_width = self.base_reader.width();
let sdr_height = self.base_reader.height();
let stream_decoder = StreamDecoder::new(
metadata.clone(),
sdr_width,
sdr_height,
gm_width,
gm_height,
gm_channels,
self.config.display_boost,
ColorGamut::Bt709,
)
.map_err(|e| Error::decode_error(e.to_string()))?;
let gm_reader = crate::decode::Decoder::new()
.scanline_reader(gainmap_data)
.map_err(|e| {
Error::decode_error(format!("Failed to create gainmap reader: {}", e))
})?;
self.hdr_state = Some(HdrDecoderState::StreamDecoder {
decoder: Box::new(stream_decoder),
gainmap_reader: Box::new(gm_reader),
});
}
}
Ok(())
}
#[inline]
pub fn is_ultrahdr(&self) -> bool {
self.is_ultrahdr
}
pub fn metadata(&self) -> Option<&GainMapMetadata> {
self.metadata.as_ref()
}
#[inline]
pub fn dimensions(&self) -> Dimensions {
Dimensions {
width: self.base_reader.width(),
height: self.base_reader.height(),
}
}
#[inline]
pub fn current_row(&self) -> usize {
self.base_reader.current_row()
}
#[inline]
pub fn is_finished(&self) -> bool {
self.base_reader.is_finished()
}
pub fn read_rows(
&mut self,
rows: usize,
sdr_output: Option<&mut [u8]>,
hdr_output: Option<&mut [f32]>,
gainmap_output: Option<&mut [u8]>,
) -> Result<usize> {
let height = self.base_reader.height() as usize;
let remaining = height - self.current_row();
let actual_rows = rows.min(remaining);
if actual_rows == 0 {
return Ok(0);
}
match self.config.mode {
UltraHdrMode::SdrOnly => self.read_sdr_only(actual_rows, sdr_output),
UltraHdrMode::Hdr => self.read_hdr_only(actual_rows, hdr_output),
UltraHdrMode::SdrAndHdr => self.read_sdr_and_hdr(actual_rows, sdr_output, hdr_output),
UltraHdrMode::SdrAndGainMap => {
self.read_sdr_and_gainmap(actual_rows, sdr_output, gainmap_output)
}
}
}
fn read_sdr_only(&mut self, rows: usize, sdr_output: Option<&mut [u8]>) -> Result<usize> {
let Some(output) = sdr_output else {
return Err(Error::internal(
"SDR output buffer required for SdrOnly mode",
));
};
let width = self.base_reader.width() as usize;
let stride = width * 3;
let output_ref = imgref::ImgRefMut::new(output, stride, rows);
self.base_reader.read_rows_rgb8(output_ref)
}
fn read_hdr_only(&mut self, rows: usize, hdr_output: Option<&mut [f32]>) -> Result<usize> {
let Some(output) = hdr_output else {
return Err(Error::internal("HDR output buffer required for Hdr mode"));
};
let width = self.base_reader.width() as usize;
let sdr_stride = width * 3;
let mut sdr_buf = vec![0u8; sdr_stride * rows];
let sdr_ref = imgref::ImgRefMut::new(&mut sdr_buf, sdr_stride, rows);
let actual_rows = self.base_reader.read_rows_rgb8(sdr_ref)?;
if actual_rows == 0 {
return Ok(0);
}
self.apply_hdr_reconstruction(&sdr_buf[..sdr_stride * actual_rows], actual_rows, output)?;
Ok(actual_rows)
}
fn read_sdr_and_hdr(
&mut self,
rows: usize,
sdr_output: Option<&mut [u8]>,
hdr_output: Option<&mut [f32]>,
) -> Result<usize> {
let Some(sdr_out) = sdr_output else {
return Err(Error::internal(
"SDR output buffer required for SdrAndHdr mode",
));
};
let Some(hdr_out) = hdr_output else {
return Err(Error::internal(
"HDR output buffer required for SdrAndHdr mode",
));
};
let width = self.base_reader.width() as usize;
let sdr_stride = width * 3;
let sdr_ref = imgref::ImgRefMut::new(sdr_out, sdr_stride, rows);
let actual_rows = self.base_reader.read_rows_rgb8(sdr_ref)?;
if actual_rows == 0 {
return Ok(0);
}
self.apply_hdr_reconstruction(&sdr_out[..sdr_stride * actual_rows], actual_rows, hdr_out)?;
Ok(actual_rows)
}
fn read_sdr_and_gainmap(
&mut self,
rows: usize,
sdr_output: Option<&mut [u8]>,
_gainmap_output: Option<&mut [u8]>,
) -> Result<usize> {
let Some(sdr_out) = sdr_output else {
return Err(Error::internal(
"SDR output buffer required for SdrAndGainMap mode",
));
};
let width = self.base_reader.width() as usize;
let sdr_stride = width * 3;
let sdr_ref = imgref::ImgRefMut::new(sdr_out, sdr_stride, rows);
let actual_rows = self.base_reader.read_rows_rgb8(sdr_ref)?;
Ok(actual_rows)
}
fn apply_hdr_reconstruction(
&mut self,
sdr_data: &[u8],
rows: usize,
hdr_output: &mut [f32],
) -> Result<()> {
let Some(ref mut hdr_state) = self.hdr_state else {
self.sdr_to_linear_fallback(sdr_data, rows, hdr_output);
return Ok(());
};
let width = self.base_reader.width() as usize;
let sdr_linear = srgb_u8_to_linear_f32(sdr_data, width, rows);
match hdr_state {
HdrDecoderState::RowDecoder(decoder) => {
let hdr_floats = decoder
.process_rows(&sdr_linear, rows as u32)
.map_err(|e| Error::decode_error(e.to_string()))?;
let copy_len = hdr_output.len().min(hdr_floats.len());
hdr_output[..copy_len].copy_from_slice(&hdr_floats[..copy_len]);
}
HdrDecoderState::StreamDecoder {
decoder,
gainmap_reader,
..
} => {
while !decoder.can_process(rows as u32) {
let gm_width = gainmap_reader.width() as usize;
let gm_stride = gm_width * 3;
let mut gm_row = vec![0u8; gm_stride];
let gm_ref = imgref::ImgRefMut::new(&mut gm_row, gm_stride, 1);
let gm_rows_read = gainmap_reader.read_rows_rgb8(gm_ref)?;
if gm_rows_read == 0 {
break;
}
decoder
.push_gainmap_row(&gm_row)
.map_err(|e| Error::decode_error(e.to_string()))?;
}
if decoder.can_process(rows as u32) {
let hdr_floats = decoder
.process_sdr_rows(&sdr_linear, rows as u32)
.map_err(|e| Error::decode_error(e.to_string()))?;
let copy_len = hdr_output.len().min(hdr_floats.len());
hdr_output[..copy_len].copy_from_slice(&hdr_floats[..copy_len]);
} else {
self.sdr_to_linear_fallback(sdr_data, rows, hdr_output);
}
}
}
Ok(())
}
fn sdr_to_linear_fallback(&self, sdr_data: &[u8], rows: usize, hdr_output: &mut [f32]) {
let width = self.base_reader.width() as usize;
for row in 0..rows {
for x in 0..width {
let sdr_idx = (row * width + x) * 3;
let hdr_idx = (row * width + x) * 4;
if sdr_idx + 2 < sdr_data.len() && hdr_idx + 3 < hdr_output.len() {
let r = srgb_to_linear(sdr_data[sdr_idx]);
let g = srgb_to_linear(sdr_data[sdr_idx + 1]);
let b = srgb_to_linear(sdr_data[sdr_idx + 2]);
hdr_output[hdr_idx] = r;
hdr_output[hdr_idx + 1] = g;
hdr_output[hdr_idx + 2] = b;
hdr_output[hdr_idx + 3] = 1.0;
}
}
}
}
pub fn take_extras(&mut self) -> Option<DecodedExtras> {
self.extras.take()
}
pub fn gainmap_jpeg(&self) -> Option<&'a [u8]> {
self.gainmap_range
.map(|(start, end)| &self.data[start..end])
}
pub fn take_gainmap_data(&mut self) -> Option<Vec<u8>> {
self.gainmap_range
.take()
.map(|(start, end)| self.data[start..end].to_vec())
}
}
#[inline]
fn srgb_to_linear(srgb: u8) -> f32 {
let s = srgb as f32 / 255.0;
if s <= 0.04045 {
s / 12.92
} else {
((s + 0.055) / 1.055).powf(2.4)
}
}
#[cfg(feature = "ultrahdr")]
fn srgb_u8_to_linear_f32(srgb_data: &[u8], width: usize, rows: usize) -> Vec<f32> {
let pixel_count = width * rows;
let mut linear = Vec::with_capacity(pixel_count * 3);
for pixel in srgb_data[..pixel_count * 3].chunks_exact(3) {
linear.push(srgb_to_linear(pixel[0]));
linear.push(srgb_to_linear(pixel[1]));
linear.push(srgb_to_linear(pixel[2]));
}
linear
}
#[cfg(feature = "ultrahdr")]
fn decode_gainmap_jpeg(jpeg_data: &[u8]) -> Result<GainMap> {
let decoded = crate::decode::Decoder::new().decode(jpeg_data, enough::Unstoppable)?;
let width = decoded.width();
let height = decoded.height();
let pixels = decoded.pixels_u8().unwrap().to_vec();
let channels = if is_grayscale_content(&pixels) { 1 } else { 3 };
let data = if channels == 1 {
pixels.chunks_exact(3).map(|p| p[0]).collect()
} else {
pixels
};
Ok(GainMap {
width,
height,
channels,
data,
})
}
#[cfg(feature = "ultrahdr")]
fn is_grayscale_content(pixels: &[u8]) -> bool {
pixels
.chunks_exact(3)
.take(100) .all(|p| p[0] == p[1] && p[1] == p[2])
}
#[cfg(all(test, feature = "ultrahdr"))]
mod tests {
use super::*;
use crate::decode::Decoder;
#[test]
fn test_config_builder() {
let config = UltraHdrReaderConfig::new()
.mode(UltraHdrMode::SdrAndHdr)
.display_boost(4.0)
.memory_strategy(GainMapMemory::Streaming)
.preserve_metadata(true);
assert_eq!(config.mode, UltraHdrMode::SdrAndHdr);
assert_eq!(config.display_boost, 4.0);
assert_eq!(config.memory_strategy, GainMapMemory::Streaming);
assert!(config.preserve_metadata);
}
#[test]
fn test_preset_configs() {
let sdr = UltraHdrReaderConfig::sdr_only();
assert_eq!(sdr.mode, UltraHdrMode::SdrOnly);
let hdr = UltraHdrReaderConfig::hdr_default();
assert_eq!(hdr.mode, UltraHdrMode::Hdr);
assert_eq!(hdr.display_boost, 4.0);
let edit = UltraHdrReaderConfig::editing();
assert_eq!(edit.mode, UltraHdrMode::SdrAndGainMap);
assert!(edit.preserve_metadata);
}
fn ultrahdr_test_path() -> std::path::PathBuf {
std::env::var("ULTRAHDR_TEST_IMAGE")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("/mnt/v/gen-dress.jpg"))
}
#[test]
#[ignore = "requires UltraHDR test image (set ULTRAHDR_TEST_IMAGE)"]
fn test_real_ultrahdr_sdr_decode() {
let path = ultrahdr_test_path();
if !path.exists() {
return;
}
let data = std::fs::read(path).expect("failed to read test file");
let config = UltraHdrReaderConfig::sdr_only();
let mut reader = Decoder::new()
.ultrahdr_reader(&data, config)
.expect("failed to create reader");
assert!(reader.is_ultrahdr());
let dims = reader.dimensions();
assert!(dims.width > 0);
assert!(dims.height > 0);
let row_size = dims.width as usize * 3;
let mut sdr_buf = vec![0u8; row_size * 16];
let mut total_rows = 0;
while !reader.is_finished() {
let rows = reader
.read_rows(16, Some(&mut sdr_buf), None, None)
.expect("failed to read rows");
total_rows += rows;
}
assert_eq!(total_rows, dims.height as usize);
}
#[test]
#[ignore = "requires UltraHDR test image (set ULTRAHDR_TEST_IMAGE)"]
fn test_real_ultrahdr_hdr_decode() {
let path = ultrahdr_test_path();
if !path.exists() {
return;
}
let data = std::fs::read(path).expect("failed to read test file");
let config = UltraHdrReaderConfig::hdr_default();
let mut reader = Decoder::new()
.ultrahdr_reader(&data, config)
.expect("failed to create reader");
assert!(reader.is_ultrahdr());
assert!(reader.metadata().is_some());
let dims = reader.dimensions();
let hdr_row_size = dims.width as usize * 4;
let mut hdr_buf = vec![0.0f32; hdr_row_size];
let mut total_rows = 0;
while !reader.is_finished() {
let rows = reader
.read_rows(1, None, Some(&mut hdr_buf), None)
.expect("failed to read rows");
if rows > 0 {
total_rows += rows;
for &v in &hdr_buf[..hdr_row_size] {
assert!(v.is_finite(), "HDR value should be finite");
}
}
}
assert_eq!(total_rows, dims.height as usize);
}
}