gainforge/
tonemapper.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 2/2025. All rights reserved.
3 * //
4 * // Redistribution and use in source and binary forms, with or without modification,
5 * // are permitted provided that the following conditions are met:
6 * //
7 * // 1.  Redistributions of source code must retain the above copyright notice, this
8 * // list of conditions and the following disclaimer.
9 * //
10 * // 2.  Redistributions in binary form must reproduce the above copyright notice,
11 * // this list of conditions and the following disclaimer in the documentation
12 * // and/or other materials provided with the distribution.
13 * //
14 * // 3.  Neither the name of the copyright holder nor the names of its
15 * // contributors may be used to endorse or promote products derived from
16 * // this software without specific prior written permission.
17 * //
18 * // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 * // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 * // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 * // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 * // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 * // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 * // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 * // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 * // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 * // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 */
29use 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/// Defines gamut clipping mode
47#[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    /// Scene linear light RGB colorspace.
57    ///
58    /// Some description what to expect:
59    /// - Does not produce colours outside the destination gamut.
60    /// - Generally tends to produce “natural” looking colours, although saturation of extreme colours
61    ///   is reduced substantially and some colours may be changed in hue.
62    /// - Problematic when it is desired to retain bright saturated colours, such as coloured lights at
63    ///   night
64    /// - Depending on the amount of compression, the saturation decrease may be excessive, and
65    ///   occasionally hue changes can be objectionable.
66    Rgb(RgbToneMapperParameters),
67    /// Yrg filmic colorspace.
68    ///
69    /// Slightly better results as in linear RGB, for a little computational cost.
70    ///
71    /// Some description what to expect:
72    /// - Has the potential to produce colours outside the destination gamut, which then require gamut
73    ///   mapping
74    /// - Preserves chromaticity except for where gamut mapping is applied. Does not produce a
75    ///   “natural” looking desaturation of tonally compressed colours.
76    /// - Problems can be avoided by using in combination with a variable desaturation and gamut
77    ///   mapping algorithm, although such algorithms generally perform best in hue, saturation and
78    ///   lightness colour spaces (requiring a colour space change).
79    ///
80    /// Algorithm here do not perform gamut mapping.
81    YRgb(CommonToneMapperParameters),
82    /// Oklab perceptual colorspace.
83    ///
84    /// It exists more for *experiments* how does it look like.
85    /// Results are often strange and might not be acceptable.
86    /// *Oklab is not really were created for HDR*.
87    ///
88    /// Some description of what to expect:
89    /// - Provides perceptually uniform lightness adjustments.
90    /// - Preserves hue better than RGB but can lead to color shifts due to applying aggressive compression.
91    /// - Suitable for applications where perceptual lightness control is more important than strict colorimetric accuracy.
92    Oklab(CommonToneMapperParameters),
93    /// JzAzBz perceptual HDR colorspace.
94    ///
95    /// Some description of what to expect:
96    /// - Designed for HDR workflows, explicitly modeling display-referred luminance.
97    /// - Thus, as at the first point often produces the best results.
98    /// - Provides better perceptual uniformity than Oklab, particularly in high dynamic range content.
99    /// - Preserves hue well, but chroma adjustments may be necessary when mapping between displays with different peak brightness.
100    /// - Can help avoid color distortions and oversaturation when adapting HDR content to lower-luminance displays.
101    ///
102    /// This is very slow computational method.
103    /// Content brightness should be specified.
104    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    /// Tone map image lane.
282    ///
283    /// Lane length must be multiple of channels.
284    /// Lane length must match.
285    fn tonemap_lane(&self, src: &[T], dst: &mut [T]) -> Result<(), ForgeError>;
286
287    /// Tone map lane whereas content been linearized.
288    ///
289    /// Lane length must be multiple of channels.
290    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            // We need to adapt PCS D50 to CIE XYZ 2006 with D65 white point first.
759            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            // We need to adapt PCS D50 to XYZ with D65 white point first.
794            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            // We need to adapt PCS D50 to CIE XYZ 2006 with D65 white point first.
892            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            // We need to adapt PCS D50 to XYZ with D65 white point first.
927            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");