1use crate::err::ForgeError;
30use crate::gamma::trc_from_cicp;
31use crate::mappers::{
32 AcesToneMapper, AgxDefault, AgxGolden, AgxLook, AgxPunchy, AgxToneMapper, ClampToneMapper,
33 ExtendedReinhardToneMapper, FilmicToneMapper, Rec2408ToneMapper, ReinhardJodieToneMapper,
34 ReinhardToneMapper, ToneMap,
35};
36use crate::mlaf::mlaf;
37use crate::spline::{create_spline, SplineToneMapper};
38use crate::{m_clamp, ToneMappingMethod};
39use moxcms::{
40 adaption_matrix, filmlike_clip, CmsError, ColorProfile, InPlaceStage, Jzazbz, Matrix3f, Oklab,
41 Rgb, Yrg, WHITE_POINT_D50, WHITE_POINT_D65,
42};
43use num_traits::AsPrimitive;
44use std::fmt::Debug;
45
46#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
48pub enum GamutClipping {
49 #[default]
50 NoClip,
51 Clip,
52}
53
54#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
55pub enum MappingColorSpace {
56 Rgb(RgbToneMapperParameters),
67 YRgb(CommonToneMapperParameters),
82 Oklab(CommonToneMapperParameters),
93 Jzazbz(JzazbzToneMapperParameters),
105}
106
107#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
108pub struct JzazbzToneMapperParameters {
109 pub content_brightness: f32,
110 pub exposure: f32,
111 pub gamut_clipping: GamutClipping,
112}
113
114#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
115pub struct CommonToneMapperParameters {
116 pub exposure: f32,
117 pub gamut_clipping: GamutClipping,
118}
119
120#[derive(Debug, Copy, Clone, PartialOrd, PartialEq)]
121pub struct RgbToneMapperParameters {
122 pub exposure: f32,
123 pub gamut_clipping: GamutClipping,
124}
125
126impl Default for JzazbzToneMapperParameters {
127 fn default() -> Self {
128 Self {
129 content_brightness: 1000f32,
130 exposure: 1.0f32,
131 gamut_clipping: GamutClipping::Clip,
132 }
133 }
134}
135
136impl Default for CommonToneMapperParameters {
137 fn default() -> Self {
138 Self {
139 exposure: 1.0f32,
140 gamut_clipping: GamutClipping::Clip,
141 }
142 }
143}
144
145impl Default for RgbToneMapperParameters {
146 fn default() -> Self {
147 Self {
148 exposure: 1.0f32,
149 gamut_clipping: GamutClipping::default(),
150 }
151 }
152}
153
154pub type SyncToneMapper8Bit = dyn ToneMapper<u8> + Send + Sync;
155pub type SyncToneMapper16Bit = dyn ToneMapper<u16> + Send + Sync;
156
157type SyncToneMap = dyn ToneMap + Send + Sync;
158
159struct MatrixStage<const CN: usize> {
160 pub(crate) gamut_color_conversion: Matrix3f,
161}
162
163impl<const CN: usize> InPlaceStage for MatrixStage<CN> {
164 fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> {
165 let c = self.gamut_color_conversion;
166 for chunk in dst.chunks_exact_mut(CN) {
167 let r = mlaf(
168 mlaf(chunk[0] * c.v[0][0], chunk[1], c.v[0][1]),
169 chunk[2],
170 c.v[0][2],
171 );
172 let g = mlaf(
173 mlaf(chunk[0] * c.v[1][0], chunk[1], c.v[1][1]),
174 chunk[2],
175 c.v[1][2],
176 );
177 let b = mlaf(
178 mlaf(chunk[0] * c.v[2][0], chunk[1], c.v[2][1]),
179 chunk[2],
180 c.v[2][2],
181 );
182
183 chunk[0] = m_clamp(r, 0.0, 1.0);
184 chunk[1] = m_clamp(g, 0.0, 1.0);
185 chunk[2] = m_clamp(b, 0.0, 1.0);
186 }
187 Ok(())
188 }
189}
190
191struct MatrixGamutClipping<const CN: usize> {
192 pub(crate) gamut_color_conversion: Matrix3f,
193}
194
195impl<const CN: usize> InPlaceStage for MatrixGamutClipping<CN> {
196 fn transform(&self, dst: &mut [f32]) -> Result<(), CmsError> {
197 let c = self.gamut_color_conversion;
198 for chunk in dst.chunks_exact_mut(CN) {
199 let r = mlaf(
200 mlaf(chunk[0] * c.v[0][0], chunk[1], c.v[0][1]),
201 chunk[2],
202 c.v[0][2],
203 );
204 let g = mlaf(
205 mlaf(chunk[0] * c.v[1][0], chunk[1], c.v[1][1]),
206 chunk[2],
207 c.v[1][2],
208 );
209 let b = mlaf(
210 mlaf(chunk[0] * c.v[2][0], chunk[1], c.v[2][1]),
211 chunk[2],
212 c.v[2][2],
213 );
214
215 let mut rgb = Rgb::new(r, g, b);
216 if rgb.is_out_of_gamut() {
217 rgb = filmlike_clip(rgb);
218 chunk[0] = m_clamp(rgb.r, 0.0, 1.0);
219 chunk[1] = m_clamp(rgb.g, 0.0, 1.0);
220 chunk[2] = m_clamp(rgb.b, 0.0, 1.0);
221 } else {
222 chunk[0] = m_clamp(r, 0.0, 1.0);
223 chunk[1] = m_clamp(g, 0.0, 1.0);
224 chunk[2] = m_clamp(b, 0.0, 1.0);
225 }
226 }
227 Ok(())
228 }
229}
230
231struct ToneMapperImpl<T: Copy, const N: usize, const CN: usize, const GAMMA_SIZE: usize> {
232 linear_map_r: Box<[f32; N]>,
233 linear_map_g: Box<[f32; N]>,
234 linear_map_b: Box<[f32; N]>,
235 gamma_map_r: Box<[T; 65536]>,
236 gamma_map_g: Box<[T; 65536]>,
237 gamma_map_b: Box<[T; 65536]>,
238 im_stage: Option<Box<dyn InPlaceStage + Sync + Send>>,
239 tone_map: Box<SyncToneMap>,
240 params: RgbToneMapperParameters,
241}
242
243struct ToneMapperImplYrg<T: Copy, const N: usize, const CN: usize, const GAMMA_SIZE: usize> {
244 linear_map_r: Box<[f32; N]>,
245 linear_map_g: Box<[f32; N]>,
246 linear_map_b: Box<[f32; N]>,
247 gamma_map_r: Box<[T; 65536]>,
248 gamma_map_g: Box<[T; 65536]>,
249 gamma_map_b: Box<[T; 65536]>,
250 to_xyz: Matrix3f,
251 to_rgb: Matrix3f,
252 tone_map: Box<SyncToneMap>,
253 parameters: CommonToneMapperParameters,
254}
255
256struct ToneMapperImplOklab<T: Copy, const N: usize, const CN: usize, const GAMMA_SIZE: usize> {
257 linear_map_r: Box<[f32; N]>,
258 linear_map_g: Box<[f32; N]>,
259 linear_map_b: Box<[f32; N]>,
260 gamma_map_r: Box<[T; 65536]>,
261 gamma_map_g: Box<[T; 65536]>,
262 gamma_map_b: Box<[T; 65536]>,
263 tone_map: Box<SyncToneMap>,
264 parameters: CommonToneMapperParameters,
265}
266
267struct ToneMapperImplJzazbz<T: Copy, const N: usize, const CN: usize, const GAMMA_SIZE: usize> {
268 linear_map_r: Box<[f32; N]>,
269 linear_map_g: Box<[f32; N]>,
270 linear_map_b: Box<[f32; N]>,
271 gamma_map_r: Box<[T; 65536]>,
272 gamma_map_g: Box<[T; 65536]>,
273 gamma_map_b: Box<[T; 65536]>,
274 to_xyz: Matrix3f,
275 to_rgb: Matrix3f,
276 tone_map: Box<SyncToneMap>,
277 parameters: JzazbzToneMapperParameters,
278}
279
280pub trait ToneMapper<T: Copy + Default + Debug> {
281 fn tonemap_lane(&self, src: &[T], dst: &mut [T]) -> Result<(), ForgeError>;
286
287 fn tonemap_linearized_lane(&self, in_place: &mut [f32]) -> Result<(), ForgeError>;
291}
292
293impl<
294 T: Copy + AsPrimitive<usize> + Clone + Default + Debug,
295 const N: usize,
296 const CN: usize,
297 const GAMMA_SIZE: usize,
298 > ToneMapper<T> for ToneMapperImpl<T, N, CN, GAMMA_SIZE>
299where
300 u32: AsPrimitive<T>,
301{
302 fn tonemap_lane(&self, src: &[T], dst: &mut [T]) -> Result<(), ForgeError> {
303 assert!(CN == 3 || CN == 4);
304 if src.len() != dst.len() {
305 return Err(ForgeError::LaneSizeMismatch);
306 }
307 if src.len() % CN != 0 {
308 return Err(ForgeError::LaneMultipleOfChannels);
309 }
310 assert_eq!(src.len(), dst.len());
311 let mut linearized_content = vec![0f32; src.len()];
312 for (src, dst) in src
313 .chunks_exact(CN)
314 .zip(linearized_content.chunks_exact_mut(CN))
315 {
316 dst[0] = self.linear_map_r[src[0].as_()] * self.params.exposure;
317 dst[1] = self.linear_map_g[src[1].as_()] * self.params.exposure;
318 dst[2] = self.linear_map_b[src[2].as_()] * self.params.exposure;
319 if CN == 4 {
320 dst[3] = f32::from_bits(src[3].as_() as u32);
321 }
322 }
323
324 self.tonemap_linearized_lane(&mut linearized_content)?;
325
326 if let Some(c) = &self.im_stage {
327 c.transform(&mut linearized_content)
328 .map_err(|_| ForgeError::UnknownError)?;
329 } else {
330 for chunk in linearized_content.chunks_exact_mut(CN) {
331 let rgb = Rgb::new(chunk[0], chunk[1], chunk[2]);
332 chunk[0] = rgb.r.min(1.).max(0.);
333 chunk[1] = rgb.g.min(1.).max(0.);
334 chunk[2] = rgb.b.min(1.).max(0.);
335 }
336 }
337
338 let scale_value = (GAMMA_SIZE - 1) as f32;
339
340 for (dst, src) in dst
341 .chunks_exact_mut(CN)
342 .zip(linearized_content.chunks_exact(CN))
343 {
344 let r = mlaf(0.5f32, src[0], scale_value) as u16;
345 let g = mlaf(0.5f32, src[1], scale_value) as u16;
346 let b = mlaf(0.5f32, src[2], scale_value) as u16;
347 dst[0] = self.gamma_map_r[r as usize];
348 dst[1] = self.gamma_map_g[g as usize];
349 dst[2] = self.gamma_map_b[b as usize];
350 if CN == 4 {
351 dst[3] = src[3].to_bits().as_();
352 }
353 }
354
355 Ok(())
356 }
357
358 fn tonemap_linearized_lane(&self, in_place: &mut [f32]) -> Result<(), ForgeError> {
359 assert!(CN == 3 || CN == 4);
360 if in_place.len() % CN != 0 {
361 return Err(ForgeError::LaneMultipleOfChannels);
362 }
363 self.tone_map.process_lane(in_place);
364 Ok(())
365 }
366}
367
368impl<
369 T: Copy + AsPrimitive<usize> + Clone + Default + Debug,
370 const N: usize,
371 const CN: usize,
372 const GAMMA_SIZE: usize,
373 > ToneMapper<T> for ToneMapperImplYrg<T, N, CN, GAMMA_SIZE>
374where
375 u32: AsPrimitive<T>,
376{
377 fn tonemap_lane(&self, src: &[T], dst: &mut [T]) -> Result<(), ForgeError> {
378 assert!(CN == 3 || CN == 4);
379 if src.len() != dst.len() {
380 return Err(ForgeError::LaneSizeMismatch);
381 }
382 if src.len() % CN != 0 {
383 return Err(ForgeError::LaneMultipleOfChannels);
384 }
385 assert_eq!(src.len(), dst.len());
386 let mut linearized_content = vec![0f32; src.len()];
387 for (src, dst) in src
388 .chunks_exact(CN)
389 .zip(linearized_content.chunks_exact_mut(CN))
390 {
391 let xyz = (Rgb::new(
392 self.linear_map_r[src[0].as_()],
393 self.linear_map_g[src[1].as_()],
394 self.linear_map_b[src[2].as_()],
395 ) * self.parameters.exposure)
396 .to_xyz(self.to_xyz);
397 let yrg = Yrg::from_xyz(xyz);
398 dst[0] = yrg.y;
399 dst[1] = yrg.r;
400 dst[2] = yrg.g;
401 if CN == 4 {
402 dst[3] = f32::from_bits(src[3].as_() as u32);
403 }
404 }
405
406 self.tonemap_linearized_lane(&mut linearized_content)?;
407
408 match self.parameters.gamut_clipping {
409 GamutClipping::NoClip => {
410 for dst in linearized_content.chunks_exact_mut(CN) {
411 let yrg = Yrg::new(dst[0], dst[1], dst[2]);
412 let xyz = yrg.to_xyz();
413 let rgb = xyz.to_linear_rgb(self.to_rgb);
414 dst[0] = rgb.r.min(1.).max(0.);
415 dst[1] = rgb.g.min(1.).max(0.);
416 dst[2] = rgb.b.min(1.).max(0.);
417 }
418 }
419 GamutClipping::Clip => {
420 for dst in linearized_content.chunks_exact_mut(CN) {
421 let yrg = Yrg::new(dst[0], dst[1], dst[2]);
422 let xyz = yrg.to_xyz();
423 let mut rgb = xyz.to_linear_rgb(self.to_rgb);
424 if rgb.is_out_of_gamut() {
425 rgb = filmlike_clip(rgb);
426 }
427 dst[0] = rgb.r.min(1.).max(0.);
428 dst[1] = rgb.g.min(1.).max(0.);
429 dst[2] = rgb.b.min(1.).max(0.);
430 }
431 }
432 }
433
434 let scale_value = (GAMMA_SIZE - 1) as f32;
435
436 for (dst, src) in dst
437 .chunks_exact_mut(CN)
438 .zip(linearized_content.chunks_exact(CN))
439 {
440 let r = mlaf(0.5f32, src[0], scale_value) as u16;
441 let g = mlaf(0.5f32, src[1], scale_value) as u16;
442 let b = mlaf(0.5f32, src[2], scale_value) as u16;
443 dst[0] = self.gamma_map_r[r as usize];
444 dst[1] = self.gamma_map_g[g as usize];
445 dst[2] = self.gamma_map_b[b as usize];
446 if CN == 4 {
447 dst[3] = src[3].to_bits().as_();
448 }
449 }
450
451 Ok(())
452 }
453
454 fn tonemap_linearized_lane(&self, in_place: &mut [f32]) -> Result<(), ForgeError> {
455 assert!(CN == 3 || CN == 4);
456 if in_place.len() % CN != 0 {
457 return Err(ForgeError::LaneMultipleOfChannels);
458 }
459 self.tone_map.process_luma_lane(in_place);
460 Ok(())
461 }
462}
463
464impl<
465 T: Copy + AsPrimitive<usize> + Clone + Default + Debug,
466 const N: usize,
467 const CN: usize,
468 const GAMMA_SIZE: usize,
469 > ToneMapper<T> for ToneMapperImplOklab<T, N, CN, GAMMA_SIZE>
470where
471 u32: AsPrimitive<T>,
472{
473 fn tonemap_lane(&self, src: &[T], dst: &mut [T]) -> Result<(), ForgeError> {
474 assert!(CN == 3 || CN == 4);
475 if src.len() != dst.len() {
476 return Err(ForgeError::LaneSizeMismatch);
477 }
478 if src.len() % CN != 0 {
479 return Err(ForgeError::LaneMultipleOfChannels);
480 }
481 assert_eq!(src.len(), dst.len());
482 let mut linearized_content = vec![0f32; src.len()];
483 for (src, dst) in src
484 .chunks_exact(CN)
485 .zip(linearized_content.chunks_exact_mut(CN))
486 {
487 let xyz = Rgb::new(
488 self.linear_map_r[src[0].as_()],
489 self.linear_map_g[src[1].as_()],
490 self.linear_map_b[src[2].as_()],
491 ) * self.parameters.exposure;
492 let yrg = Oklab::from_linear_rgb(xyz);
493 dst[0] = yrg.l;
494 dst[1] = yrg.a;
495 dst[2] = yrg.b;
496 if CN == 4 {
497 dst[3] = f32::from_bits(src[3].as_() as u32);
498 }
499 }
500
501 self.tonemap_linearized_lane(&mut linearized_content)?;
502
503 for dst in linearized_content.chunks_exact_mut(CN) {
504 let yrg = Oklab::new(dst[0], dst[1], dst[2]);
505 let rgb = yrg.to_linear_rgb();
506 dst[0] = rgb.r;
507 dst[1] = rgb.g;
508 dst[2] = rgb.b;
509 }
510
511 for chunk in linearized_content.chunks_exact_mut(CN) {
512 let mut rgb = Rgb::new(chunk[0], chunk[1], chunk[2]);
513 if rgb.is_out_of_gamut() {
514 rgb = filmlike_clip(rgb);
515 }
516 chunk[0] = rgb.r.min(1.).max(0.);
517 chunk[1] = rgb.g.min(1.).max(0.);
518 chunk[2] = rgb.b.min(1.).max(0.);
519 }
520
521 let scale_value = (GAMMA_SIZE - 1) as f32;
522
523 for (dst, src) in dst
524 .chunks_exact_mut(CN)
525 .zip(linearized_content.chunks_exact(CN))
526 {
527 let r = mlaf(0.5f32, src[0], scale_value) as u16;
528 let g = mlaf(0.5f32, src[1], scale_value) as u16;
529 let b = mlaf(0.5f32, src[2], scale_value) as u16;
530 dst[0] = self.gamma_map_r[r as usize];
531 dst[1] = self.gamma_map_g[g as usize];
532 dst[2] = self.gamma_map_b[b as usize];
533 if CN == 4 {
534 dst[3] = src[3].to_bits().as_();
535 }
536 }
537
538 Ok(())
539 }
540
541 fn tonemap_linearized_lane(&self, in_place: &mut [f32]) -> Result<(), ForgeError> {
542 assert!(CN == 3 || CN == 4);
543 if in_place.len() % CN != 0 {
544 return Err(ForgeError::LaneMultipleOfChannels);
545 }
546 self.tone_map.process_luma_lane(in_place);
547 Ok(())
548 }
549}
550
551impl<
552 T: Copy + AsPrimitive<usize> + Clone + Default + Debug,
553 const N: usize,
554 const CN: usize,
555 const GAMMA_SIZE: usize,
556 > ToneMapper<T> for ToneMapperImplJzazbz<T, N, CN, GAMMA_SIZE>
557where
558 u32: AsPrimitive<T>,
559{
560 fn tonemap_lane(&self, src: &[T], dst: &mut [T]) -> Result<(), ForgeError> {
561 assert!(CN == 3 || CN == 4);
562 if src.len() != dst.len() {
563 return Err(ForgeError::LaneSizeMismatch);
564 }
565 if src.len() % CN != 0 {
566 return Err(ForgeError::LaneMultipleOfChannels);
567 }
568 assert_eq!(src.len(), dst.len());
569 let mut linearized_content = vec![0f32; src.len()];
570
571 for (src, dst) in src
572 .chunks_exact(CN)
573 .zip(linearized_content.chunks_exact_mut(CN))
574 {
575 let xyz = (Rgb::new(
576 self.linear_map_r[src[0].as_()],
577 self.linear_map_g[src[1].as_()],
578 self.linear_map_b[src[2].as_()],
579 ) * self.parameters.exposure)
580 .to_xyz(self.to_xyz);
581 let jab =
582 Jzazbz::from_xyz_with_display_luminance(xyz, self.parameters.content_brightness);
583 dst[0] = jab.jz;
584 dst[1] = jab.az;
585 dst[2] = jab.bz;
586 if CN == 4 {
587 dst[3] = f32::from_bits(src[3].as_() as u32);
588 }
589 }
590
591 self.tonemap_linearized_lane(&mut linearized_content)?;
592
593 match self.parameters.gamut_clipping {
594 GamutClipping::NoClip => {
595 for dst in linearized_content.chunks_exact_mut(CN) {
596 let jab = Jzazbz::new(dst[0], dst[1], dst[2]);
597 let xyz = jab.to_xyz(self.parameters.content_brightness);
598 let rgb = xyz.to_linear_rgb(self.to_rgb);
599 dst[0] = rgb.r.min(1.).max(0.);
600 dst[1] = rgb.g.min(1.).max(0.);
601 dst[2] = rgb.b.min(1.).max(0.);
602 }
603 }
604 GamutClipping::Clip => {
605 for dst in linearized_content.chunks_exact_mut(CN) {
606 let jab = Jzazbz::new(dst[0], dst[1], dst[2]);
607 let xyz = jab.to_xyz(self.parameters.content_brightness);
608 let mut rgb = xyz.to_linear_rgb(self.to_rgb);
609 if rgb.is_out_of_gamut() {
610 rgb = filmlike_clip(rgb);
611 }
612 dst[0] = rgb.r.min(1.).max(0.);
613 dst[1] = rgb.g.min(1.).max(0.);
614 dst[2] = rgb.b.min(1.).max(0.);
615 }
616 }
617 }
618
619 let scale_value = (GAMMA_SIZE - 1) as f32;
620
621 for (dst, src) in dst
622 .chunks_exact_mut(CN)
623 .zip(linearized_content.chunks_exact(CN))
624 {
625 let r = mlaf(0.5f32, src[0], scale_value) as u16;
626 let g = mlaf(0.5f32, src[1], scale_value) as u16;
627 let b = mlaf(0.5f32, src[2], scale_value) as u16;
628 dst[0] = self.gamma_map_r[r as usize];
629 dst[1] = self.gamma_map_g[g as usize];
630 dst[2] = self.gamma_map_b[b as usize];
631 if CN == 4 {
632 dst[3] = src[3].to_bits().as_();
633 }
634 }
635
636 Ok(())
637 }
638
639 fn tonemap_linearized_lane(&self, in_place: &mut [f32]) -> Result<(), ForgeError> {
640 assert!(CN == 3 || CN == 4);
641 if in_place.len() % CN != 0 {
642 return Err(ForgeError::LaneMultipleOfChannels);
643 }
644 self.tone_map.process_luma_lane(in_place);
645 Ok(())
646 }
647}
648
649#[derive(Debug, Clone, Copy, PartialOrd, PartialEq)]
650pub struct GainHdrMetadata {
651 pub content_max_brightness: f32,
652 pub display_max_brightness: f32,
653}
654
655impl Default for GainHdrMetadata {
656 fn default() -> Self {
657 Self {
658 content_max_brightness: 1000f32,
659 display_max_brightness: 250f32,
660 }
661 }
662}
663
664impl GainHdrMetadata {
665 pub fn new(content_max_brightness: f32, display_max_brightness: f32) -> Self {
666 Self {
667 content_max_brightness,
668 display_max_brightness,
669 }
670 }
671}
672
673fn make_icc_transform(
674 input_color_space: &ColorProfile,
675 output_color_space: &ColorProfile,
676) -> Matrix3f {
677 input_color_space
678 .transform_matrix(output_color_space)
679 .to_f32()
680}
681
682fn create_tone_mapper_u8<const CN: usize>(
683 input_color_space: &ColorProfile,
684 output_color_space: &ColorProfile,
685 method: ToneMappingMethod,
686 working_color_space: MappingColorSpace,
687) -> Result<Box<SyncToneMapper8Bit>, ForgeError> {
688 let (linear_table_r, linear_table_g, linear_table_b);
689 if let Some(trc) = input_color_space
690 .cicp
691 .and_then(|x| trc_from_cicp(x.transfer_characteristics))
692 {
693 linear_table_r = trc.generate_linear_table_u8();
694 linear_table_g = linear_table_r.clone();
695 linear_table_b = linear_table_g.clone();
696 } else {
697 linear_table_r = input_color_space
698 .build_r_linearize_table::<u8, 256, 8>(true)
699 .map_err(|_| ForgeError::InvalidTrcCurve)?;
700 linear_table_g = input_color_space
701 .build_g_linearize_table::<u8, 256, 8>(true)
702 .map_err(|_| ForgeError::InvalidTrcCurve)?;
703 linear_table_b = input_color_space
704 .build_b_linearize_table::<u8, 256, 8>(true)
705 .map_err(|_| ForgeError::InvalidTrcCurve)?;
706 }
707 let (gamma_table_r, gamma_table_g, gamma_table_b);
708 if let Some(trc) = output_color_space
709 .cicp
710 .and_then(|x| trc_from_cicp(x.transfer_characteristics))
711 {
712 gamma_table_r = trc.generate_gamma_table_u8();
713 gamma_table_g = gamma_table_r.clone();
714 gamma_table_b = gamma_table_g.clone();
715 } else {
716 gamma_table_r = output_color_space
717 .build_gamma_table::<u8, 65536, 8192, 8>(&output_color_space.red_trc, true)
718 .unwrap();
719 gamma_table_g = output_color_space
720 .build_gamma_table::<u8, 65536, 8192, 8>(&output_color_space.green_trc, true)
721 .unwrap();
722 gamma_table_b = output_color_space
723 .build_gamma_table::<u8, 65536, 8192, 8>(&output_color_space.blue_trc, true)
724 .unwrap();
725 }
726 let conversion = make_icc_transform(input_color_space, output_color_space);
727
728 let tone_map = make_mapper::<CN>(input_color_space, method);
729
730 match working_color_space {
731 MappingColorSpace::Rgb(params) => {
732 let im_stage: Box<dyn InPlaceStage + Send + Sync> =
733 if params.gamut_clipping == GamutClipping::Clip {
734 Box::new(MatrixGamutClipping::<CN> {
735 gamut_color_conversion: conversion,
736 })
737 } else {
738 Box::new(MatrixStage::<CN> {
739 gamut_color_conversion: conversion,
740 })
741 };
742
743 Ok(Box::new(ToneMapperImpl::<u8, 256, CN, 8192> {
744 linear_map_r: linear_table_r,
745 linear_map_g: linear_table_g,
746 linear_map_b: linear_table_b,
747 gamma_map_r: gamma_table_r,
748 gamma_map_b: gamma_table_g,
749 gamma_map_g: gamma_table_b,
750 im_stage: Some(im_stage),
751 tone_map,
752 params,
753 }))
754 }
755 MappingColorSpace::YRgb(params) => {
756 let d50_to_d65 = adaption_matrix(WHITE_POINT_D50.to_xyz(), WHITE_POINT_D65.to_xyz());
757
758 let mut to_xyz = d50_to_d65 * input_color_space.rgb_to_xyz_matrix().to_f32();
760 to_xyz = to_xyz.mul_row::<1>(1.05785528f32);
761 let output_d50 = output_color_space.rgb_to_xyz_matrix().to_f32();
762 let mut to_rgb = output_d50.inverse() * d50_to_d65;
763 to_rgb = to_rgb.mul_row::<1>(1. / 1.05785528f32);
764
765 Ok(Box::new(ToneMapperImplYrg::<u8, 256, CN, 8192> {
766 linear_map_r: linear_table_r,
767 linear_map_g: linear_table_g,
768 linear_map_b: linear_table_b,
769 gamma_map_r: gamma_table_r,
770 gamma_map_b: gamma_table_g,
771 gamma_map_g: gamma_table_b,
772 to_xyz,
773 to_rgb,
774 tone_map,
775 parameters: params,
776 }))
777 }
778 MappingColorSpace::Oklab(params) => {
779 Ok(Box::new(ToneMapperImplOklab::<u8, 256, CN, 8192> {
780 linear_map_r: linear_table_r,
781 linear_map_g: linear_table_g,
782 linear_map_b: linear_table_b,
783 gamma_map_r: gamma_table_r,
784 gamma_map_b: gamma_table_g,
785 gamma_map_g: gamma_table_b,
786 tone_map,
787 parameters: params,
788 }))
789 }
790 MappingColorSpace::Jzazbz(brightness) => {
791 let d50_to_d65 = adaption_matrix(WHITE_POINT_D50.to_xyz(), WHITE_POINT_D65.to_xyz());
792
793 let to_xyz = d50_to_d65 * input_color_space.rgb_to_xyz_matrix().to_f32();
795 let output_d65 = output_color_space.rgb_to_xyz_matrix().to_f32();
796 let to_rgb = output_d65.inverse() * d50_to_d65;
797
798 Ok(Box::new(ToneMapperImplJzazbz::<u8, 256, CN, 8192> {
799 linear_map_r: linear_table_r,
800 linear_map_g: linear_table_g,
801 linear_map_b: linear_table_b,
802 gamma_map_r: gamma_table_r,
803 gamma_map_b: gamma_table_g,
804 gamma_map_g: gamma_table_b,
805 to_xyz,
806 to_rgb,
807 tone_map,
808 parameters: brightness,
809 }))
810 }
811 }
812}
813
814fn create_tone_mapper_u16<const CN: usize, const BIT_DEPTH: usize>(
815 input_color_space: &ColorProfile,
816 output_color_space: &ColorProfile,
817 method: ToneMappingMethod,
818 working_color_space: MappingColorSpace,
819) -> Result<Box<SyncToneMapper16Bit>, ForgeError> {
820 assert!((8..=16).contains(&BIT_DEPTH));
821 let (linear_table_r, linear_table_g, linear_table_b);
822 if let Some(trc) = input_color_space
823 .cicp
824 .and_then(|x| trc_from_cicp(x.transfer_characteristics))
825 {
826 linear_table_r = trc.generate_linear_table_u16(BIT_DEPTH);
827 linear_table_g = linear_table_r.clone();
828 linear_table_b = linear_table_g.clone();
829 } else {
830 linear_table_r = input_color_space
831 .build_r_linearize_table::<u16, 65536, BIT_DEPTH>(true)
832 .map_err(|_| ForgeError::InvalidTrcCurve)?;
833 linear_table_g = input_color_space
834 .build_g_linearize_table::<u16, 65536, BIT_DEPTH>(true)
835 .map_err(|_| ForgeError::InvalidTrcCurve)?;
836 linear_table_b = input_color_space
837 .build_b_linearize_table::<u16, 65536, BIT_DEPTH>(true)
838 .map_err(|_| ForgeError::InvalidTrcCurve)?;
839 }
840 let (gamma_table_r, gamma_table_g, gamma_table_b);
841 if let Some(trc) = output_color_space
842 .cicp
843 .and_then(|x| trc_from_cicp(x.transfer_characteristics))
844 {
845 gamma_table_r = trc.generate_gamma_table_u16(BIT_DEPTH);
846 gamma_table_g = gamma_table_r.clone();
847 gamma_table_b = gamma_table_g.clone();
848 } else {
849 gamma_table_r = output_color_space
850 .build_gamma_table::<u16, 65536, 65536, BIT_DEPTH>(&output_color_space.red_trc, true)
851 .unwrap();
852 gamma_table_g = output_color_space
853 .build_gamma_table::<u16, 65536, 65536, BIT_DEPTH>(&output_color_space.green_trc, true)
854 .unwrap();
855 gamma_table_b = output_color_space
856 .build_gamma_table::<u16, 65536, 65536, BIT_DEPTH>(&output_color_space.blue_trc, true)
857 .unwrap();
858 }
859 let tone_map = make_mapper::<CN>(input_color_space, method);
860
861 match working_color_space {
862 MappingColorSpace::Rgb(params) => {
863 let conversion = make_icc_transform(input_color_space, output_color_space);
864
865 let im_stage: Box<dyn InPlaceStage + Send + Sync> =
866 if params.gamut_clipping == GamutClipping::Clip {
867 Box::new(MatrixGamutClipping::<CN> {
868 gamut_color_conversion: conversion,
869 })
870 } else {
871 Box::new(MatrixStage::<CN> {
872 gamut_color_conversion: conversion,
873 })
874 };
875
876 Ok(Box::new(ToneMapperImpl::<u16, 65536, CN, 65536> {
877 linear_map_r: linear_table_r,
878 linear_map_g: linear_table_g,
879 linear_map_b: linear_table_b,
880 gamma_map_r: gamma_table_r,
881 gamma_map_b: gamma_table_g,
882 gamma_map_g: gamma_table_b,
883 im_stage: Some(im_stage),
884 tone_map,
885 params,
886 }))
887 }
888 MappingColorSpace::YRgb(params) => {
889 let d50_to_d65 = adaption_matrix(WHITE_POINT_D50.to_xyz(), WHITE_POINT_D65.to_xyz());
890
891 let mut to_xyz = d50_to_d65 * input_color_space.rgb_to_xyz_matrix().to_f32();
893 to_xyz = to_xyz.mul_row::<1>(1.05785528f32);
894 let output_d50 = output_color_space.rgb_to_xyz_matrix().to_f32();
895 let mut to_rgb = output_d50.inverse() * d50_to_d65;
896 to_rgb = to_rgb.mul_row::<1>(1. / 1.05785528f32);
897
898 Ok(Box::new(ToneMapperImplYrg::<u16, 65536, CN, 65536> {
899 linear_map_r: linear_table_r,
900 linear_map_g: linear_table_g,
901 linear_map_b: linear_table_b,
902 gamma_map_r: gamma_table_r,
903 gamma_map_b: gamma_table_g,
904 gamma_map_g: gamma_table_b,
905 to_xyz,
906 to_rgb,
907 tone_map,
908 parameters: params,
909 }))
910 }
911 MappingColorSpace::Oklab(params) => {
912 Ok(Box::new(ToneMapperImplOklab::<u16, 65536, CN, 65536> {
913 linear_map_r: linear_table_r,
914 linear_map_g: linear_table_g,
915 linear_map_b: linear_table_b,
916 gamma_map_r: gamma_table_r,
917 gamma_map_b: gamma_table_g,
918 gamma_map_g: gamma_table_b,
919 tone_map,
920 parameters: params,
921 }))
922 }
923 MappingColorSpace::Jzazbz(brightness) => {
924 let d50_to_d65 = adaption_matrix(WHITE_POINT_D50.to_xyz(), WHITE_POINT_D65.to_xyz());
925
926 let to_xyz = d50_to_d65 * input_color_space.rgb_to_xyz_matrix().to_f32();
928 let output_d65 = output_color_space.rgb_to_xyz_matrix().to_f32();
929 let to_rgb = output_d65.inverse() * d50_to_d65;
930
931 Ok(Box::new(ToneMapperImplJzazbz::<u16, 65536, CN, 65536> {
932 linear_map_r: linear_table_r,
933 linear_map_g: linear_table_g,
934 linear_map_b: linear_table_b,
935 gamma_map_r: gamma_table_r,
936 gamma_map_b: gamma_table_g,
937 gamma_map_g: gamma_table_b,
938 to_xyz,
939 to_rgb,
940 tone_map,
941 parameters: brightness,
942 }))
943 }
944 }
945}
946
947fn make_mapper<const CN: usize>(
948 input_color_space: &ColorProfile,
949 method: ToneMappingMethod,
950) -> Box<SyncToneMap> {
951 let primaries = input_color_space.rgb_to_xyz_matrix().to_f32();
952 let luma_primaries: [f32; 3] = primaries.v[1];
953 let tone_map: Box<SyncToneMap> = match method {
954 ToneMappingMethod::Rec2408(data) => Box::new(Rec2408ToneMapper::<CN>::new(
955 data.content_max_brightness,
956 data.display_max_brightness,
957 203f32,
958 luma_primaries,
959 )),
960 ToneMappingMethod::Filmic => Box::new(FilmicToneMapper::<CN>::default()),
961 ToneMappingMethod::Aces => Box::new(AcesToneMapper::<CN>::default()),
962 ToneMappingMethod::ExtendedReinhard => Box::new(ExtendedReinhardToneMapper::<CN> {
963 primaries: luma_primaries,
964 }),
965 ToneMappingMethod::ReinhardJodie => Box::new(ReinhardJodieToneMapper::<CN> {
966 primaries: luma_primaries,
967 }),
968 ToneMappingMethod::Reinhard => Box::new(ReinhardToneMapper::<CN>::default()),
969 ToneMappingMethod::Clamp => Box::new(ClampToneMapper::<CN>::default()),
970 ToneMappingMethod::FilmicSpline(params) => {
971 let spline = create_spline(params);
972 Box::new(SplineToneMapper::<CN> {
973 spline,
974 primaries: luma_primaries,
975 })
976 }
977 ToneMappingMethod::Agx(look) => match look {
978 AgxLook::Agx => Box::new(AgxToneMapper::<CN> {
979 primaries: luma_primaries,
980 agx_custom_look: AgxDefault::custom_look(),
981 }),
982 AgxLook::Punchy => Box::new(AgxToneMapper::<CN> {
983 primaries: luma_primaries,
984 agx_custom_look: AgxPunchy::custom_look(),
985 }),
986 AgxLook::Golden => Box::new(AgxToneMapper::<CN> {
987 primaries: luma_primaries,
988 agx_custom_look: AgxGolden::custom_look(),
989 }),
990 AgxLook::Custom(look) => Box::new(AgxToneMapper::<CN> {
991 primaries: luma_primaries,
992 agx_custom_look: look,
993 }),
994 },
995 };
996 tone_map
997}
998
999macro_rules! define8 {
1000 ($method: ident, $cn: expr, $name: expr) => {
1001 #[doc = concat!("Creates an ", $name," tone mapper. \
1002 \
1003 ICC profile do expect that for HDR tone management `CICP` tag will be used. \
1004 Tone mapper will search for `CICP` in [ColorProfile] and if there is some value, \
1005 then transfer function from `CICP` will be used. \
1006 Otherwise, we will interpolate ICC tone reproduction LUT tables.")]
1007 pub fn $method(
1008 input_color_space: &ColorProfile,
1009 output_color_space: &ColorProfile,
1010 method: ToneMappingMethod,
1011 working_color_space: MappingColorSpace,
1012 ) -> Result<Box<SyncToneMapper8Bit>, ForgeError> {
1013 create_tone_mapper_u8::<$cn>(
1014 input_color_space,
1015 output_color_space,
1016 method,
1017 working_color_space,
1018 )
1019 }
1020 };
1021}
1022
1023define8!(create_tone_mapper_rgb, 3, "RGB8");
1024define8!(create_tone_mapper_rgba, 4, "RGBA8");
1025
1026macro_rules! define16 {
1027 ($method: ident, $cn: expr, $bp: expr, $name: expr) => {
1028 #[doc = concat!("Creates an ", $name," tone mapper. \
1029 \
1030 ICC profile do expect that for HDR tone management `CICP` tag will be used. \
1031 Tone mapper will search for `CICP` in [ColorProfile] and if there is some value, \
1032 then transfer function from `CICP` will be used. \
1033 Otherwise, we will interpolate ICC tone reproduction LUT tables.")]
1034 pub fn $method(
1035 input_color_space: &ColorProfile,
1036 output_color_space: &ColorProfile,
1037 method: ToneMappingMethod,
1038 working_color_space: MappingColorSpace,
1039 ) -> Result<Box<SyncToneMapper16Bit>, ForgeError> {
1040 create_tone_mapper_u16::<$cn, $bp>(
1041 input_color_space,
1042 output_color_space,
1043 method,
1044 working_color_space,
1045 )
1046 }
1047 };
1048}
1049
1050define16!(create_tone_mapper_rgb10, 3, 10, "RGB10");
1051define16!(create_tone_mapper_rgba10, 4, 10, "RGBA10");
1052
1053define16!(create_tone_mapper_rgb12, 3, 12, "RGB12");
1054define16!(create_tone_mapper_rgba12, 4, 12, "RGBA12");
1055
1056define16!(create_tone_mapper_rgb14, 3, 14, "RGB14");
1057define16!(create_tone_mapper_rgba14, 4, 14, "RGBA14");
1058
1059define16!(create_tone_mapper_rgb16, 3, 16, "RGB16");
1060define16!(create_tone_mapper_rgba16, 4, 16, "RGBA16");