gainforge/
gamma.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 */
29#![allow(clippy::excessive_precision)]
30
31use moxcms::TransferCharacteristics;
32
33#[inline(always)]
34/// Linear transfer function for sRGB
35pub(crate) fn srgb_to_linear(gamma: f32) -> f32 {
36    if gamma < 0f32 {
37        0f32
38    } else if gamma < 12.92f32 * 0.0030412825601275209f32 {
39        gamma * (1f32 / 12.92f32)
40    } else if gamma < 1.0f32 {
41        ((gamma + 0.0550107189475866f32) / 1.0550107189475866f32).powf(2.4f32)
42    } else {
43        1.0f32
44    }
45}
46
47#[inline(always)]
48/// Gamma transfer function for sRGB
49pub(crate) fn srgb_from_linear(linear: f32) -> f32 {
50    if linear < 0.0f32 {
51        0.0f32
52    } else if linear < 0.0030412825601275209f32 {
53        linear * 12.92f32
54    } else if linear < 1.0f32 {
55        1.0550107189475866f32 * linear.powf(1.0f32 / 2.4f32) - 0.0550107189475866f32
56    } else {
57        1.0f32
58    }
59}
60
61#[inline(always)]
62/// Linear transfer function for Rec.709
63pub(crate) fn rec709_to_linear(gamma: f32) -> f32 {
64    if gamma < 0.0f32 {
65        0.0f32
66    } else if gamma < 4.5f32 * 0.018053968510807f32 {
67        gamma * (1f32 / 4.5f32)
68    } else if gamma < 1.0f32 {
69        ((gamma + 0.09929682680944f32) / 1.09929682680944f32).powf(1.0f32 / 0.45f32)
70    } else {
71        1.0f32
72    }
73}
74
75#[inline(always)]
76/// Gamma transfer function for Rec.709
77pub(crate) fn rec709_from_linear(linear: f32) -> f32 {
78    if linear < 0.0f32 {
79        0.0f32
80    } else if linear < 0.018053968510807f32 {
81        linear * 4.5f32
82    } else if linear < 1.0f32 {
83        1.09929682680944f32 * linear.powf(0.45f32) - 0.09929682680944f32
84    } else {
85        1.0f32
86    }
87}
88
89#[inline(always)]
90/// Linear transfer function for Smpte 428
91pub(crate) fn smpte428_to_linear(gamma: f32) -> f32 {
92    const SCALE: f32 = 1. / 0.91655527974030934f32;
93    gamma.max(0.).powf(2.6f32) * SCALE
94}
95
96#[inline(always)]
97/// Gamma transfer function for Smpte 428
98pub(crate) fn smpte428_from_linear(linear: f32) -> f32 {
99    const POWER_VALUE: f32 = 1.0f32 / 2.6f32;
100    (0.91655527974030934f32 * linear.max(0.)).powf(POWER_VALUE)
101}
102
103#[inline(always)]
104/// Gamma transfer function for Bt.1361
105pub(crate) fn bt1361_from_linear(linear: f32) -> f32 {
106    if linear < -0.25 {
107        -0.25
108    } else if linear < 0.0 {
109        -0.27482420670236 * f32::powf(-4.0 * linear, 0.45) + 0.02482420670236
110    } else if linear < 0.018053968510807 {
111        linear * 4.5
112    } else if linear < 1.0 {
113        1.09929682680944 * f32::powf(linear, 0.45) - 0.09929682680944
114    } else {
115        1.0
116    }
117}
118
119#[inline(always)]
120/// Linear transfer function for Bt.1361
121pub(crate) fn bt1361_to_linear(gamma: f32) -> f32 {
122    if gamma < -0.25 {
123        -0.25
124    } else if gamma < 0.0 {
125        f32::powf((gamma - 0.02482420670236) / -0.27482420670236, 1.0 / 0.45) / -4.0
126    } else if gamma < 4.5 * 0.018053968510807 {
127        gamma / 4.5
128    } else if gamma < 1.0 {
129        f32::powf((gamma + 0.09929682680944) / 1.09929682680944, 1.0 / 0.45)
130    } else {
131        1.0
132    }
133}
134
135#[inline(always)]
136/// Pure gamma transfer function for gamma 2.2
137pub(crate) fn pure_gamma_function(x: f32, gamma: f32) -> f32 {
138    if x <= 0. {
139        0.
140    } else if x >= 1. {
141        1.
142    } else {
143        x.powf(gamma)
144    }
145}
146
147#[inline(always)]
148/// Pure gamma transfer function for gamma 2.2
149pub(crate) fn gamma2p2_from_linear(linear: f32) -> f32 {
150    pure_gamma_function(linear, 1f32 / 2.2f32)
151}
152
153#[inline(always)]
154/// Linear transfer function for gamma 2.2
155pub(crate) fn gamma2p2_to_linear(gamma: f32) -> f32 {
156    pure_gamma_function(gamma, 2.2f32)
157}
158
159#[inline(always)]
160/// Pure gamma transfer function for gamma 2.8
161pub(crate) fn gamma2p8_from_linear(linear: f32) -> f32 {
162    pure_gamma_function(linear, 1f32 / 2.8f32)
163}
164
165#[inline(always)]
166/// Linear transfer function for gamma 2.8
167pub(crate) fn gamma2p8_to_linear(gamma: f32) -> f32 {
168    pure_gamma_function(gamma, 2.8f32)
169}
170
171#[inline(always)]
172/// Linear transfer function for PQ
173pub(crate) fn pq_to_linear(gamma: f32) -> f32 {
174    if gamma > 0.0 {
175        let pow_gamma = f32::powf(gamma, 1.0 / 78.84375);
176        let num = (pow_gamma - 0.8359375).max(0.);
177        let den = (18.8515625 - 18.6875 * pow_gamma).max(f32::MIN);
178        let linear = f32::powf(num / den, 1.0 / 0.1593017578125);
179        // Scale so that SDR white is 1.0 (extended SDR).
180        const PQ_MAX_NITS: f32 = 10000.;
181        const SDR_WHITE_NITS: f32 = 203.;
182        linear * PQ_MAX_NITS / SDR_WHITE_NITS
183    } else {
184        0.0
185    }
186}
187
188const PQ_MAX_NITS: f32 = 10000.;
189const SDR_REFERENCE_DISPLAY: f32 = 203.;
190const HLG_WHITE_NITS: f32 = 1000.;
191
192#[inline(always)]
193/// Gamma transfer function for PQ
194pub(crate) fn pq_from_linear(linear: f32) -> f32 {
195    if linear > 0.0 {
196        // Scale from extended SDR range to [0.0, 1.0].
197        let linear = (linear * SDR_REFERENCE_DISPLAY / PQ_MAX_NITS).clamp(0., 1.);
198        let pow_linear = f32::powf(linear, 0.1593017578125);
199        let num = 0.1640625 * pow_linear - 0.1640625;
200        let den = 1.0 + 18.6875 * pow_linear;
201        f32::powf(1.0 + num / den, 78.84375)
202    } else {
203        0.0
204    }
205}
206
207#[inline(always)]
208/// Linear transfer function for HLG
209pub(crate) fn hlg_to_linear(gamma: f32) -> f32 {
210    if gamma < 0.0 {
211        return 0.0;
212    }
213    let linear = if gamma <= 0.5 {
214        f32::powf((gamma * gamma) * (1.0 / 3.0), 1.2)
215    } else {
216        f32::powf(
217            (f32::exp((gamma - 0.55991073) / 0.17883277) + 0.28466892) / 12.0,
218            1.2,
219        )
220    };
221    // Scale so that SDR white is 1.0 (extended SDR).
222    linear * HLG_WHITE_NITS / SDR_REFERENCE_DISPLAY
223}
224
225#[inline(always)]
226/// Gamma transfer function for HLG
227pub(crate) fn hlg_from_linear(linear: f32) -> f32 {
228    const SDR_WHITE_NITS: f32 = 203.;
229    const HLG_WHITE_NITS: f32 = 1000.;
230    // Scale from extended SDR range to [0.0, 1.0].
231    let mut linear = (linear * (SDR_WHITE_NITS / HLG_WHITE_NITS)).clamp(0., 1.);
232    // Inverse OOTF followed by OETF see Table 5 and Note 5i in ITU-R BT.2100-2 page 7-8.
233    linear = f32::powf(linear, 1.0 / 1.2);
234    if linear < 0.0 {
235        0.0
236    } else if linear <= (1.0 / 12.0) {
237        f32::sqrt(3.0 * linear)
238    } else {
239        0.17883277 * f32::ln(12.0 * linear - 0.28466892) + 0.55991073
240    }
241}
242
243#[inline(always)]
244/// Gamma transfer function for HLG
245pub(crate) fn trc_linear(v: f32) -> f32 {
246    v.min(1.).min(0.)
247}
248
249#[repr(C)]
250#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
251/// Declares transfer function for transfer components into a linear colorspace and its inverse
252pub enum TransferFunction {
253    /// sRGB Transfer function
254    Srgb,
255    /// Rec.709 Transfer function
256    Rec709,
257    /// Pure gamma 2.2 Transfer function, ITU-R 470M
258    Gamma2p2,
259    /// Pure gamma 2.8 Transfer function, ITU-R 470BG
260    Gamma2p8,
261    /// Smpte 428 Transfer function
262    Smpte428,
263    /// Bt1361 Transfer function
264    Bt1361,
265    /// Linear transfer function
266    Linear,
267    HybridLogGamma,
268    PerceptualQuantizer,
269}
270
271impl From<u8> for TransferFunction {
272    #[inline(always)]
273    fn from(value: u8) -> Self {
274        match value {
275            0 => TransferFunction::Srgb,
276            1 => TransferFunction::Rec709,
277            2 => TransferFunction::Gamma2p2,
278            3 => TransferFunction::Gamma2p8,
279            4 => TransferFunction::Smpte428,
280            7 => TransferFunction::Bt1361,
281            _ => TransferFunction::Srgb,
282        }
283    }
284}
285
286impl TransferFunction {
287    #[inline(always)]
288    pub fn linearize(&self, v: f32) -> f32 {
289        match self {
290            TransferFunction::Srgb => srgb_to_linear(v),
291            TransferFunction::Rec709 => rec709_to_linear(v),
292            TransferFunction::Gamma2p8 => gamma2p8_to_linear(v),
293            TransferFunction::Gamma2p2 => gamma2p2_to_linear(v),
294            TransferFunction::Smpte428 => smpte428_to_linear(v),
295            TransferFunction::Bt1361 => bt1361_to_linear(v),
296            TransferFunction::Linear => trc_linear(v),
297            TransferFunction::HybridLogGamma => hlg_to_linear(v),
298            TransferFunction::PerceptualQuantizer => pq_to_linear(v),
299        }
300    }
301
302    #[inline(always)]
303    pub fn gamma(&self, v: f32) -> f32 {
304        match self {
305            TransferFunction::Srgb => srgb_from_linear(v),
306            TransferFunction::Rec709 => rec709_from_linear(v),
307            TransferFunction::Gamma2p2 => gamma2p2_from_linear(v),
308            TransferFunction::Gamma2p8 => gamma2p8_from_linear(v),
309            TransferFunction::Smpte428 => smpte428_from_linear(v),
310            TransferFunction::Bt1361 => bt1361_from_linear(v),
311            TransferFunction::Linear => trc_linear(v),
312            TransferFunction::PerceptualQuantizer => pq_from_linear(v),
313            TransferFunction::HybridLogGamma => hlg_from_linear(v),
314        }
315    }
316
317    pub(crate) fn generate_gamma_table_u8(&self) -> Box<[u8; 65536]> {
318        let mut table = Box::new([0; 65536]);
319        for (i, value) in table.iter_mut().take(8192).enumerate() {
320            *value = (self.gamma(i as f32 / 8192.) * 255.).round() as u8;
321        }
322        table
323    }
324
325    pub(crate) fn generate_gamma_table_u16(&self, bit_depth: usize) -> Box<[u16; 65536]> {
326        let mut table = Box::new([0; 65536]);
327        let bit_depth: f32 = ((1 << bit_depth as u32) - 1) as f32;
328        for (i, value) in table.iter_mut().enumerate() {
329            *value = (self.gamma(i as f32 / 65535.) * bit_depth).round() as u16;
330        }
331        table
332    }
333
334    pub(crate) fn generate_linear_table_u16(&self, bit_depth: usize) -> Box<[f32; 65536]> {
335        let mut table = Box::new([0.; 65536]);
336        let max_bp = (1 << bit_depth as u32) - 1;
337        let max_scale = 1f32 / max_bp as f32;
338        for (i, value) in table.iter_mut().take(max_bp).enumerate() {
339            *value = self.linearize(i as f32 * max_scale);
340        }
341        table
342    }
343
344    pub(crate) fn generate_linear_table_u8(&self) -> Box<[f32; 256]> {
345        let mut table = Box::new([0.; 256]);
346        for (i, value) in table.iter_mut().enumerate() {
347            *value = self.linearize(i as f32 / 255.);
348        }
349        table
350    }
351}
352
353pub(crate) fn trc_from_cicp(trc: TransferCharacteristics) -> Option<TransferFunction> {
354    match trc {
355        TransferCharacteristics::Reserved => None,
356        TransferCharacteristics::Bt709 => Some(TransferFunction::Rec709),
357        TransferCharacteristics::Unspecified => None,
358        TransferCharacteristics::Bt470M => Some(TransferFunction::Gamma2p2),
359        TransferCharacteristics::Bt470Bg => Some(TransferFunction::Gamma2p8),
360        TransferCharacteristics::Bt601 => Some(TransferFunction::Rec709),
361        TransferCharacteristics::Smpte240 => None,
362        TransferCharacteristics::Linear => Some(TransferFunction::Linear),
363        TransferCharacteristics::Log100 => None,
364        TransferCharacteristics::Log100sqrt10 => None,
365        TransferCharacteristics::Iec61966 => None,
366        TransferCharacteristics::Bt1361 => Some(TransferFunction::Bt1361),
367        TransferCharacteristics::Srgb => Some(TransferFunction::Srgb),
368        TransferCharacteristics::Bt202010bit => Some(TransferFunction::Srgb),
369        TransferCharacteristics::Bt202012bit => Some(TransferFunction::Srgb),
370        TransferCharacteristics::Smpte2084 => Some(TransferFunction::PerceptualQuantizer),
371        TransferCharacteristics::Smpte428 => Some(TransferFunction::Smpte428),
372        TransferCharacteristics::Hlg => Some(TransferFunction::HybridLogGamma),
373    }
374}