zenjxl_decoder/container/
gain_map.rs1use crate::error::{Error, Result};
17
18const JHGM_VERSION: u8 = 0x00;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct GainMapBundle {
28 pub metadata: Vec<u8>,
30 pub color_encoding: Option<Vec<u8>>,
32 pub alt_icc_compressed: Option<Vec<u8>>,
34 pub gain_map_codestream: Vec<u8>,
36}
37
38impl GainMapBundle {
39 pub fn parse(data: &[u8]) -> Result<Self> {
53 let mut pos = 0;
54
55 if data.is_empty() {
57 return Err(Error::InvalidGainMap("empty jhgm box".into()));
58 }
59 let version = data[pos];
60 pos += 1;
61 if version != JHGM_VERSION {
62 return Err(Error::InvalidGainMap(format!(
63 "unsupported jhgm version: {version:#04x}, expected {JHGM_VERSION:#04x}"
64 )));
65 }
66
67 if pos + 2 > data.len() {
69 return Err(Error::InvalidGainMap(
70 "truncated: missing metadata size".into(),
71 ));
72 }
73 let metadata_size = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
74 pos += 2;
75
76 if pos + metadata_size > data.len() {
77 return Err(Error::InvalidGainMap(format!(
78 "truncated: metadata size {metadata_size} exceeds remaining {} bytes",
79 data.len() - pos
80 )));
81 }
82 let metadata = data[pos..pos + metadata_size].to_vec();
83 pos += metadata_size;
84
85 if pos >= data.len() {
87 return Err(Error::InvalidGainMap(
88 "truncated: missing color_encoding_size".into(),
89 ));
90 }
91 let color_encoding_size = data[pos] as usize;
92 pos += 1;
93
94 let color_encoding = if color_encoding_size == 0 {
95 None
96 } else {
97 if pos + color_encoding_size > data.len() {
98 return Err(Error::InvalidGainMap(format!(
99 "truncated: color_encoding size {color_encoding_size} exceeds remaining {} bytes",
100 data.len() - pos
101 )));
102 }
103 let ce = data[pos..pos + color_encoding_size].to_vec();
104 pos += color_encoding_size;
105 Some(ce)
106 };
107
108 if pos + 4 > data.len() {
110 return Err(Error::InvalidGainMap(
111 "truncated: missing alt_icc_size".into(),
112 ));
113 }
114 let alt_icc_size =
115 u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]) as usize;
116 pos += 4;
117
118 let alt_icc_compressed = if alt_icc_size == 0 {
119 None
120 } else {
121 if pos + alt_icc_size > data.len() {
122 return Err(Error::InvalidGainMap(format!(
123 "truncated: alt_icc size {alt_icc_size} exceeds remaining {} bytes",
124 data.len() - pos
125 )));
126 }
127 let icc = data[pos..pos + alt_icc_size].to_vec();
128 pos += alt_icc_size;
129 Some(icc)
130 };
131
132 let gain_map_codestream = data[pos..].to_vec();
134
135 Ok(GainMapBundle {
136 metadata,
137 color_encoding,
138 alt_icc_compressed,
139 gain_map_codestream,
140 })
141 }
142
143 pub fn serialize(&self) -> Vec<u8> {
147 let metadata_size = self.metadata.len();
148 let color_encoding_size = self.color_encoding.as_ref().map_or(0, |v| v.len());
149 let alt_icc_size = self.alt_icc_compressed.as_ref().map_or(0, |v| v.len());
150
151 let total = 1
154 + 2
155 + metadata_size
156 + 1
157 + color_encoding_size
158 + 4
159 + alt_icc_size
160 + self.gain_map_codestream.len();
161 let mut buf = Vec::with_capacity(total);
162
163 buf.push(JHGM_VERSION);
165
166 let meta_len = metadata_size.min(u16::MAX as usize) as u16;
169 buf.extend_from_slice(&meta_len.to_be_bytes());
170 buf.extend_from_slice(&self.metadata[..meta_len as usize]);
171
172 let ce_len = color_encoding_size.min(u8::MAX as usize) as u8;
175 buf.push(ce_len);
176 if let Some(ref ce) = self.color_encoding {
177 buf.extend_from_slice(&ce[..ce_len as usize]);
178 }
179
180 let icc_len = alt_icc_size.min(u32::MAX as usize) as u32;
182 buf.extend_from_slice(&icc_len.to_be_bytes());
183 if let Some(ref icc) = self.alt_icc_compressed {
184 buf.extend_from_slice(&icc[..icc_len as usize]);
185 }
186
187 buf.extend_from_slice(&self.gain_map_codestream);
189
190 buf
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use super::*;
197
198 fn build_minimal_bundle(
200 metadata: &[u8],
201 color_encoding: Option<&[u8]>,
202 alt_icc: Option<&[u8]>,
203 gain_map: &[u8],
204 ) -> Vec<u8> {
205 let mut buf = Vec::new();
206 buf.push(0x00);
208 buf.extend_from_slice(&(metadata.len() as u16).to_be_bytes());
210 buf.extend_from_slice(metadata);
211 match color_encoding {
213 None => buf.push(0),
214 Some(ce) => {
215 buf.push(ce.len() as u8);
216 buf.extend_from_slice(ce);
217 }
218 }
219 match alt_icc {
221 None => buf.extend_from_slice(&0u32.to_be_bytes()),
222 Some(icc) => {
223 buf.extend_from_slice(&(icc.len() as u32).to_be_bytes());
224 buf.extend_from_slice(icc);
225 }
226 }
227 buf.extend_from_slice(gain_map);
229 buf
230 }
231
232 #[test]
233 fn test_parse_minimal_bundle() {
234 let metadata = b"\x01\x02\x03";
235 let gain_map = b"\xff\x0a"; let data = build_minimal_bundle(metadata, None, None, gain_map);
237
238 let bundle = GainMapBundle::parse(&data).unwrap();
239 assert_eq!(bundle.metadata, metadata);
240 assert!(bundle.color_encoding.is_none());
241 assert!(bundle.alt_icc_compressed.is_none());
242 assert_eq!(bundle.gain_map_codestream, gain_map);
243 }
244
245 #[test]
246 fn test_parse_full_bundle() {
247 let metadata = b"ISO21496-1 test metadata blob";
248 let color_encoding = b"\xAA\xBB\xCC\xDD";
249 let alt_icc = b"brotli-compressed-icc-data-here";
250 let gain_map = b"\xff\x0a\x00\x01\x02\x03\x04\x05";
251
252 let data = build_minimal_bundle(metadata, Some(color_encoding), Some(alt_icc), gain_map);
253
254 let bundle = GainMapBundle::parse(&data).unwrap();
255 assert_eq!(bundle.metadata.as_slice(), metadata.as_slice());
256 assert_eq!(
257 bundle.color_encoding.as_deref(),
258 Some(color_encoding.as_slice())
259 );
260 assert_eq!(
261 bundle.alt_icc_compressed.as_deref(),
262 Some(alt_icc.as_slice())
263 );
264 assert_eq!(bundle.gain_map_codestream.as_slice(), gain_map.as_slice());
265 }
266
267 #[test]
268 fn test_roundtrip_minimal() {
269 let original = GainMapBundle {
270 metadata: vec![0x10, 0x20, 0x30],
271 color_encoding: None,
272 alt_icc_compressed: None,
273 gain_map_codestream: vec![0xFF, 0x0A, 0x00],
274 };
275 let serialized = original.serialize();
276 let parsed = GainMapBundle::parse(&serialized).unwrap();
277 assert_eq!(original, parsed);
278 }
279
280 #[test]
281 fn test_roundtrip_full() {
282 let original = GainMapBundle {
283 metadata: vec![0x01; 100],
284 color_encoding: Some(vec![0xAA, 0xBB, 0xCC]),
285 alt_icc_compressed: Some(vec![0xDD; 256]),
286 gain_map_codestream: vec![0xFF, 0x0A, 0x00, 0x01, 0x02],
287 };
288 let serialized = original.serialize();
289 let parsed = GainMapBundle::parse(&serialized).unwrap();
290 assert_eq!(original, parsed);
291 }
292
293 #[test]
294 fn test_roundtrip_empty_gain_map() {
295 let original = GainMapBundle {
297 metadata: vec![0x42],
298 color_encoding: None,
299 alt_icc_compressed: None,
300 gain_map_codestream: vec![],
301 };
302 let serialized = original.serialize();
303 let parsed = GainMapBundle::parse(&serialized).unwrap();
304 assert_eq!(original, parsed);
305 }
306
307 #[test]
308 fn test_error_empty_data() {
309 let result = GainMapBundle::parse(&[]);
310 assert!(result.is_err());
311 let err = result.unwrap_err().to_string();
312 assert!(err.contains("empty"), "unexpected error: {err}");
313 }
314
315 #[test]
316 fn test_error_wrong_version() {
317 let mut data = build_minimal_bundle(b"\x01", None, None, b"\xff");
318 data[0] = 0x01; let result = GainMapBundle::parse(&data);
320 assert!(result.is_err());
321 let err = result.unwrap_err().to_string();
322 assert!(err.contains("version"), "unexpected error: {err}");
323 }
324
325 #[test]
326 fn test_error_truncated_metadata_size() {
327 let result = GainMapBundle::parse(&[0x00]);
329 assert!(result.is_err());
330 let err = result.unwrap_err().to_string();
331 assert!(err.contains("truncated"), "unexpected error: {err}");
332 }
333
334 #[test]
335 fn test_error_metadata_exceeds_data() {
336 let mut data = vec![0x00]; data.extend_from_slice(&1000u16.to_be_bytes()); data.extend_from_slice(&[0x01, 0x02]); let result = GainMapBundle::parse(&data);
341 assert!(result.is_err());
342 let err = result.unwrap_err().to_string();
343 assert!(err.contains("truncated"), "unexpected error: {err}");
344 }
345
346 #[test]
347 fn test_error_truncated_color_encoding_size() {
348 let mut data = vec![0x00]; data.extend_from_slice(&0u16.to_be_bytes()); let result = GainMapBundle::parse(&data);
353 assert!(result.is_err());
354 }
355
356 #[test]
357 fn test_error_truncated_color_encoding() {
358 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);
364 assert!(result.is_err());
365 }
366
367 #[test]
368 fn test_error_truncated_alt_icc_size() {
369 let mut data = vec![0x00]; data.extend_from_slice(&0u16.to_be_bytes()); data.push(0); data.push(0x01); let result = GainMapBundle::parse(&data);
376 assert!(result.is_err());
377 }
378
379 #[test]
380 fn test_error_alt_icc_exceeds_data() {
381 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);
387 assert!(result.is_err());
388 let err = result.unwrap_err().to_string();
389 assert!(err.contains("truncated"), "unexpected error: {err}");
390 }
391
392 #[test]
393 fn test_large_metadata() {
394 let metadata = vec![0x42; 60_000];
396 let gain_map = vec![0xFF, 0x0A];
397 let original = GainMapBundle {
398 metadata,
399 color_encoding: None,
400 alt_icc_compressed: None,
401 gain_map_codestream: gain_map,
402 };
403 let serialized = original.serialize();
404 let parsed = GainMapBundle::parse(&serialized).unwrap();
405 assert_eq!(original, parsed);
406 }
407
408 #[test]
409 fn test_large_alt_icc() {
410 let alt_icc = vec![0xDD; 100_000];
412 let original = GainMapBundle {
413 metadata: vec![0x01],
414 color_encoding: None,
415 alt_icc_compressed: Some(alt_icc),
416 gain_map_codestream: vec![0xFF, 0x0A],
417 };
418 let serialized = original.serialize();
419 let parsed = GainMapBundle::parse(&serialized).unwrap();
420 assert_eq!(original, parsed);
421 }
422
423 #[test]
426 fn test_box_level_roundtrip() {
427 let bundle = GainMapBundle {
428 metadata: vec![0x01, 0x02],
429 color_encoding: Some(vec![0xAA]),
430 alt_icc_compressed: Some(vec![0xBB, 0xCC]),
431 gain_map_codestream: vec![0xFF, 0x0A, 0x00],
432 };
433
434 let payload = bundle.serialize();
436
437 let box_size = (8 + payload.len()) as u32;
439 let mut jhgm_box = Vec::new();
440 jhgm_box.extend_from_slice(&box_size.to_be_bytes());
441 jhgm_box.extend_from_slice(b"jhgm");
442 jhgm_box.extend_from_slice(&payload);
443
444 assert_eq!(&jhgm_box[4..8], b"jhgm");
446
447 let extracted_payload = &jhgm_box[8..];
449 let parsed = GainMapBundle::parse(extracted_payload).unwrap();
450 assert_eq!(bundle, parsed);
451 }
452}