1use alloc::format;
17use alloc::string::{String, ToString};
18use alloc::vec::Vec;
19
20use super::container::{ContainerItem, ItemSemantic, generate_container_directory};
21
22pub const HDRGM_NAMESPACE: &str = "http://ns.adobe.com/hdr-gain-map/1.0/";
24
25pub const CONTAINER_NAMESPACE: &str = "http://ns.google.com/photos/1.0/container/";
27
28pub const ITEM_NAMESPACE: &str = "http://ns.google.com/photos/1.0/container/item/";
30
31pub const MAX_XMP_LENGTH: usize = 16 * 1024 * 1024;
41
42#[derive(Debug)]
44#[non_exhaustive]
45pub enum XmpError {
46 LengthLimit {
48 got: usize,
50 max: usize,
52 },
53 NotUltraHdr,
55 Invalid(String),
57}
58
59impl core::fmt::Display for XmpError {
60 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61 match self {
62 Self::LengthLimit { got, max } => write!(f, "XMP length {got} exceeds maximum {max}"),
63 Self::NotUltraHdr => f.write_str(
64 "XMP is not an Ultra HDR packet (no hdrgm:Version and no hdrgm:GainMapMax)",
65 ),
66 Self::Invalid(message) => write!(f, "invalid gain-map XMP: {message}"),
67 }
68 }
69}
70impl core::error::Error for XmpError {}
71
72#[must_use]
101pub fn generate_xmp(metadata: &zencodec::GainMapParams, gainmap_length: usize) -> String {
102 let items = alloc::vec![
103 ContainerItem::primary("image/jpeg"),
104 ContainerItem::secondary(ItemSemantic::GainMap, "image/jpeg", gainmap_length),
105 ];
106 generate_xmp_with_items(metadata, &items)
107}
108
109#[must_use]
117pub fn generate_primary_xmp(gainmap_length: usize) -> String {
118 let items = alloc::vec![
119 ContainerItem::primary("image/jpeg"),
120 ContainerItem::secondary(ItemSemantic::GainMap, "image/jpeg", gainmap_length),
121 ];
122 generate_primary_xmp_with_items(&items)
123}
124
125#[must_use]
128pub fn generate_primary_xmp_with_items(items: &[ContainerItem]) -> String {
129 let container_dir = generate_container_directory(items);
130 format!(
131 r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
132<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core">
133 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
134 <rdf:Description rdf:about=""
135 xmlns:hdrgm="{HDRGM_NAMESPACE}"
136 xmlns:Container="{CONTAINER_NAMESPACE}"
137 xmlns:Item="{ITEM_NAMESPACE}"
138 hdrgm:Version="1.0">
139{container_dir}
140 </rdf:Description>
141 </rdf:RDF>
142</x:xmpmeta>
143<?xpacket end="w"?>"#
144 )
145}
146
147#[must_use]
154pub fn generate_gainmap_xmp(metadata: &zencodec::GainMapParams) -> String {
155 let ch = &metadata.channels;
156 let single = metadata.is_single_channel();
157
158 let gain_map_min = format_triple(&[ch[0].min, ch[1].min, ch[2].min], single);
159 let gain_map_max = format_triple(&[ch[0].max, ch[1].max, ch[2].max], single);
160 let gamma = format_triple(&[ch[0].gamma, ch[1].gamma, ch[2].gamma], single);
161 let offset_sdr = format_triple(
162 &[ch[0].base_offset, ch[1].base_offset, ch[2].base_offset],
163 single,
164 );
165 let offset_hdr = format_triple(
166 &[
167 ch[0].alternate_offset,
168 ch[1].alternate_offset,
169 ch[2].alternate_offset,
170 ],
171 single,
172 );
173 let hdr_capacity_min = metadata.base_hdr_headroom;
174 let hdr_capacity_max = metadata.alternate_hdr_headroom;
175
176 format!(
177 r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
178<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core">
179 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
180 <rdf:Description rdf:about=""
181 xmlns:hdrgm="{HDRGM_NAMESPACE}"
182 hdrgm:Version="1.0"
183 hdrgm:GainMapMin="{gain_map_min}"
184 hdrgm:GainMapMax="{gain_map_max}"
185 hdrgm:Gamma="{gamma}"
186 hdrgm:OffsetSDR="{offset_sdr}"
187 hdrgm:OffsetHDR="{offset_hdr}"
188 hdrgm:HDRCapacityMin="{hdr_capacity_min:.6}"
189 hdrgm:HDRCapacityMax="{hdr_capacity_max:.6}"
190 hdrgm:BaseRenditionIsHDR="False"/>
191 </rdf:RDF>
192</x:xmpmeta>
193<?xpacket end="w"?>"#
194 )
195}
196
197#[must_use]
201pub fn generate_xmp_with_items(
202 metadata: &zencodec::GainMapParams,
203 items: &[ContainerItem],
204) -> String {
205 let ch = &metadata.channels;
206 let single = metadata.is_single_channel();
207 let gain_map_min = format_triple(&[ch[0].min, ch[1].min, ch[2].min], single);
208 let gain_map_max = format_triple(&[ch[0].max, ch[1].max, ch[2].max], single);
209 let gamma = format_triple(&[ch[0].gamma, ch[1].gamma, ch[2].gamma], single);
210 let offset_sdr = format_triple(
211 &[ch[0].base_offset, ch[1].base_offset, ch[2].base_offset],
212 single,
213 );
214 let offset_hdr = format_triple(
215 &[
216 ch[0].alternate_offset,
217 ch[1].alternate_offset,
218 ch[2].alternate_offset,
219 ],
220 single,
221 );
222 let hdr_capacity_min = metadata.base_hdr_headroom;
223 let hdr_capacity_max = metadata.alternate_hdr_headroom;
224 let container_dir = generate_container_directory(items);
225 format!(
226 r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
227<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core">
228 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
229 <rdf:Description rdf:about=""
230 xmlns:hdrgm="{HDRGM_NAMESPACE}"
231 xmlns:Container="{CONTAINER_NAMESPACE}"
232 xmlns:Item="{ITEM_NAMESPACE}"
233 hdrgm:Version="1.0"
234 hdrgm:GainMapMin="{gain_map_min}"
235 hdrgm:GainMapMax="{gain_map_max}"
236 hdrgm:Gamma="{gamma}"
237 hdrgm:OffsetSDR="{offset_sdr}"
238 hdrgm:OffsetHDR="{offset_hdr}"
239 hdrgm:HDRCapacityMin="{hdr_capacity_min:.6}"
240 hdrgm:HDRCapacityMax="{hdr_capacity_max:.6}"
241 hdrgm:BaseRenditionIsHDR="False">
242{container_dir}
243 </rdf:Description>
244 </rdf:RDF>
245</x:xmpmeta>
246<?xpacket end="w"?>"#
247 )
248}
249
250fn format_triple(values: &[f64; 3], single_channel: bool) -> String {
251 if single_channel {
252 format!("{:.6}", values[0])
253 } else {
254 format!("{:.6}, {:.6}, {:.6}", values[0], values[1], values[2])
255 }
256}
257
258pub fn parse_xmp(xmp: &str) -> Result<(zencodec::GainMapParams, Option<usize>), XmpError> {
265 if xmp.len() > MAX_XMP_LENGTH {
266 return Err(XmpError::LengthLimit {
267 got: xmp.len(),
268 max: MAX_XMP_LENGTH,
269 });
270 }
271 let packet = crate::xmp::Packet::parse(xmp).map_err(|e| XmpError::Invalid(e.to_string()))?;
272 let property = |name| {
273 packet
274 .property(HDRGM_NAMESPACE, name)
275 .map_err(|e| XmpError::Invalid(e.to_string()))
276 };
277 let version = property("Version")?;
278 if version.is_none() && property("GainMapMax")?.is_none() {
279 return Err(XmpError::NotUltraHdr);
280 }
281 if version.is_some_and(|v| v != ["1.0"]) {
282 return Err(XmpError::Invalid("unsupported hdrgm version".into()));
283 }
284 if property("BaseRenditionIsHDR")?.is_some_and(|v| v != ["False"]) {
285 return Err(XmpError::Invalid(
286 "HDR-base XMP requires a supported inverse reconstruction path".into(),
287 ));
288 }
289 let mut metadata = zencodec::GainMapParams::default();
290 for (name, field) in [
291 ("GainMapMin", 0),
292 ("GainMapMax", 1),
293 ("Gamma", 2),
294 ("OffsetSDR", 3),
295 ("OffsetHDR", 4),
296 ] {
297 if let Some(values) = property(name)? {
298 let values = numeric_values(values, name)?;
299 let triple = match values.as_slice() {
300 [v] => [*v; 3],
301 [r, g, b] => [*r, *g, *b],
302 _ => {
303 return Err(XmpError::Invalid(format!(
304 "{name}: expected one or three values"
305 )));
306 }
307 };
308 for (ch, value) in metadata.channels.iter_mut().zip(triple) {
309 match field {
310 0 => ch.min = value,
311 1 => ch.max = value,
312 2 => ch.gamma = value,
313 3 => ch.base_offset = value,
314 _ => ch.alternate_offset = value,
315 }
316 }
317 }
318 }
319 for (name, slot) in [
320 ("HDRCapacityMin", &mut metadata.base_hdr_headroom),
321 ("HDRCapacityMax", &mut metadata.alternate_hdr_headroom),
322 ] {
323 if let Some(values) = property(name)? {
324 let values = numeric_values(values, name)?;
325 if values.len() != 1 {
326 return Err(XmpError::Invalid(format!("{name}: expected scalar")));
327 }
328 *slot = values[0];
329 }
330 }
331 metadata
332 .validate()
333 .map_err(|e| XmpError::Invalid(e.to_string()))?;
334 let items = checked_container_items(&packet)?;
335 let mut gainmaps = items.iter().filter(|i| i.semantic == ItemSemantic::GainMap);
336 let length = gainmaps.next().and_then(|i| i.length);
337 if gainmaps.next().is_some() {
338 return Err(XmpError::Invalid("multiple gain-map items".into()));
339 }
340 Ok((metadata, length))
341}
342fn numeric_values(values: Vec<String>, name: &str) -> Result<Vec<f64>, XmpError> {
343 values
344 .iter()
345 .flat_map(|v| v.split(','))
346 .map(|v| {
347 v.trim()
348 .parse::<f64>()
349 .ok()
350 .filter(|v| v.is_finite())
351 .ok_or_else(|| XmpError::Invalid(format!("{name}: invalid number")))
352 })
353 .collect()
354}
355
356#[must_use]
359pub fn parse_xmp_full(xmp: &str) -> (Option<zencodec::GainMapParams>, Vec<ContainerItem>) {
360 let Ok(packet) = crate::xmp::Packet::parse(xmp) else {
361 return (None, Vec::new());
362 };
363 (
364 parse_xmp(xmp).ok().map(|(m, _)| m),
365 checked_container_items(&packet).unwrap_or_default(),
366 )
367}
368
369pub(super) fn checked_container_items(
370 packet: &crate::xmp::Packet<'_>,
371) -> Result<Vec<ContainerItem>, XmpError> {
372 checked_items_in_namespace(packet, CONTAINER_NAMESPACE, ITEM_NAMESPACE)
373}
374
375pub(super) fn checked_items_in_namespace(
376 packet: &crate::xmp::Packet<'_>,
377 container_ns: &str,
378 item_ns: &str,
379) -> Result<Vec<ContainerItem>, XmpError> {
380 packet
381 .resource_sequence(container_ns, "Directory", container_ns, "Item")
382 .map_err(|e| XmpError::Invalid(e.to_string()))?
383 .into_iter()
384 .map(|fields| {
385 let value = |name: &str| {
386 fields
387 .iter()
388 .find(|f| f.namespace() == item_ns && f.name() == name)
389 .map(|f| f.value())
390 };
391 let semantic = value("Semantic")
392 .ok_or_else(|| XmpError::Invalid("missing item semantic".into()))?;
393 let mime =
394 value("Mime").ok_or_else(|| XmpError::Invalid("missing item MIME".into()))?;
395 let number = |name| {
396 value(name)
397 .map(|s| {
398 s.parse::<usize>()
399 .map_err(|_| XmpError::Invalid(format!("invalid item {name}")))
400 })
401 .transpose()
402 };
403 Ok(ContainerItem {
404 semantic: ItemSemantic::from_xmp(semantic),
405 mime: mime.into(),
406 length: number("Length")?,
407 padding: number("Padding")?,
408 })
409 })
410 .collect()
411}
412
413#[cfg(test)]
418mod tests {
419 use super::*;
420
421 fn test_metadata() -> zencodec::GainMapParams {
422 let mut m = zencodec::GainMapParams::default();
423 for i in 0..3 {
424 m.channels[i].min = 0.0;
425 m.channels[i].max = 2.0;
426 m.channels[i].gamma = 1.0;
427 m.channels[i].base_offset = 1.0 / 64.0;
428 m.channels[i].alternate_offset = 1.0 / 64.0;
429 }
430 m.base_hdr_headroom = 0.0;
431 m.alternate_hdr_headroom = 2.0;
432 m
433 }
434
435 fn multi_channel_metadata() -> zencodec::GainMapParams {
436 let mut m = test_metadata();
437 m.channels[0].min = 1.0;
438 m.channels[1].min = 2.0;
439 m.channels[2].min = 3.0;
440 m.channels[0].max = 4.0;
441 m.channels[1].max = 5.0;
442 m.channels[2].max = 6.0;
443 m
444 }
445
446 #[test]
447 fn generate_xmp_contains_version_and_length() {
448 let xmp = generate_xmp(&test_metadata(), 10_000);
449 assert!(xmp.contains("hdrgm:Version=\"1.0\""));
450 assert!(xmp.contains("hdrgm:GainMapMax"));
451 assert!(xmp.contains("Item:Length=\"10000\""));
452 assert!(xmp.contains("Item:Semantic=\"GainMap\""));
453 }
454
455 const XMP_FLOAT_TOL: f64 = 1e-5;
459
460 #[test]
461 fn parse_xmp_roundtrips_scalar_values() {
462 let original = test_metadata();
463 let xmp = generate_xmp(&original, 5_000);
464 let (parsed, length) = parse_xmp(&xmp).unwrap();
465 assert_eq!(length, Some(5_000));
466 for i in 0..3 {
468 assert!((parsed.channels[i].min - original.channels[i].min).abs() < XMP_FLOAT_TOL);
469 assert!((parsed.channels[i].max - original.channels[i].max).abs() < XMP_FLOAT_TOL);
470 assert!((parsed.channels[i].gamma - original.channels[i].gamma).abs() < XMP_FLOAT_TOL);
471 assert!(
472 (parsed.channels[i].base_offset - original.channels[i].base_offset).abs()
473 < XMP_FLOAT_TOL
474 );
475 assert!(
476 (parsed.channels[i].alternate_offset - original.channels[i].alternate_offset).abs()
477 < XMP_FLOAT_TOL
478 );
479 }
480 assert!((parsed.base_hdr_headroom - original.base_hdr_headroom).abs() < XMP_FLOAT_TOL);
481 assert!(
482 (parsed.alternate_hdr_headroom - original.alternate_hdr_headroom).abs() < XMP_FLOAT_TOL
483 );
484 }
485
486 #[test]
487 fn parse_xmp_roundtrips_multi_channel_values() {
488 let original = multi_channel_metadata();
489 let xmp = generate_xmp(&original, 5_000);
490 let (parsed, _) = parse_xmp(&xmp).unwrap();
491 for i in 0..3 {
494 assert!((parsed.channels[i].min - original.channels[i].min).abs() < XMP_FLOAT_TOL);
495 assert!((parsed.channels[i].max - original.channels[i].max).abs() < XMP_FLOAT_TOL);
496 }
497 assert!(
498 (parsed.channels[0].min - parsed.channels[1].min).abs() > 0.5,
499 "channels must remain distinct across the roundtrip"
500 );
501 }
502
503 #[test]
504 fn parse_xmp_rejects_non_ultrahdr_packet() {
505 let xmp = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
506<x:xmpmeta xmlns:x="adobe:ns:meta/">
507 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
508 <rdf:Description rdf:about=""
509 xmlns:dc="http://purl.org/dc/elements/1.1/"
510 dc:creator="SomeCamera"/>
511 </rdf:RDF>
512</x:xmpmeta>
513<?xpacket end="w"?>"#;
514 assert!(matches!(parse_xmp(xmp), Err(XmpError::NotUltraHdr)));
515 }
516
517 #[test]
522 fn max_xmp_length_is_16_mib() {
523 assert_eq!(MAX_XMP_LENGTH, 16 * 1024 * 1024);
524 }
525
526 #[test]
527 fn parse_xmp_rejects_oversized_input() {
528 let mut big = String::with_capacity(MAX_XMP_LENGTH + 100);
529 big.push_str("hdrgm:Version=\"1.0\" ");
530 while big.len() <= MAX_XMP_LENGTH {
531 big.push_str(" ");
532 }
533 assert!(matches!(parse_xmp(&big), Err(XmpError::LengthLimit { .. })));
534 }
535
536 #[test]
539 fn parse_xmp_accepts_1_mib_input() {
540 let mut big = String::with_capacity(1024 * 1024 + 100);
541 big.push_str(&generate_gainmap_xmp(&test_metadata()));
542 while big.len() <= 1024 * 1024 {
543 big.push_str(" ");
544 }
545 assert!(parse_xmp(&big).is_ok(), "1 MiB XMP must not trip the cap");
546 }
547
548 #[test]
553 fn parse_xmp_rdf_seq_element_form_seine_fixture() {
554 let xmp = r#"<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
555<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="Adobe XMP Core 7.0-c000 1.000000, 0000/00/00-00:00:00 ">
556 <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
557 <rdf:Description rdf:about=""
558 xmlns:hdrgm="http://ns.adobe.com/hdr-gain-map/1.0/"
559 hdrgm:Version="1.0"
560 hdrgm:BaseRenditionIsHDR="False"
561 hdrgm:OffsetSDR="0.015625"
562 hdrgm:OffsetHDR="0.015625"
563 hdrgm:HDRCapacityMin="0"
564 hdrgm:HDRCapacityMax="1.3">
565 <hdrgm:GainMapMin>
566 <rdf:Seq>
567 <rdf:li>-0.256907</rdf:li>
568 <rdf:li>-0.261365</rdf:li>
569 <rdf:li>-0.280284</rdf:li>
570 </rdf:Seq>
571 </hdrgm:GainMapMin>
572 <hdrgm:GainMapMax>
573 <rdf:Seq>
574 <rdf:li>1.277177</rdf:li>
575 <rdf:li>1.277203</rdf:li>
576 <rdf:li>1.277969</rdf:li>
577 </rdf:Seq>
578 </hdrgm:GainMapMax>
579 <hdrgm:Gamma>
580 <rdf:Seq>
581 <rdf:li>0.953784</rdf:li>
582 <rdf:li>0.941095</rdf:li>
583 <rdf:li>0.919422</rdf:li>
584 </rdf:Seq>
585 </hdrgm:Gamma>
586 </rdf:Description>
587 </rdf:RDF>
588</x:xmpmeta>
589<?xpacket end="w"?>"#;
590 let (parsed, length) = parse_xmp(xmp).expect("seine gain-map XMP must parse");
591 assert_eq!(length, None);
592
593 let expect_min = [-0.256907, -0.261365, -0.280284];
594 let expect_max = [1.277177, 1.277203, 1.277969];
595 let expect_gamma = [0.953784, 0.941095, 0.919422];
596 for i in 0..3 {
597 assert!(
598 (parsed.channels[i].min - expect_min[i]).abs() < XMP_FLOAT_TOL,
599 "ch[{i}].min: {}",
600 parsed.channels[i].min
601 );
602 assert!(
603 (parsed.channels[i].max - expect_max[i]).abs() < XMP_FLOAT_TOL,
604 "ch[{i}].max: {}",
605 parsed.channels[i].max
606 );
607 assert!(
608 (parsed.channels[i].gamma - expect_gamma[i]).abs() < XMP_FLOAT_TOL,
609 "ch[{i}].gamma: {}",
610 parsed.channels[i].gamma
611 );
612 assert!((parsed.channels[i].base_offset - 0.015625).abs() < XMP_FLOAT_TOL);
613 assert!((parsed.channels[i].alternate_offset - 0.015625).abs() < XMP_FLOAT_TOL);
614 }
615 assert!((parsed.base_hdr_headroom - 0.0).abs() < XMP_FLOAT_TOL);
616 assert!((parsed.alternate_hdr_headroom - 1.3).abs() < XMP_FLOAT_TOL);
617 parsed.validate().expect("parsed params must validate");
619 }
620
621 #[test]
622 fn malformed_numbers_refuse_instead_of_changing_rendering() {
623 let xml = generate_gainmap_xmp(&test_metadata())
624 .replace("hdrgm:Gamma=\"1.000000\"", "hdrgm:Gamma=\"oops\"");
625 assert!(matches!(parse_xmp(&xml), Err(XmpError::Invalid(_))));
626 }
627
628 #[test]
629 fn namespace_alias_and_gainmap_item_identity() {
630 let items = alloc::vec![
631 ContainerItem::primary("image/jpeg"),
632 ContainerItem::secondary(ItemSemantic::DepthMap, "image/jpeg", 9),
633 ContainerItem::secondary(ItemSemantic::GainMap, "image/jpeg", 123)
634 ];
635 let xml = generate_xmp_with_items(&test_metadata(), &items)
636 .replace("hdrgm:", "gain:")
637 .replace("xmlns:hdrgm=", "xmlns:gain=");
638 assert_eq!(parse_xmp(&xml).unwrap().1, Some(123));
639 let fake = xml.replace(HDRGM_NAMESPACE, "urn:spoof");
640 assert!(matches!(parse_xmp(&fake), Err(XmpError::NotUltraHdr)));
641 let duplicate = xml.replace(
642 "gain:Version=\"1.0\"",
643 "gain:Version=\"1.0\" gain:Version=\"1.0\"",
644 );
645 assert!(parse_xmp(&duplicate).is_err());
646 }
647
648 #[test]
649 fn parse_xmp_full_returns_items_even_without_numeric_metadata() {
650 let items = alloc::vec![
651 ContainerItem::primary("image/jpeg"),
652 ContainerItem::secondary(ItemSemantic::GainMap, "image/jpeg", 123),
653 ];
654 let xmp = generate_primary_xmp_with_items(&items);
655 let (meta, parsed) = parse_xmp_full(&xmp);
656 assert!(meta.is_some()); assert_eq!(parsed.len(), 2);
658 assert_eq!(parsed[1].length, Some(123));
659 }
660
661 #[test]
662 fn generate_gainmap_xmp_has_only_numeric_attrs() {
663 let xmp = generate_gainmap_xmp(&test_metadata());
664 assert!(xmp.contains("hdrgm:Version=\"1.0\""));
665 assert!(xmp.contains("hdrgm:GainMapMax"));
666 assert!(!xmp.contains("Container:Directory"));
668 }
669
670 #[test]
671 fn parse_xmp_no_panic_on_garbage() {
672 let _ = parse_xmp("");
673 let _ = parse_xmp("hdrgm:GainMapMax=\"not_a_number\"");
674 let garbage: String = (0..4096).map(|i| (i % 128) as u8 as char).collect();
675 let _ = parse_xmp(&garbage);
676 }
677
678 use proptest::prelude::*;
683
684 #[allow(clippy::field_reassign_with_default)]
689 fn arb_gainmap_params() -> impl Strategy<Value = zencodec::GainMapParams> {
690 let ch_arb = (
694 -100.0f64..100.0, -100.0f64..100.0, 0.01f64..10.0, 0.0f64..1.0, 0.0f64..1.0, )
700 .prop_map(|(min, max, gamma, bo, ao)| {
701 let mut c = zencodec::GainMapChannel::default();
702 c.min = min.min(max);
703 c.max = min.max(max);
704 c.gamma = gamma;
705 c.base_offset = bo;
706 c.alternate_offset = ao;
707 c
708 });
709
710 (
711 [ch_arb.clone(), ch_arb.clone(), ch_arb],
712 -10.0f64..10.0, 0.0f64..10.0, any::<bool>(), )
716 .prop_map(|(channels, base_hr, alt_hr, use_base)| {
717 let mut m = zencodec::GainMapParams::default();
718 m.channels = channels;
719 m.base_hdr_headroom = base_hr;
720 m.alternate_hdr_headroom = alt_hr;
721 m.use_base_color_space = use_base;
722 m
723 })
724 }
725
726 proptest! {
727 #[test]
732 fn gainmap_xmp_roundtrip(params in arb_gainmap_params(), length in 1usize..10_000_000) {
733 let xmp = generate_xmp(¶ms, length);
734 let (parsed, parsed_length) = parse_xmp(&xmp)
735 .expect("parse should succeed on self-generated XMP");
736 prop_assert_eq!(parsed_length, Some(length));
737
738 for i in 0..3 {
739 prop_assert!(
740 (parsed.channels[i].min - params.channels[i].min).abs() < XMP_FLOAT_TOL,
741 "ch[{i}].min: orig {} parsed {}", params.channels[i].min, parsed.channels[i].min
742 );
743 prop_assert!(
744 (parsed.channels[i].max - params.channels[i].max).abs() < XMP_FLOAT_TOL,
745 "ch[{i}].max",
746 );
747 prop_assert!(
748 (parsed.channels[i].gamma - params.channels[i].gamma).abs() < XMP_FLOAT_TOL,
749 "ch[{i}].gamma",
750 );
751 prop_assert!(
752 (parsed.channels[i].base_offset - params.channels[i].base_offset).abs()
753 < XMP_FLOAT_TOL,
754 "ch[{i}].base_offset",
755 );
756 prop_assert!(
757 (parsed.channels[i].alternate_offset - params.channels[i].alternate_offset).abs()
758 < XMP_FLOAT_TOL,
759 "ch[{i}].alternate_offset",
760 );
761 }
762 prop_assert!(
763 (parsed.base_hdr_headroom - params.base_hdr_headroom).abs() < XMP_FLOAT_TOL,
764 );
765 prop_assert!(
766 (parsed.alternate_hdr_headroom - params.alternate_hdr_headroom).abs()
767 < XMP_FLOAT_TOL,
768 );
769 }
770 }
771}