use crate::error::{Error, Result};
const JHGM_VERSION: u8 = 0x00;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GainMapBundle {
pub metadata: Vec<u8>,
pub color_encoding: Option<Vec<u8>>,
pub alt_icc_compressed: Option<Vec<u8>>,
pub gain_map_codestream: Vec<u8>,
}
impl GainMapBundle {
pub fn parse(data: &[u8]) -> Result<Self> {
let mut pos = 0;
if data.is_empty() {
return Err(Error::InvalidGainMap("empty jhgm box".into()));
}
let version = data[pos];
pos += 1;
if version != JHGM_VERSION {
return Err(Error::InvalidGainMap(format!(
"unsupported jhgm version: {version:#04x}, expected {JHGM_VERSION:#04x}"
)));
}
if pos + 2 > data.len() {
return Err(Error::InvalidGainMap(
"truncated: missing metadata size".into(),
));
}
let metadata_size = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
pos += 2;
if pos + metadata_size > data.len() {
return Err(Error::InvalidGainMap(format!(
"truncated: metadata size {metadata_size} exceeds remaining {} bytes",
data.len() - pos
)));
}
let metadata = data[pos..pos + metadata_size].to_vec();
pos += metadata_size;
if pos >= data.len() {
return Err(Error::InvalidGainMap(
"truncated: missing color_encoding_size".into(),
));
}
let color_encoding_size = data[pos] as usize;
pos += 1;
let color_encoding = if color_encoding_size == 0 {
None
} else {
if pos + color_encoding_size > data.len() {
return Err(Error::InvalidGainMap(format!(
"truncated: color_encoding size {color_encoding_size} exceeds remaining {} bytes",
data.len() - pos
)));
}
let ce = data[pos..pos + color_encoding_size].to_vec();
pos += color_encoding_size;
Some(ce)
};
if pos + 4 > data.len() {
return Err(Error::InvalidGainMap(
"truncated: missing alt_icc_size".into(),
));
}
let alt_icc_size =
u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
pos += 4;
let alt_icc_compressed = if alt_icc_size == 0 {
None
} else {
if pos + alt_icc_size > data.len() {
return Err(Error::InvalidGainMap(format!(
"truncated: alt_icc size {alt_icc_size} exceeds remaining {} bytes",
data.len() - pos
)));
}
let icc = data[pos..pos + alt_icc_size].to_vec();
pos += alt_icc_size;
Some(icc)
};
let gain_map_codestream = data[pos..].to_vec();
Ok(GainMapBundle {
metadata,
color_encoding,
alt_icc_compressed,
gain_map_codestream,
})
}
pub fn serialize(&self) -> Vec<u8> {
let metadata_size = self.metadata.len();
let color_encoding_size = self.color_encoding.as_ref().map_or(0, |v| v.len());
let alt_icc_size = self.alt_icc_compressed.as_ref().map_or(0, |v| v.len());
let total = 1
+ 2
+ metadata_size
+ 1
+ color_encoding_size
+ 4
+ alt_icc_size
+ self.gain_map_codestream.len();
let mut buf = Vec::with_capacity(total);
buf.push(JHGM_VERSION);
let meta_len = metadata_size.min(u16::MAX as usize) as u16;
buf.extend_from_slice(&meta_len.to_be_bytes());
buf.extend_from_slice(&self.metadata[..meta_len as usize]);
let ce_len = color_encoding_size.min(u8::MAX as usize) as u8;
buf.push(ce_len);
if let Some(ref ce) = self.color_encoding {
buf.extend_from_slice(&ce[..ce_len as usize]);
}
let icc_len = alt_icc_size.min(u32::MAX as usize) as u32;
buf.extend_from_slice(&icc_len.to_be_bytes());
if let Some(ref icc) = self.alt_icc_compressed {
buf.extend_from_slice(&icc[..icc_len as usize]);
}
buf.extend_from_slice(&self.gain_map_codestream);
buf
}
}
#[cfg(test)]
mod tests {
use super::*;
fn build_minimal_bundle(
metadata: &[u8],
color_encoding: Option<&[u8]>,
alt_icc: Option<&[u8]>,
gain_map: &[u8],
) -> Vec<u8> {
let mut buf = Vec::new();
buf.push(0x00);
buf.extend_from_slice(&(metadata.len() as u16).to_be_bytes());
buf.extend_from_slice(metadata);
match color_encoding {
None => buf.push(0),
Some(ce) => {
buf.push(ce.len() as u8);
buf.extend_from_slice(ce);
}
}
match alt_icc {
None => buf.extend_from_slice(&0u32.to_be_bytes()),
Some(icc) => {
buf.extend_from_slice(&(icc.len() as u32).to_be_bytes());
buf.extend_from_slice(icc);
}
}
buf.extend_from_slice(gain_map);
buf
}
#[test]
fn test_parse_minimal_bundle() {
let metadata = b"\x01\x02\x03";
let gain_map = b"\xff\x0a"; let data = build_minimal_bundle(metadata, None, None, gain_map);
let bundle = GainMapBundle::parse(&data).unwrap();
assert_eq!(bundle.metadata, metadata);
assert!(bundle.color_encoding.is_none());
assert!(bundle.alt_icc_compressed.is_none());
assert_eq!(bundle.gain_map_codestream, gain_map);
}
#[test]
fn test_parse_full_bundle() {
let metadata = b"ISO21496-1 test metadata blob";
let color_encoding = b"\xAA\xBB\xCC\xDD";
let alt_icc = b"brotli-compressed-icc-data-here";
let gain_map = b"\xff\x0a\x00\x01\x02\x03\x04\x05";
let data = build_minimal_bundle(metadata, Some(color_encoding), Some(alt_icc), gain_map);
let bundle = GainMapBundle::parse(&data).unwrap();
assert_eq!(bundle.metadata.as_slice(), metadata.as_slice());
assert_eq!(
bundle.color_encoding.as_deref(),
Some(color_encoding.as_slice())
);
assert_eq!(
bundle.alt_icc_compressed.as_deref(),
Some(alt_icc.as_slice())
);
assert_eq!(bundle.gain_map_codestream.as_slice(), gain_map.as_slice());
}
#[test]
fn test_roundtrip_minimal() {
let original = GainMapBundle {
metadata: vec![0x10, 0x20, 0x30],
color_encoding: None,
alt_icc_compressed: None,
gain_map_codestream: vec![0xFF, 0x0A, 0x00],
};
let serialized = original.serialize();
let parsed = GainMapBundle::parse(&serialized).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn test_roundtrip_full() {
let original = GainMapBundle {
metadata: vec![0x01; 100],
color_encoding: Some(vec![0xAA, 0xBB, 0xCC]),
alt_icc_compressed: Some(vec![0xDD; 256]),
gain_map_codestream: vec![0xFF, 0x0A, 0x00, 0x01, 0x02],
};
let serialized = original.serialize();
let parsed = GainMapBundle::parse(&serialized).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn test_roundtrip_empty_gain_map() {
let original = GainMapBundle {
metadata: vec![0x42],
color_encoding: None,
alt_icc_compressed: None,
gain_map_codestream: vec![],
};
let serialized = original.serialize();
let parsed = GainMapBundle::parse(&serialized).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn test_error_empty_data() {
let result = GainMapBundle::parse(&[]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("empty"), "unexpected error: {err}");
}
#[test]
fn test_error_wrong_version() {
let mut data = build_minimal_bundle(b"\x01", None, None, b"\xff");
data[0] = 0x01; let result = GainMapBundle::parse(&data);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("version"), "unexpected error: {err}");
}
#[test]
fn test_error_truncated_metadata_size() {
let result = GainMapBundle::parse(&[0x00]);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("truncated"), "unexpected error: {err}");
}
#[test]
fn test_error_metadata_exceeds_data() {
let mut data = vec![0x00]; data.extend_from_slice(&1000u16.to_be_bytes()); data.extend_from_slice(&[0x01, 0x02]); let result = GainMapBundle::parse(&data);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("truncated"), "unexpected error: {err}");
}
#[test]
fn test_error_truncated_color_encoding_size() {
let mut data = vec![0x00]; data.extend_from_slice(&0u16.to_be_bytes()); let result = GainMapBundle::parse(&data);
assert!(result.is_err());
}
#[test]
fn test_error_truncated_color_encoding() {
let mut data = vec![0x00]; data.extend_from_slice(&0u16.to_be_bytes()); data.push(10); data.extend_from_slice(&[0x01, 0x02, 0x03]); let result = GainMapBundle::parse(&data);
assert!(result.is_err());
}
#[test]
fn test_error_truncated_alt_icc_size() {
let mut data = vec![0x00]; data.extend_from_slice(&0u16.to_be_bytes()); data.push(0); data.push(0x01); let result = GainMapBundle::parse(&data);
assert!(result.is_err());
}
#[test]
fn test_error_alt_icc_exceeds_data() {
let mut data = vec![0x00]; data.extend_from_slice(&0u16.to_be_bytes()); data.push(0); data.extend_from_slice(&500u32.to_be_bytes()); data.extend_from_slice(&[0xAA; 10]); let result = GainMapBundle::parse(&data);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("truncated"), "unexpected error: {err}");
}
#[test]
fn test_large_metadata() {
let metadata = vec![0x42; 60_000];
let gain_map = vec![0xFF, 0x0A];
let original = GainMapBundle {
metadata,
color_encoding: None,
alt_icc_compressed: None,
gain_map_codestream: gain_map,
};
let serialized = original.serialize();
let parsed = GainMapBundle::parse(&serialized).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn test_large_alt_icc() {
let alt_icc = vec![0xDD; 100_000];
let original = GainMapBundle {
metadata: vec![0x01],
color_encoding: None,
alt_icc_compressed: Some(alt_icc),
gain_map_codestream: vec![0xFF, 0x0A],
};
let serialized = original.serialize();
let parsed = GainMapBundle::parse(&serialized).unwrap();
assert_eq!(original, parsed);
}
#[test]
fn test_box_level_roundtrip() {
let bundle = GainMapBundle {
metadata: vec![0x01, 0x02],
color_encoding: Some(vec![0xAA]),
alt_icc_compressed: Some(vec![0xBB, 0xCC]),
gain_map_codestream: vec![0xFF, 0x0A, 0x00],
};
let payload = bundle.serialize();
let box_size = (8 + payload.len()) as u32;
let mut jhgm_box = Vec::new();
jhgm_box.extend_from_slice(&box_size.to_be_bytes());
jhgm_box.extend_from_slice(b"jhgm");
jhgm_box.extend_from_slice(&payload);
assert_eq!(&jhgm_box[4..8], b"jhgm");
let extracted_payload = &jhgm_box[8..];
let parsed = GainMapBundle::parse(extracted_payload).unwrap();
assert_eq!(bundle, parsed);
}
}