1use alloc::vec;
4
5use crate::color::gamut::rgb_to_luminance;
6
7use crate::color::transfer::{hlg_eotf, pq_eotf, srgb_eotf};
8use crate::types::TransferFunction;
9use crate::types::{
10 ColorPrimaries, GainMap, GainMapMetadata, PixelBuffer, PixelFormat, PixelSlice, Result,
11};
12use enough::Stop;
13
14#[derive(Debug, Clone)]
20pub struct GainMapConfig {
21 pub scale_factor: u8,
23 pub gamma: f32,
25 pub multi_channel: bool,
27 pub min_boost: f32,
29 pub max_boost: f32,
31 pub base_offset: f32,
33 pub alternate_offset: f32,
35 pub base_hdr_headroom: f32,
37 pub alternate_hdr_headroom: f32,
39}
40
41impl Default for GainMapConfig {
42 fn default() -> Self {
43 Self {
44 scale_factor: 4,
45 gamma: 1.0,
46 multi_channel: false,
47 min_boost: 1.0,
48 max_boost: 6.0, base_offset: 1.0 / 64.0,
50 alternate_offset: 1.0 / 64.0,
51 base_hdr_headroom: 1.0,
52 alternate_hdr_headroom: 6.0,
53 }
54 }
55}
56
57pub fn compute_gainmap(
65 hdr: &PixelBuffer,
66 sdr: &PixelBuffer,
67 config: &GainMapConfig,
68 stop: impl Stop,
69) -> Result<(GainMap, GainMapMetadata)> {
70 compute_gainmap_slice(hdr.as_slice(), sdr.as_slice(), config, stop)
71}
72
73pub fn compute_gainmap_slice(
75 hdr: PixelSlice<'_>,
76 sdr: PixelSlice<'_>,
77 config: &GainMapConfig,
78 stop: impl Stop,
79) -> Result<(GainMap, GainMapMetadata)> {
80 crate::types::validate_ultrahdr_slice(&hdr)?;
81 crate::types::validate_ultrahdr_slice(&sdr)?;
82
83 let hdr_w = hdr.width();
84 let hdr_h = hdr.rows();
85 let sdr_w = sdr.width();
86 let sdr_h = sdr.rows();
87
88 if hdr_w != sdr_w || hdr_h != sdr_h {
89 return Err(crate::types::Error::DimensionMismatch {
90 hdr_w,
91 hdr_h,
92 sdr_w,
93 sdr_h,
94 });
95 }
96
97 let scale = config.scale_factor.max(1) as u32;
98 let gm_width = hdr_w.div_ceil(scale);
99 let gm_height = hdr_h.div_ceil(scale);
100
101 let mut actual_min_boost = f32::MAX;
103 let mut actual_max_boost = f32::MIN;
104
105 let gainmap = if config.multi_channel {
107 compute_multichannel_gainmap(
108 &hdr,
109 &sdr,
110 gm_width,
111 gm_height,
112 scale,
113 config,
114 &mut actual_min_boost,
115 &mut actual_max_boost,
116 &stop,
117 )?
118 } else {
119 compute_luminance_gainmap(
120 &hdr,
121 &sdr,
122 gm_width,
123 gm_height,
124 scale,
125 config,
126 &mut actual_min_boost,
127 &mut actual_max_boost,
128 &stop,
129 )?
130 };
131
132 actual_min_boost = actual_min_boost.max(config.min_boost);
134 actual_max_boost = actual_max_boost.min(config.max_boost);
135
136 let metadata = crate::types::metadata_from_arrays(
138 [(actual_min_boost as f64).log2(); 3],
139 [(actual_max_boost as f64).log2(); 3],
140 [config.gamma as f64; 3],
141 [config.base_offset as f64; 3],
142 [config.alternate_offset as f64; 3],
143 (config.base_hdr_headroom as f64).log2(),
144 (config.alternate_hdr_headroom.max(actual_max_boost) as f64).log2(),
145 true,
146 false,
147 );
148
149 Ok((gainmap, metadata))
150}
151
152#[allow(clippy::too_many_arguments)]
154fn compute_luminance_gainmap(
155 hdr: &PixelSlice<'_>,
156 sdr: &PixelSlice<'_>,
157 gm_width: u32,
158 gm_height: u32,
159 scale: u32,
160 config: &GainMapConfig,
161 actual_min_boost: &mut f32,
162 actual_max_boost: &mut f32,
163 stop: &impl Stop,
164) -> Result<GainMap> {
165 let mut gainmap = GainMap::new(gm_width, gm_height)?;
166 let hdr_w = hdr.width();
167 let hdr_h = hdr.rows();
168 let hdr_gamut = hdr.descriptor().primaries;
169 let sdr_gamut = sdr.descriptor().primaries;
170
171 let row_len = gm_width as usize * 3;
175 let mut hdr_row_rgb = vec![0.0f32; row_len];
176 let mut sdr_row_rgb = vec![0.0f32; row_len];
177 let mut min_max = (*actual_min_boost, *actual_max_boost);
178
179 for gy in 0..gm_height {
180 stop.check()?;
182
183 let y = (gy * scale + scale / 2).min(hdr_h - 1);
185 for gx in 0..gm_width {
186 let x = (gx * scale + scale / 2).min(hdr_w - 1);
187 let hdr_rgb = get_linear_rgb(hdr, x, y);
188 let sdr_rgb = get_linear_rgb(sdr, x, y);
189 let off = gx as usize * 3;
190 hdr_row_rgb[off] = hdr_rgb[0];
191 hdr_row_rgb[off + 1] = hdr_rgb[1];
192 hdr_row_rgb[off + 2] = hdr_rgb[2];
193 sdr_row_rgb[off] = sdr_rgb[0];
194 sdr_row_rgb[off + 1] = sdr_rgb[1];
195 sdr_row_rgb[off + 2] = sdr_rgb[2];
196 }
197
198 let row_start = (gy * gm_width) as usize;
199 let row_end = row_start + gm_width as usize;
200 compute_gain_row(
201 &hdr_row_rgb,
202 &sdr_row_rgb,
203 3,
204 hdr_gamut,
205 sdr_gamut,
206 &mut gainmap.data[row_start..row_end],
207 config,
208 &mut min_max,
209 );
210 }
211
212 *actual_min_boost = min_max.0;
213 *actual_max_boost = min_max.1;
214 Ok(gainmap)
215}
216
217#[allow(clippy::too_many_arguments)]
238pub fn compute_gain_row(
239 hdr_row: &[f32],
240 sdr_row: &[f32],
241 channels: u8,
242 hdr_primaries: ColorPrimaries,
243 sdr_primaries: ColorPrimaries,
244 gainmap_byte_out: &mut [u8],
245 config: &GainMapConfig,
246 observed_min_max: &mut (f32, f32),
247) {
248 debug_assert!(channels == 3 || channels == 4);
249 let chan = channels as usize;
250 debug_assert_eq!(hdr_row.len(), sdr_row.len());
251 debug_assert_eq!(gainmap_byte_out.len(), hdr_row.len() / chan);
252
253 let log_min = config.min_boost.ln();
254 let log_range = config.max_boost.ln() - log_min;
255
256 for (i, byte_out) in gainmap_byte_out.iter_mut().enumerate() {
257 let off = i * chan;
258 let hdr_rgb = [hdr_row[off], hdr_row[off + 1], hdr_row[off + 2]];
259 let sdr_rgb = [sdr_row[off], sdr_row[off + 1], sdr_row[off + 2]];
260 let hdr_lum = rgb_to_luminance(hdr_rgb, hdr_primaries);
261 let sdr_lum = rgb_to_luminance(sdr_rgb, sdr_primaries);
262 *byte_out = compute_and_encode_gain(
263 hdr_lum,
264 sdr_lum,
265 config,
266 log_min,
267 log_range,
268 &mut observed_min_max.0,
269 &mut observed_min_max.1,
270 );
271 }
272}
273
274pub(super) fn compute_and_encode_gain(
282 hdr: f32,
283 sdr: f32,
284 config: &GainMapConfig,
285 log_min: f32,
286 log_range: f32,
287 actual_min_boost: &mut f32,
288 actual_max_boost: &mut f32,
289) -> u8 {
290 let gain = (hdr + config.alternate_offset) / (sdr + config.base_offset).max(0.001);
291 *actual_min_boost = actual_min_boost.min(gain);
292 *actual_max_boost = actual_max_boost.max(gain);
293 let gain_clamped = gain.clamp(config.min_boost, config.max_boost);
294 let log_gain = gain_clamped.ln();
295 let normalized = if log_range > 0.0 {
296 (log_gain - log_min) / log_range
297 } else {
298 0.5
299 };
300 let gamma_corrected = normalized.powf(config.gamma);
301 (gamma_corrected * 255.0).round().clamp(0.0, 255.0) as u8
302}
303
304#[allow(clippy::too_many_arguments)]
306fn compute_multichannel_gainmap(
307 hdr: &PixelSlice<'_>,
308 sdr: &PixelSlice<'_>,
309 gm_width: u32,
310 gm_height: u32,
311 scale: u32,
312 config: &GainMapConfig,
313 actual_min_boost: &mut f32,
314 actual_max_boost: &mut f32,
315 stop: &impl Stop,
316) -> Result<GainMap> {
317 let mut gainmap = GainMap::new_multichannel(gm_width, gm_height)?;
318 let hdr_w = hdr.width();
319 let hdr_h = hdr.rows();
320
321 let log_min = config.min_boost.ln();
322 let log_max = config.max_boost.ln();
323 let log_range = log_max - log_min;
324
325 for gy in 0..gm_height {
326 stop.check()?;
328
329 for gx in 0..gm_width {
330 let x = (gx * scale + scale / 2).min(hdr_w - 1);
331 let y = (gy * scale + scale / 2).min(hdr_h - 1);
332
333 let hdr_rgb = get_linear_rgb(hdr, x, y);
334 let sdr_rgb = get_linear_rgb(sdr, x, y);
335
336 for c in 0..3 {
337 let encoded = compute_and_encode_gain(
338 hdr_rgb[c],
339 sdr_rgb[c],
340 config,
341 log_min,
342 log_range,
343 actual_min_boost,
344 actual_max_boost,
345 );
346 let idx = (gy * gm_width + gx) as usize * 3 + c;
347 gainmap.data[idx] = encoded;
348 }
349 }
350 }
351
352 Ok(gainmap)
353}
354
355#[inline]
362fn apply_transfer_to_linear(rgb: [f32; 3], transfer: TransferFunction) -> [f32; 3] {
363 match transfer {
364 TransferFunction::Linear => rgb,
365 TransferFunction::Srgb => [srgb_eotf(rgb[0]), srgb_eotf(rgb[1]), srgb_eotf(rgb[2])],
366 TransferFunction::Pq => [pq_eotf(rgb[0]), pq_eotf(rgb[1]), pq_eotf(rgb[2])],
367 TransferFunction::Hlg => [
368 hlg_eotf(rgb[0], 1000.0) / 1000.0,
370 hlg_eotf(rgb[1], 1000.0) / 1000.0,
371 hlg_eotf(rgb[2], 1000.0) / 1000.0,
372 ],
373 _ => rgb,
374 }
375}
376
377fn get_linear_rgb(img: &PixelSlice<'_>, x: u32, y: u32) -> [f32; 3] {
382 let desc = img.descriptor();
383 let format = desc.pixel_format();
384 let transfer = desc.transfer();
385 let stride = img.stride();
386 let data = img.as_strided_bytes();
387 match format {
388 PixelFormat::Rgba8 | PixelFormat::Rgb8 => {
389 let bpp = if format == PixelFormat::Rgba8 { 4 } else { 3 };
390 let idx = y as usize * stride + x as usize * bpp;
391 let r = data[idx] as f32 / 255.0;
392 let g = data[idx + 1] as f32 / 255.0;
393 let b = data[idx + 2] as f32 / 255.0;
394
395 match transfer {
397 TransferFunction::Srgb => [srgb_eotf(r), srgb_eotf(g), srgb_eotf(b)],
398 TransferFunction::Linear => [r, g, b],
399 _ => [srgb_eotf(r), srgb_eotf(g), srgb_eotf(b)], }
401 }
402
403 PixelFormat::RgbaF32 => {
404 let idx = y as usize * stride + x as usize * 16;
405 let r = f32::from_le_bytes(data[idx..idx + 4].try_into().unwrap());
406 let g = f32::from_le_bytes(data[idx + 4..idx + 8].try_into().unwrap());
407 let b = f32::from_le_bytes(data[idx + 8..idx + 12].try_into().unwrap());
408 apply_transfer_to_linear([r, g, b], transfer)
409 }
410
411 PixelFormat::RgbaF16 | PixelFormat::RgbF16 => {
412 let bpp = if format == PixelFormat::RgbaF16 { 8 } else { 6 };
413 let idx = y as usize * stride + x as usize * bpp;
414 let r = half::f16::from_le_bytes([data[idx], data[idx + 1]]).to_f32();
415 let g = half::f16::from_le_bytes([data[idx + 2], data[idx + 3]]).to_f32();
416 let b = half::f16::from_le_bytes([data[idx + 4], data[idx + 5]]).to_f32();
417 apply_transfer_to_linear([r, g, b], transfer)
418 }
419
420 PixelFormat::Gray8 => {
421 let idx = y as usize * stride + x as usize;
422 let v = data[idx] as f32 / 255.0;
423 let linear = srgb_eotf(v);
424 [linear, linear, linear]
425 }
426 _ => [0.0, 0.0, 0.0],
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use crate::ColorPrimaries;
434 use crate::types::new_pixel_buffer;
435
436 #[test]
437 fn test_gainmap_config_default() {
438 let config = GainMapConfig::default();
439 assert_eq!(config.scale_factor, 4);
440 assert_eq!(config.gamma, 1.0);
441 assert!(!config.multi_channel);
442 }
443
444 #[test]
445 fn test_compute_gainmap_basic() {
446 let mut hdr = new_pixel_buffer(
448 8,
449 8,
450 PixelFormat::Rgba8,
451 ColorPrimaries::Bt709,
452 TransferFunction::Srgb,
453 )
454 .unwrap();
455 {
456 let mut slice = hdr.as_slice_mut();
457 let bytes = slice.as_strided_bytes_mut();
458 for i in 0..bytes.len() / 4 {
459 bytes[i * 4] = 180;
460 bytes[i * 4 + 1] = 180;
461 bytes[i * 4 + 2] = 180;
462 bytes[i * 4 + 3] = 255;
463 }
464 }
465
466 let mut sdr = new_pixel_buffer(
467 8,
468 8,
469 PixelFormat::Rgba8,
470 ColorPrimaries::Bt709,
471 TransferFunction::Srgb,
472 )
473 .unwrap();
474 {
475 let mut slice = sdr.as_slice_mut();
476 let bytes = slice.as_strided_bytes_mut();
477 for i in 0..bytes.len() / 4 {
478 bytes[i * 4] = 128;
479 bytes[i * 4 + 1] = 128;
480 bytes[i * 4 + 2] = 128;
481 bytes[i * 4 + 3] = 255;
482 }
483 }
484
485 let config = GainMapConfig {
486 scale_factor: 2,
487 ..Default::default()
488 };
489
490 let (gainmap, metadata) =
491 compute_gainmap(&hdr, &sdr, &config, enough::Unstoppable).unwrap();
492
493 assert_eq!(gainmap.width, 4);
495 assert_eq!(gainmap.height, 4);
496 assert_eq!(gainmap.channels, 1);
497
498 assert!(metadata.channels[0].max >= 1.0);
500 }
501
502 fn encode_gain_reference(sdr: f32, hdr: f32, min_boost: f32, max_boost: f32) -> u8 {
518 let offset = 1.0 / 64.0;
519 let gain = (hdr + offset) / (sdr + offset);
520 let gain_clamped = gain.clamp(min_boost, max_boost);
521 let log_min = min_boost.ln();
522 let log_max = max_boost.ln();
523 let log_range = log_max - log_min;
524 let normalized = (gain_clamped.ln() - log_min) / log_range;
525 (normalized * 255.0).round().clamp(0.0, 255.0) as u8
526 }
527
528 #[test]
532 fn test_gain_encoding_cpp_reference() {
533 let min_boost = 0.25_f32;
534 let max_boost = 4.0_f32;
535
536 let cases: &[(f32, f32, &str)] = &[
538 (0.5, 0.5, "equal SDR/HDR"),
540 (0.25, 1.0, "HDR 4x brighter"),
542 (1.0, 0.25, "HDR 4x darker"),
544 (0.0, 0.0, "both black"),
546 (0.0, 1.0, "SDR black HDR bright"),
548 (0.18, 0.36, "HDR ~2x mid-gray"),
550 (0.5, 0.75, "HDR 1.5x"),
552 ];
553
554 for &(sdr, hdr, desc) in cases {
555 let expected = encode_gain_reference(sdr, hdr, min_boost, max_boost);
556 let offset = 1.0 / 64.0;
558 let gain = (hdr + offset) / (sdr + offset);
559 let gain_clamped = gain.clamp(min_boost, max_boost);
560
561 if sdr > 0.01 && hdr > 0.01 {
563 if hdr > sdr * 1.5 {
564 assert!(
565 expected > 128,
566 "{}: hdr>sdr but encoded={} (gain={})",
567 desc,
568 expected,
569 gain
570 );
571 }
572 if hdr < sdr * 0.7 {
573 assert!(
574 expected < 128,
575 "{}: hdr<sdr but encoded={} (gain={})",
576 desc,
577 expected,
578 gain
579 );
580 }
581 }
582
583 eprintln!(
585 " {}: sdr={:.3}, hdr={:.3}, gain={:.4}, clamped={:.4}, encoded={}",
586 desc, sdr, hdr, gain, gain_clamped, expected
587 );
588 }
589 }
590
591 fn make_hdr_8x8(r: f32, g: f32, b: f32) -> PixelBuffer {
593 let w = 8u32;
594 let h = 8u32;
595 let pixel_count = (w * h) as usize;
596 let mut data = Vec::with_capacity(pixel_count * 16);
597 for _ in 0..pixel_count {
598 data.extend_from_slice(&r.to_le_bytes());
599 data.extend_from_slice(&g.to_le_bytes());
600 data.extend_from_slice(&b.to_le_bytes());
601 data.extend_from_slice(&1.0f32.to_le_bytes());
602 }
603 crate::types::pixel_buffer_from_vec(
604 data,
605 w,
606 h,
607 PixelFormat::RgbaF32,
608 ColorPrimaries::Bt709,
609 TransferFunction::Linear,
610 )
611 .unwrap()
612 }
613
614 fn make_sdr_8x8(r: u8, g: u8, b: u8) -> PixelBuffer {
616 let w = 8u32;
617 let h = 8u32;
618 let pixel_count = (w * h) as usize;
619 let mut data = vec![0u8; pixel_count * 4];
620 for i in 0..pixel_count {
621 data[i * 4] = r;
622 data[i * 4 + 1] = g;
623 data[i * 4 + 2] = b;
624 data[i * 4 + 3] = 255;
625 }
626 crate::types::pixel_buffer_from_vec(
627 data,
628 w,
629 h,
630 PixelFormat::Rgba8,
631 ColorPrimaries::Bt709,
632 TransferFunction::Srgb,
633 )
634 .unwrap()
635 }
636
637 #[test]
638 fn test_compute_gainmap_multichannel() {
639 let hdr = make_hdr_8x8(0.8, 0.5, 0.3);
640 let sdr = make_sdr_8x8(180, 128, 100);
641
642 let config = GainMapConfig {
643 multi_channel: true,
644 scale_factor: 1,
645 ..Default::default()
646 };
647
648 let (gainmap, _metadata) =
649 compute_gainmap(&hdr, &sdr, &config, enough::Unstoppable).unwrap();
650
651 assert_eq!(gainmap.channels, 3);
652 assert_eq!(
653 gainmap.data.len(),
654 (gainmap.width * gainmap.height) as usize * 3
655 );
656 }
657
658 #[test]
659 fn test_compute_gainmap_scale_factor_1() {
660 let hdr = make_hdr_8x8(0.5, 0.5, 0.5);
661 let sdr = make_sdr_8x8(186, 186, 186);
662
663 let config = GainMapConfig {
664 scale_factor: 1,
665 ..Default::default()
666 };
667
668 let (gainmap, _metadata) =
669 compute_gainmap(&hdr, &sdr, &config, enough::Unstoppable).unwrap();
670
671 assert_eq!(gainmap.width, 8);
672 assert_eq!(gainmap.height, 8);
673 }
674
675 #[test]
676 fn test_compute_gainmap_scale_factor_8() {
677 let hdr = make_hdr_8x8(0.5, 0.5, 0.5);
678 let sdr = make_sdr_8x8(186, 186, 186);
679
680 let config = GainMapConfig {
681 scale_factor: 8,
682 ..Default::default()
683 };
684
685 let (gainmap, _metadata) =
686 compute_gainmap(&hdr, &sdr, &config, enough::Unstoppable).unwrap();
687
688 assert_eq!(gainmap.width, 8u32.div_ceil(8));
690 assert_eq!(gainmap.height, 8u32.div_ceil(8));
691 }
692
693 #[test]
694 fn test_compute_gainmap_uniform_images() {
695 let hdr = make_hdr_8x8(0.5, 0.5, 0.5);
697 let sdr = make_sdr_8x8(186, 186, 186);
698
699 let config = GainMapConfig {
700 scale_factor: 1,
701 ..Default::default()
702 };
703
704 let (gainmap, _metadata) =
705 compute_gainmap(&hdr, &sdr, &config, enough::Unstoppable).unwrap();
706
707 let first = gainmap.data[0];
709 for &val in &gainmap.data {
710 assert!(
711 (val as i16 - first as i16).unsigned_abs() <= 1,
712 "non-uniform gainmap: first={}, got={}",
713 first,
714 val
715 );
716 }
717 }
718
719 #[test]
720 fn test_compute_gainmap_bright_hdr() {
721 let hdr = make_hdr_8x8(5.0, 5.0, 5.0);
723 let sdr = make_sdr_8x8(186, 186, 186);
724
725 let config = GainMapConfig {
726 scale_factor: 1,
727 max_boost: 12.0,
728 alternate_hdr_headroom: 12.0,
729 ..Default::default()
730 };
731
732 let (gainmap, _metadata) =
733 compute_gainmap(&hdr, &sdr, &config, enough::Unstoppable).unwrap();
734
735 let avg: f32 =
739 gainmap.data.iter().map(|&v| v as f32).sum::<f32>() / gainmap.data.len() as f32;
740 assert!(
741 avg > 128.0,
742 "bright HDR should produce high gainmap values, got average {}",
743 avg
744 );
745 }
746
747 #[test]
748 fn test_compute_gainmap_dimension_mismatch() {
749 let hdr = make_hdr_8x8(0.5, 0.5, 0.5);
750 let sdr = crate::types::pixel_buffer_from_vec(
752 vec![128u8; 4 * 4 * 4],
753 4,
754 4,
755 PixelFormat::Rgba8,
756 ColorPrimaries::Bt709,
757 TransferFunction::Srgb,
758 )
759 .unwrap();
760
761 let config = GainMapConfig::default();
762 let result = compute_gainmap(&hdr, &sdr, &config, enough::Unstoppable);
763 assert!(result.is_err());
764 assert!(matches!(
765 result.unwrap_err(),
766 crate::types::Error::DimensionMismatch { .. }
767 ));
768 }
769
770 #[test]
771 fn compute_gain_row_matches_compute_gainmap() {
772 let hdr = make_hdr_8x8(0.6, 0.4, 0.2);
778 let sdr = make_sdr_8x8(160, 110, 70);
779 let config = GainMapConfig {
780 scale_factor: 1,
781 ..Default::default()
782 };
783 let (gainmap_batch, _meta) =
784 compute_gainmap(&hdr, &sdr, &config, enough::Unstoppable).unwrap();
785
786 let hdr_slice = hdr.as_slice();
789 let sdr_slice = sdr.as_slice();
790 let w = hdr_slice.width() as usize;
791 let h = hdr_slice.rows() as usize;
792 let mut min_max = (f32::MAX, f32::MIN);
793 let mut row_bytes = vec![0u8; w];
794 let mut hdr_row_rgb = vec![0.0f32; w * 3];
795 let mut sdr_row_rgb = vec![0.0f32; w * 3];
796 for y in 0..h {
797 for x in 0..w {
798 let h_rgb = get_linear_rgb(&hdr_slice, x as u32, y as u32);
799 let s_rgb = get_linear_rgb(&sdr_slice, x as u32, y as u32);
800 hdr_row_rgb[x * 3..x * 3 + 3].copy_from_slice(&h_rgb);
801 sdr_row_rgb[x * 3..x * 3 + 3].copy_from_slice(&s_rgb);
802 }
803 compute_gain_row(
804 &hdr_row_rgb,
805 &sdr_row_rgb,
806 3,
807 hdr_slice.descriptor().primaries,
808 sdr_slice.descriptor().primaries,
809 &mut row_bytes,
810 &config,
811 &mut min_max,
812 );
813 let expected = &gainmap_batch.data[y * w..y * w + w];
815 assert_eq!(row_bytes, expected, "row {y} bytes diverged");
816 }
817 }
818
819 #[test]
820 fn test_compute_gainmap_cancellation() {
821 struct ImmediateCancel;
823
824 impl enough::Stop for ImmediateCancel {
825 fn check(&self) -> std::result::Result<(), enough::StopReason> {
826 Err(enough::StopReason::Cancelled)
827 }
828 }
829
830 let hdr = new_pixel_buffer(
832 8,
833 8,
834 PixelFormat::Rgba8,
835 ColorPrimaries::Bt709,
836 TransferFunction::Srgb,
837 )
838 .unwrap();
839 let sdr = new_pixel_buffer(
840 8,
841 8,
842 PixelFormat::Rgba8,
843 ColorPrimaries::Bt709,
844 TransferFunction::Srgb,
845 )
846 .unwrap();
847 let config = GainMapConfig::default();
848
849 let result = compute_gainmap(&hdr, &sdr, &config, ImmediateCancel);
851
852 assert!(matches!(
853 result,
854 Err(crate::Error::Stopped(enough::StopReason::Cancelled))
855 ));
856 }
857}