moxcms/
defaults.rs

1/*
2 * // Copyright (c) Radzivon Bartoshyk 3/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::chad::BRADFORD_D;
30use crate::cicp::create_rec709_parametric;
31use crate::trc::{ToneReprCurve, curve_from_gamma};
32use crate::{
33    CicpColorPrimaries, CicpProfile, ColorPrimaries, ColorProfile, DataColorSpace,
34    LocalizableString, Matrix3d, MatrixCoefficients, ProfileClass, ProfileText, RenderingIntent,
35    TransferCharacteristics, XyY,
36};
37use pxfm::{copysignk, exp, floor, pow};
38
39/// From lcms: `cmsWhitePointFromTemp`
40/// tempK must be >= 4000. and <= 25000.
41/// Invalid values of tempK will return
42/// (x,y,Y) = (-1.0, -1.0, -1.0)
43/// similar to argyll: `icx_DTEMP2XYZ()`
44const fn white_point_from_temperature(temp_k: i32) -> XyY {
45    let mut white_point = XyY {
46        x: 0.,
47        y: 0.,
48        yb: 0.,
49    };
50    // No optimization provided.
51    let temp_k = temp_k as f64; // Square
52    let temp_k2 = temp_k * temp_k; // Cube
53    let temp_k3 = temp_k2 * temp_k;
54    // For correlated color temperature (T) between 4000K and 7000K:
55    let x = if temp_k > 4000.0 && temp_k <= 7000.0 {
56        -4.6070 * (1E9 / temp_k3) + 2.9678 * (1E6 / temp_k2) + 0.09911 * (1E3 / temp_k) + 0.244063
57    } else if temp_k > 7000.0 && temp_k <= 25000.0 {
58        -2.0064 * (1E9 / temp_k3) + 1.9018 * (1E6 / temp_k2) + 0.24748 * (1E3 / temp_k) + 0.237040
59    } else {
60        // or for correlated color temperature (T) between 7000K and 25000K:
61        // Invalid tempK
62        white_point.x = -1.0;
63        white_point.y = -1.0;
64        white_point.yb = -1.0;
65        debug_assert!(false, "invalid temp");
66        return white_point;
67    };
68    // Obtain y(x)
69    let y = -3.000 * (x * x) + 2.870 * x - 0.275;
70    // wave factors (not used, but here for futures extensions)
71    // let M1 = (-1.3515 - 1.7703*x + 5.9114 *y)/(0.0241 + 0.2562*x - 0.7341*y);
72    // let M2 = (0.0300 - 31.4424*x + 30.0717*y)/(0.0241 + 0.2562*x - 0.7341*y);
73    // Fill white_point struct
74    white_point.x = x;
75    white_point.y = y;
76    white_point.yb = 1.0;
77    white_point
78}
79
80pub const WHITE_POINT_D50: XyY = white_point_from_temperature(5003);
81pub const WHITE_POINT_D60: XyY = white_point_from_temperature(6000);
82pub const WHITE_POINT_D65: XyY = white_point_from_temperature(6504);
83pub const WHITE_POINT_DCI_P3: XyY = white_point_from_temperature(6300);
84
85// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-F.pdf
86// Perceptual Quantization / SMPTE standard ST.2084
87#[inline]
88const fn pq_curve(x: f64) -> f64 {
89    const M1: f64 = 2610.0 / 16384.0;
90    const M2: f64 = (2523.0 / 4096.0) * 128.0;
91    const C1: f64 = 3424.0 / 4096.0;
92    const C2: f64 = (2413.0 / 4096.0) * 32.0;
93    const C3: f64 = (2392.0 / 4096.0) * 32.0;
94
95    if x == 0.0 {
96        return 0.0;
97    }
98    let sign = x;
99    let x = x.abs();
100
101    let xpo = pow(x, 1.0 / M2);
102    let num = (xpo - C1).max(0.0);
103    let den = C2 - C3 * xpo;
104    let res = pow(num / den, 1.0 / M1);
105
106    copysignk(res, sign)
107}
108
109pub(crate) const fn build_trc_table_pq() -> [u16; 4096] {
110    let mut table = [0u16; 4096];
111
112    const NUM_ENTRIES: usize = 4096;
113    let mut i = 0usize;
114    while i < NUM_ENTRIES {
115        let x: f64 = i as f64 / (NUM_ENTRIES - 1) as f64;
116        let y: f64 = pq_curve(x);
117        let mut output: f64;
118        output = y * 65535.0 + 0.5;
119        if output > 65535.0 {
120            output = 65535.0
121        }
122        if output < 0.0 {
123            output = 0.0
124        }
125        table[i] = floor(output) as u16;
126        i += 1;
127    }
128    table
129}
130
131pub(crate) const fn build_trc_table_hlg() -> [u16; 4096] {
132    let mut table = [0u16; 4096];
133
134    const NUM_ENTRIES: usize = 4096;
135    let mut i = 0usize;
136    while i < NUM_ENTRIES {
137        let x: f64 = i as f64 / (NUM_ENTRIES - 1) as f64;
138        let y: f64 = hlg_curve(x);
139        let mut output: f64;
140        output = y * 65535.0 + 0.5;
141        if output > 65535.0 {
142            output = 65535.0
143        }
144        if output < 0.0 {
145            output = 0.0
146        }
147        table[i] = floor(output) as u16;
148        i += 1;
149    }
150    table
151}
152
153// https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2100-2-201807-I!!PDF-F.pdf
154// Hybrid Log-Gamma
155const fn hlg_curve(x: f64) -> f64 {
156    const BETA: f64 = 0.04;
157    const RA: f64 = 5.591816309728916; // 1.0 / A where A = 0.17883277
158    const B: f64 = 0.28466892; // 1.0 - 4.0 * A
159    const C: f64 = 0.5599107295; // 0,5 –aln(4a)
160
161    let e = (x * (1.0 - BETA) + BETA).max(0.0);
162
163    if e == 0.0 {
164        return 0.0;
165    }
166
167    let sign = e.abs();
168
169    let res = if e <= 0.5 {
170        e * e / 3.0
171    } else {
172        (exp((e - C) * RA) + B) / 12.0
173    };
174
175    copysignk(res, sign)
176}
177
178/// Perceptual Quantizer Lookup table
179pub const PQ_LUT_TABLE: [u16; 4096] = build_trc_table_pq();
180/// Hybrid Log Gamma Lookup table
181pub const HLG_LUT_TABLE: [u16; 4096] = build_trc_table_hlg();
182
183impl ColorProfile {
184    const SRGB_COLORANTS: Matrix3d =
185        ColorProfile::colorants_matrix(WHITE_POINT_D65, ColorPrimaries::BT_709);
186
187    const DISPLAY_P3_COLORANTS: Matrix3d =
188        ColorProfile::colorants_matrix(WHITE_POINT_D65, ColorPrimaries::SMPTE_432);
189
190    const ADOBE_RGB_COLORANTS: Matrix3d =
191        ColorProfile::colorants_matrix(WHITE_POINT_D65, ColorPrimaries::ADOBE_RGB);
192
193    const DCI_P3_COLORANTS: Matrix3d =
194        ColorProfile::colorants_matrix(WHITE_POINT_DCI_P3, ColorPrimaries::DCI_P3);
195
196    const PRO_PHOTO_RGB_COLORANTS: Matrix3d =
197        ColorProfile::colorants_matrix(WHITE_POINT_D50, ColorPrimaries::PRO_PHOTO_RGB);
198
199    const BT2020_COLORANTS: Matrix3d =
200        ColorProfile::colorants_matrix(WHITE_POINT_D65, ColorPrimaries::BT_2020);
201
202    const ACES_2065_1_COLORANTS: Matrix3d =
203        ColorProfile::colorants_matrix(WHITE_POINT_D60, ColorPrimaries::ACES_2065_1);
204
205    const ACES_CG_COLORANTS: Matrix3d =
206        ColorProfile::colorants_matrix(WHITE_POINT_D60, ColorPrimaries::ACES_CG);
207
208    #[inline]
209    fn basic_rgb_profile() -> ColorProfile {
210        ColorProfile {
211            profile_class: ProfileClass::DisplayDevice,
212            rendering_intent: RenderingIntent::Perceptual,
213            color_space: DataColorSpace::Rgb,
214            pcs: DataColorSpace::Xyz,
215            chromatic_adaptation: Some(BRADFORD_D),
216            white_point: WHITE_POINT_D50.to_xyzd(),
217            ..Default::default()
218        }
219    }
220
221    /// Creates new profile from CICP
222    pub fn new_from_cicp(cicp_color_primaries: CicpProfile) -> ColorProfile {
223        let mut basic = ColorProfile::basic_rgb_profile();
224        basic.update_rgb_colorimetry_from_cicp(cicp_color_primaries);
225        basic
226    }
227
228    /// Creates new sRGB profile
229    pub fn new_srgb() -> ColorProfile {
230        let mut profile = ColorProfile::basic_rgb_profile();
231        profile.update_colorants(ColorProfile::SRGB_COLORANTS);
232
233        let curve =
234            ToneReprCurve::Parametric(vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045]);
235        profile.red_trc = Some(curve.clone());
236        profile.blue_trc = Some(curve.clone());
237        profile.green_trc = Some(curve);
238        profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd());
239        profile.cicp = Some(CicpProfile {
240            color_primaries: CicpColorPrimaries::Bt709,
241            transfer_characteristics: TransferCharacteristics::Srgb,
242            matrix_coefficients: MatrixCoefficients::Bt709,
243            full_range: false,
244        });
245        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
246            "en".to_string(),
247            "US".to_string(),
248            "sRGB IEC61966-2.1".to_string(),
249        )]));
250        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
251            "en".to_string(),
252            "US".to_string(),
253            "Public Domain".to_string(),
254        )]));
255        profile
256    }
257
258    /// Creates new Adobe RGB profile
259    pub fn new_adobe_rgb() -> ColorProfile {
260        let mut profile = ColorProfile::basic_rgb_profile();
261        profile.update_colorants(ColorProfile::ADOBE_RGB_COLORANTS);
262
263        let curve = curve_from_gamma(2.19921875f32);
264        profile.red_trc = Some(curve.clone());
265        profile.blue_trc = Some(curve.clone());
266        profile.green_trc = Some(curve);
267        profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd());
268        profile.white_point = WHITE_POINT_D50.to_xyzd();
269        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
270            "en".to_string(),
271            "US".to_string(),
272            "Adobe RGB 1998".to_string(),
273        )]));
274        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
275            "en".to_string(),
276            "US".to_string(),
277            "Public Domain".to_string(),
278        )]));
279        profile
280    }
281
282    /// Creates new Display P3 profile
283    pub fn new_display_p3() -> ColorProfile {
284        let mut profile = ColorProfile::basic_rgb_profile();
285        profile.update_colorants(ColorProfile::DISPLAY_P3_COLORANTS);
286
287        let curve =
288            ToneReprCurve::Parametric(vec![2.4, 1. / 1.055, 0.055 / 1.055, 1. / 12.92, 0.04045]);
289        profile.red_trc = Some(curve.clone());
290        profile.blue_trc = Some(curve.clone());
291        profile.green_trc = Some(curve);
292        profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd());
293        profile.cicp = Some(CicpProfile {
294            color_primaries: CicpColorPrimaries::Smpte431,
295            transfer_characteristics: TransferCharacteristics::Srgb,
296            matrix_coefficients: MatrixCoefficients::Bt709,
297            full_range: false,
298        });
299        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
300            "en".to_string(),
301            "US".to_string(),
302            "Display P3".to_string(),
303        )]));
304        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
305            "en".to_string(),
306            "US".to_string(),
307            "Public Domain".to_string(),
308        )]));
309        profile
310    }
311
312    /// Creates new Display P3 PQ profile
313    pub fn new_display_p3_pq() -> ColorProfile {
314        let mut profile = ColorProfile::basic_rgb_profile();
315        profile.update_colorants(ColorProfile::DISPLAY_P3_COLORANTS);
316
317        let curve = ToneReprCurve::Lut(PQ_LUT_TABLE.to_vec());
318
319        profile.red_trc = Some(curve.clone());
320        profile.blue_trc = Some(curve.clone());
321        profile.green_trc = Some(curve);
322        profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd());
323        profile.cicp = Some(CicpProfile {
324            color_primaries: CicpColorPrimaries::Smpte431,
325            transfer_characteristics: TransferCharacteristics::Smpte2084,
326            matrix_coefficients: MatrixCoefficients::Bt709,
327            full_range: false,
328        });
329        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
330            "en".to_string(),
331            "US".to_string(),
332            "Display P3 PQ".to_string(),
333        )]));
334        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
335            "en".to_string(),
336            "US".to_string(),
337            "Public Domain".to_string(),
338        )]));
339        profile
340    }
341
342    /// Creates new DCI P3 profile
343    pub fn new_dci_p3() -> ColorProfile {
344        let mut profile = ColorProfile::basic_rgb_profile();
345        profile.update_colorants(ColorProfile::DCI_P3_COLORANTS);
346
347        let curve = curve_from_gamma(2.6f32);
348        profile.red_trc = Some(curve.clone());
349        profile.blue_trc = Some(curve.clone());
350        profile.green_trc = Some(curve);
351        profile.media_white_point = Some(WHITE_POINT_DCI_P3.to_xyzd());
352        profile.cicp = Some(CicpProfile {
353            color_primaries: CicpColorPrimaries::Smpte432,
354            transfer_characteristics: TransferCharacteristics::Srgb,
355            matrix_coefficients: MatrixCoefficients::Bt709,
356            full_range: false,
357        });
358        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
359            "en".to_string(),
360            "US".to_string(),
361            "DCI P3".to_string(),
362        )]));
363        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
364            "en".to_string(),
365            "US".to_string(),
366            "Public Domain".to_string(),
367        )]));
368        profile
369    }
370
371    /// Creates new ProPhoto RGB profile
372    pub fn new_pro_photo_rgb() -> ColorProfile {
373        let mut profile = ColorProfile::basic_rgb_profile();
374        profile.update_colorants(ColorProfile::PRO_PHOTO_RGB_COLORANTS);
375
376        let curve = curve_from_gamma(1.8f32);
377        profile.red_trc = Some(curve.clone());
378        profile.blue_trc = Some(curve.clone());
379        profile.green_trc = Some(curve);
380        profile.media_white_point = Some(WHITE_POINT_D50.to_xyzd());
381        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
382            "en".to_string(),
383            "US".to_string(),
384            "ProPhoto RGB".to_string(),
385        )]));
386        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
387            "en".to_string(),
388            "US".to_string(),
389            "Public Domain".to_string(),
390        )]));
391        profile
392    }
393
394    /// Creates new Bt.2020 profile
395    pub fn new_bt2020() -> ColorProfile {
396        let mut profile = ColorProfile::basic_rgb_profile();
397        profile.update_colorants(ColorProfile::BT2020_COLORANTS);
398
399        let curve = ToneReprCurve::Parametric(create_rec709_parametric().to_vec());
400        profile.red_trc = Some(curve.clone());
401        profile.blue_trc = Some(curve.clone());
402        profile.green_trc = Some(curve);
403        profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd());
404        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
405            "en".to_string(),
406            "US".to_string(),
407            "Rec.2020".to_string(),
408        )]));
409        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
410            "en".to_string(),
411            "US".to_string(),
412            "Public Domain".to_string(),
413        )]));
414        profile
415    }
416
417    /// Creates new Bt.2020 PQ profile
418    pub fn new_bt2020_pq() -> ColorProfile {
419        let mut profile = ColorProfile::basic_rgb_profile();
420        profile.update_colorants(ColorProfile::BT2020_COLORANTS);
421
422        let curve = ToneReprCurve::Lut(PQ_LUT_TABLE.to_vec());
423
424        profile.red_trc = Some(curve.clone());
425        profile.blue_trc = Some(curve.clone());
426        profile.green_trc = Some(curve);
427        profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd());
428        profile.cicp = Some(CicpProfile {
429            color_primaries: CicpColorPrimaries::Bt2020,
430            transfer_characteristics: TransferCharacteristics::Smpte2084,
431            matrix_coefficients: MatrixCoefficients::Bt709,
432            full_range: false,
433        });
434        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
435            "en".to_string(),
436            "US".to_string(),
437            "Rec.2020 PQ".to_string(),
438        )]));
439        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
440            "en".to_string(),
441            "US".to_string(),
442            "Public Domain".to_string(),
443        )]));
444        profile
445    }
446
447    /// Creates new Bt.2020 HLG profile
448    pub fn new_bt2020_hlg() -> ColorProfile {
449        let mut profile = ColorProfile::basic_rgb_profile();
450        profile.update_colorants(ColorProfile::BT2020_COLORANTS);
451
452        let curve = ToneReprCurve::Lut(HLG_LUT_TABLE.to_vec());
453
454        profile.red_trc = Some(curve.clone());
455        profile.blue_trc = Some(curve.clone());
456        profile.green_trc = Some(curve);
457        profile.media_white_point = Some(WHITE_POINT_D65.to_xyzd());
458        profile.cicp = Some(CicpProfile {
459            color_primaries: CicpColorPrimaries::Bt2020,
460            transfer_characteristics: TransferCharacteristics::Hlg,
461            matrix_coefficients: MatrixCoefficients::Bt709,
462            full_range: false,
463        });
464        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
465            "en".to_string(),
466            "US".to_string(),
467            "Rec.2020 HLG".to_string(),
468        )]));
469        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
470            "en".to_string(),
471            "US".to_string(),
472            "Public Domain".to_string(),
473        )]));
474        profile
475    }
476
477    /// Creates new Monochrome profile
478    pub fn new_gray_with_gamma(gamma: f32) -> ColorProfile {
479        ColorProfile {
480            gray_trc: Some(curve_from_gamma(gamma)),
481            profile_class: ProfileClass::DisplayDevice,
482            rendering_intent: RenderingIntent::Perceptual,
483            color_space: DataColorSpace::Gray,
484            media_white_point: Some(WHITE_POINT_D65.to_xyzd()),
485            white_point: WHITE_POINT_D50.to_xyzd(),
486            chromatic_adaptation: Some(BRADFORD_D),
487            copyright: Some(ProfileText::Localizable(vec![LocalizableString::new(
488                "en".to_string(),
489                "US".to_string(),
490                "Public Domain".to_string(),
491            )])),
492            ..Default::default()
493        }
494    }
495
496    /// Creates new ACES 2065-1/AP0 profile
497    pub fn new_aces_aces_2065_1_linear() -> ColorProfile {
498        let mut profile = ColorProfile::basic_rgb_profile();
499        profile.update_colorants(ColorProfile::ACES_2065_1_COLORANTS);
500
501        let curve = ToneReprCurve::Lut(vec![]);
502        profile.red_trc = Some(curve.clone());
503        profile.blue_trc = Some(curve.clone());
504        profile.green_trc = Some(curve);
505        profile.media_white_point = Some(WHITE_POINT_D60.to_xyzd());
506        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
507            "en".to_string(),
508            "US".to_string(),
509            "ACES 2065-1".to_string(),
510        )]));
511        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
512            "en".to_string(),
513            "US".to_string(),
514            "Public Domain".to_string(),
515        )]));
516        profile
517    }
518
519    /// Creates new ACEScg profile
520    pub fn new_aces_cg_linear() -> ColorProfile {
521        let mut profile = ColorProfile::basic_rgb_profile();
522        profile.update_colorants(ColorProfile::ACES_CG_COLORANTS);
523
524        let curve = ToneReprCurve::Lut(vec![]);
525        profile.red_trc = Some(curve.clone());
526        profile.blue_trc = Some(curve.clone());
527        profile.green_trc = Some(curve);
528        profile.media_white_point = Some(WHITE_POINT_D60.to_xyzd());
529        profile.description = Some(ProfileText::Localizable(vec![LocalizableString::new(
530            "en".to_string(),
531            "US".to_string(),
532            "ACEScg/AP1".to_string(),
533        )]));
534        profile.copyright = Some(ProfileText::Localizable(vec![LocalizableString::new(
535            "en".to_string(),
536            "US".to_string(),
537            "Public Domain".to_string(),
538        )]));
539        profile
540    }
541}