use alloc::format;
use alloc::vec::Vec;
use core::mem;
use crate::types::{Error, Fraction, GainMapMetadata, Result};
pub const ISO_VERSION: u8 = 0;
const FLAG_MULTI_CHANNEL: u8 = 0x01;
const FLAG_USE_BASE_CG: u8 = 0x02;
const FLAG_BACKWARD_DIR: u8 = 0x04;
pub fn serialize_iso21496(metadata: &GainMapMetadata) -> Vec<u8> {
let mut data = Vec::with_capacity(128);
data.push(ISO_VERSION);
let mut flags = 0u8;
if !metadata.is_single_channel() {
flags |= FLAG_MULTI_CHANNEL;
}
if metadata.use_base_color_space {
flags |= FLAG_USE_BASE_CG;
}
data.push(flags);
let channels = if flags & FLAG_MULTI_CHANNEL != 0 {
3
} else {
1
};
let base_headroom = Fraction::from_f32(metadata.hdr_capacity_min.log2());
write_fraction(&mut data, base_headroom);
let alt_headroom = Fraction::from_f32(metadata.hdr_capacity_max.log2());
write_fraction(&mut data, alt_headroom);
for i in 0..channels {
let min_val = Fraction::from_f32(metadata.min_content_boost[i].log2());
write_fraction(&mut data, min_val);
let max_val = Fraction::from_f32(metadata.max_content_boost[i].log2());
write_fraction(&mut data, max_val);
let gamma = Fraction::from_f32(metadata.gamma[i]);
write_fraction(&mut data, gamma);
let base_offset = Fraction::from_f32(metadata.offset_sdr[i]);
write_fraction(&mut data, base_offset);
let alt_offset = Fraction::from_f32(metadata.offset_hdr[i]);
write_fraction(&mut data, alt_offset);
}
data
}
pub fn deserialize_iso21496(data: &[u8]) -> Result<GainMapMetadata> {
if data.len() < 2 {
return Err(Error::InvalidMetadata("ISO metadata too short".into()));
}
let mut pos = 0;
let version = data[pos];
pos += 1;
if version > ISO_VERSION {
return Err(Error::InvalidMetadata(format!(
"Unsupported ISO version: {}",
version
)));
}
let flags = data[pos];
pos += 1;
let multi_channel = flags & FLAG_MULTI_CHANNEL != 0;
let use_base_cg = flags & FLAG_USE_BASE_CG != 0;
let backward_dir = flags & FLAG_BACKWARD_DIR != 0;
let channels = if multi_channel { 3 } else { 1 };
let min_size = 2 + 16 + channels * 40;
if data.len() < min_size {
return Err(Error::InvalidMetadata("ISO metadata truncated".into()));
}
let (base_headroom, new_pos) = read_fraction(data, pos)?;
pos = new_pos;
let hdr_capacity_min = 2.0f32.powf(base_headroom.to_f32());
let (alt_headroom, new_pos) = read_fraction(data, pos)?;
pos = new_pos;
let hdr_capacity_max = 2.0f32.powf(alt_headroom.to_f32());
let mut metadata = GainMapMetadata {
hdr_capacity_min,
hdr_capacity_max,
use_base_color_space: use_base_cg,
..Default::default()
};
for i in 0..channels {
let idx = if multi_channel { i } else { 0 };
let (min_frac, new_pos) = read_fraction(data, pos)?;
pos = new_pos;
let min_val = 2.0f32.powf(min_frac.to_f32());
let (max_frac, new_pos) = read_fraction(data, pos)?;
pos = new_pos;
let max_val = 2.0f32.powf(max_frac.to_f32());
let (gamma_frac, new_pos) = read_fraction(data, pos)?;
pos = new_pos;
let (base_offset_frac, new_pos) = read_fraction(data, pos)?;
pos = new_pos;
let (alt_offset_frac, new_pos) = read_fraction(data, pos)?;
pos = new_pos;
if multi_channel {
metadata.min_content_boost[idx] = min_val;
metadata.max_content_boost[idx] = max_val;
metadata.gamma[idx] = gamma_frac.to_f32();
metadata.offset_sdr[idx] = base_offset_frac.to_f32();
metadata.offset_hdr[idx] = alt_offset_frac.to_f32();
} else {
metadata.min_content_boost = [min_val; 3];
metadata.max_content_boost = [max_val; 3];
metadata.gamma = [gamma_frac.to_f32(); 3];
metadata.offset_sdr = [base_offset_frac.to_f32(); 3];
metadata.offset_hdr = [alt_offset_frac.to_f32(); 3];
}
}
if backward_dir {
mem::swap(&mut metadata.offset_sdr, &mut metadata.offset_hdr);
}
Ok(metadata)
}
fn write_fraction(buf: &mut Vec<u8>, frac: Fraction) {
buf.extend_from_slice(&frac.numerator.to_be_bytes());
buf.extend_from_slice(&frac.denominator.to_be_bytes());
}
fn read_fraction(data: &[u8], pos: usize) -> Result<(Fraction, usize)> {
if pos + 8 > data.len() {
return Err(Error::InvalidMetadata("Unexpected end of ISO data".into()));
}
let numerator = i32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
let denominator =
u32::from_be_bytes([data[pos + 4], data[pos + 5], data[pos + 6], data[pos + 7]]);
Ok((Fraction::new(numerator, denominator), pos + 8))
}
pub fn create_iso_app2_marker(iso_data: &[u8]) -> Vec<u8> {
let namespace = b"urn:iso:std:iso:ts:21496:-1\0";
let total_length = 2 + namespace.len() + iso_data.len();
let mut marker = Vec::with_capacity(2 + total_length);
marker.push(0xFF);
marker.push(0xE2); marker.push(((total_length >> 8) & 0xFF) as u8);
marker.push((total_length & 0xFF) as u8);
marker.extend_from_slice(namespace);
marker.extend_from_slice(iso_data);
marker
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_deserialize_single_channel() {
let original = GainMapMetadata {
min_content_boost: [1.0; 3],
max_content_boost: [4.0; 3],
gamma: [1.0; 3],
offset_sdr: [0.015625; 3],
offset_hdr: [0.015625; 3],
hdr_capacity_min: 1.0,
hdr_capacity_max: 4.0,
use_base_color_space: true,
};
let serialized = serialize_iso21496(&original);
let parsed = deserialize_iso21496(&serialized).unwrap();
assert!((parsed.max_content_boost[0] - 4.0).abs() < 0.01);
assert!((parsed.hdr_capacity_max - 4.0).abs() < 0.01);
assert!((parsed.gamma[0] - 1.0).abs() < 0.01);
assert!(parsed.use_base_color_space);
}
#[test]
fn test_fraction_roundtrip() {
let values = [0.0, 0.5, 1.0, 2.0, -1.0, 0.015625];
for &v in &values {
let frac = Fraction::from_f32(v);
let back = frac.to_f32();
assert!(
(v - back).abs() < 0.0001,
"Fraction roundtrip failed for {}: got {}",
v,
back
);
}
}
}