cmx 0.2.0

Rust Spectral Color Management Library
Documentation
// SPDX-License-Identifier: Apache-2.0 OR MIT
// Copyright (c) 2021-2026, Harbers Bik LLC

//! `DisplayProfile` module, with its definition and various constructors,
//! in particular, small, bar-bones ICC 4.3 instances for the sRGB, DisplayP3, and AdobeRgb
//! color spaces:
//!
//! * [`DisplayProfile::cmx_srgb`] for sRGB,
//! * [`DisplayProfile::cmx_adobe_rgb`] for AdobeRGB,
//! * and [`DisplayProfile::cmx_display_p3`] for the DisplayP3 space.

use serde::Serialize;

use crate::{
    profile::Profile,
    tag::{bradford::Bradford, RenderingIntent},
};

use super::RawProfile;

/// A `DisplayProfile` represents display devices such as monitors.
/// Most common is the three-component matrix-based type, often used as embedded profile in image files such as PNG,
/// but the standard also defines types for  N-component LUT-based, and monochrome display devices.
///
/// It is a wrapper around `RawProfile`, where most of the functionally is delegated to.
///
/// # Required Tags
///
/// * profileDescriptionTag
/// * copyrightTag
/// * mediaWhitePointTag
///
/// ## Three-component matrix-based Display profiles
///
/// * redMatrixColumnTag, greenMatrixColumnTag, and blueMatrixColumnTag,
/// * redTRCTag, greenTRCTag, and blueTRCTag
///
/// ## N-Component LUT-based Display profiles
///
/// * AToB0Tag
/// * BToA0Tag
///
/// ## Monochrome Displays
///
/// * grayTRCTag
#[derive(Debug, Clone, Serialize, PartialEq)]
pub struct DisplayProfile(pub(crate) RawProfile);

impl TryFrom<Profile> for DisplayProfile {
    type Error = crate::Error;

    fn try_from(profile: Profile) -> Result<Self, Self::Error> {
        if let Profile::Display(display_profile) = profile {
            Ok(display_profile)
        } else {
            Err(Self::Error::IsNotA("Display Profile"))
        }
    }
}

impl DisplayProfile {
    /// Creates a new, empty `DisplayProfile` with:
    ///
    /// - the default [`RawProfile`] defaults: valid `acsp` signature, version 4.3,
    ///   and the current date as the creation timestamp
    /// - `DeviceClass` set to `Display` (`mntr`)
    /// - `ColorSpace` set to `RGB`
    ///
    /// All tags must be added explicitly via the builder API before the profile
    /// can be used with a Color Management System.
    pub fn new() -> Self {
        Self(
            Self(RawProfile::default())
                .0
                .with_device_class(crate::signatures::DeviceClass::Display)
                .with_data_color_space(crate::signatures::ColorSpace::RGB),
        )
    }

    /// Creates an display profile from a Colorimetry::RgbSpace.
    ///
    /// This type of profile is typically used to embed in image files,
    /// such as a PNG.
    ///
    /// # Example
    /// See `colorimetry-plot::examples::display_p3_gamut.rs` how this is used to show a full
    /// DisplayP3 gamut in a CIE 1931 chromaticity diagram, if viewed on a high gamut display, using
    /// a modern Web Browser with support of managed color workflows.
    #[rustfmt::skip]
    pub fn from_rgb_space(rgb_space: colorimetry::rgb::RgbSpace, rendering_intent: RenderingIntent) -> Self {
        let display_profile = Self::new();
        let pcs_illuminant = display_profile.pcs_illuminant(); // always D50
        let obs = colorimetry::observer::Observer::Cie1931;
        let pcs_illuminant_xyz = colorimetry::xyz::XYZ::new(pcs_illuminant, obs);
        let media_white_xyz = rgb_space.white_point(obs).set_illuminance(1.0).to_array();
        let m_rgb = obs.calc_rgb2xyz_matrix_with_alt_white(rgb_space, Some(pcs_illuminant_xyz));
        let r_xyz = m_rgb.column(0);
        let g_xyz = m_rgb.column(1);
        let b_xyz = m_rgb.column(2);
        let bradford = Bradford::new(media_white_xyz, pcs_illuminant).as_matrix();
        let gamma_values = rgb_space.gamma().values();

        use crate::tag::tags::*;

        display_profile
            .with_rendering_intent(rendering_intent)
            .with_tag(ProfileDescriptionTag)
                .as_text_description(|text| {
                    text.set_ascii("CMX_P3");
                })
            .with_tag(CopyrightTag)
                .as_text(|text| {
                    text.set_text("CC0 1.0");
                })
            .with_tag(MediaWhitePointTag)
                .as_xyz_array(|xyz| {
                    xyz.set(media_white_xyz);
                })
            .with_tag(RedMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set(r_xyz.as_slice().try_into().unwrap());
                })
            .with_tag(GreenMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set(g_xyz.as_slice().try_into().unwrap());
                })
            .with_tag(BlueMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set(b_xyz.as_slice().try_into().unwrap());
                })
            .with_tag(ChromaticAdaptationTag)
                .as_sf15_fixed_16_array(|array| {
                    let bradford_array: [f64; 9] = bradford.as_slice().try_into().unwrap();
                    array.set(bradford_array);
                })
            .with_tag(RedTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters_slice(gamma_values)
                        .expect("gamma_values has a valid ICC parametric curve parameter count");
                })
            .with_tag(BlueTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters_slice(gamma_values)
                        .expect("gamma_values has a valid ICC parametric curve parameter count");
                })
            .with_tag(GreenTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters_slice(gamma_values)
                        .expect("gamma_values has a valid ICC parametric curve parameter count");
                })
            .with_profile_id()
    }

    /// Creates a sRGB Display Profile, with the specified rendering intent.
    ///
    /// # Note
    /// sRGB is the standard color space for web images and most consumer devices.
    /// It is designed to match typical home and office viewing conditions, making it suitable for
    /// general-purpose images that will be viewed on a variety of devices.
    ///
    /// # License: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
    /// This ICC Display Profile by Harbers Bik LLC is marked <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0</a>
    #[rustfmt::skip]
    pub fn cmx_srgb(rendering_intent: RenderingIntent) -> Self {
        use crate::tag::tags::*;
        DisplayProfile::new()
            .with_rendering_intent(rendering_intent)
            .with_tag(ProfileDescriptionTag)
                .as_text_description(|text| {
                    text.set_ascii("CMX_SRGB");
                })
            .with_tag(CopyrightTag)
                .as_text(|text| {
                    text.set_text("CC0 1.0");
                })
            .with_tag(MediaWhitePointTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.950455, 1.00000, 1.08905]);
                })
            .with_tag(RedMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.436066, 0.222488, 0.013916]);
                })
            .with_tag(GreenMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.385147, 0.716873, 0.097076]);
                })
            .with_tag(BlueMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.143066, 0.060608, 0.714096]);
                })
            .with_tag(RedTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.39999, 0.94786, 0.05214, 0.07739, 0.04045]);
                })
            .with_tag(BlueTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.39999, 0.94786, 0.05214, 0.07739, 0.04045]);
                })
            .with_tag(GreenTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.39999, 0.94786, 0.05214, 0.07739, 0.04045]);
                })
            .with_tag(ChromaticAdaptationTag)
                .as_sf15_fixed_16_array(|array| {
                    array.set([
                         1.047882, 0.022919, -0.050201,
                         0.029587, 0.990479, -0.017059,
                        -0.009232, 0.015076,  0.751678
                    ]);
                })
    }

    /// Creates a DisplayProfile for the Adobe RGB color space, with the specified rendering intent.
    ///
    /// # Note
    /// Adobe RGB is the preferred color space for high-end printing and premium photography.
    /// It has a wider gamut than sRGB, especially in the green and cyan areas, making it suitable for
    /// images that require vibrant and accurate color reproduction.
    ///
    /// # License: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
    /// This ICC Display Profile by Harbers Bik LLC is marked <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0</a>
    #[rustfmt::skip]
    pub fn cmx_adobe_rgb(rendering_intent: RenderingIntent) -> Self {
        use crate::tag::tags::*;
        DisplayProfile::new()
            .with_rendering_intent(rendering_intent)
            .with_tag(ProfileDescriptionTag)
                .as_text_description(|text| {
                    text.set_ascii("CMX_AdobeRGB");
                })
            .with_tag(CopyrightTag)
                .as_text(|text| {
                    text.set_text("CC0 1.0");
                })
            .with_tag(MediaWhitePointTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.950455, 1.00000, 1.08905]);
                })
            .with_tag(RedMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.609741, 0.311111, 0.01947]);
                })
            .with_tag(GreenMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.205276, 0.625671, 0.060867]);
                })
            .with_tag(BlueMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.149185, 0.063217, 0.744568]);
                })
            .with_tag(RedTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.1992]);
                })
            .with_tag(BlueTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.1992]);
                })
            .with_tag(GreenTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.1992]);
                })
            .with_tag(ChromaticAdaptationTag)
                .as_sf15_fixed_16_array(|array| {
                    array.set([
                         1.047882, 0.022919, -0.050201,
                         0.029587, 0.990479, -0.017059,
                        -0.009232, 0.015076,  0.751678
                    ]);
                })
    }

    /// Creates a DisplayProfile for Apple's Display P3 RGB color space, with the specified rendering intent.
    ///
    /// # Notes
    ///
    /// * Display P3 is commonlay high-end monitors, and Apple devices.
    ///   It offers a wider gamut than sRGB, particularly in the red and green areas, making it ideal for
    ///   vibrant and immersive visual experiences.
    /// * It shares the same white point as sRGB (D65) and uses the same gamma curve as sRGB, but uses
    ///   the primaries as defined in the DCI-P3 standard, which are more saturated than the ones in sRGB.
    /// * Although Apple created Display P3, it is now an open standard, and is now widely supported
    ///   across various platforms and devices.
    ///
    /// # License: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication
    /// This ICC Display Profile by Harbers Bik LLC is marked <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0</a>
    #[rustfmt::skip]
    pub fn cmx_display_p3(rendering_intent: RenderingIntent) -> Self {
        use crate::tag::tags::*;
        DisplayProfile::new()
            .with_rendering_intent(rendering_intent)
            .with_tag(ProfileDescriptionTag)
                .as_text_description(|text| {
                    text.set_ascii("CMX_P3");
                })
            .with_tag(CopyrightTag)
                .as_text(|text| {
                    text.set_text("CC0 1.0");
                })
            .with_tag(MediaWhitePointTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.950455, 1.00000, 1.08905]);
                })
            .with_tag(RedMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.515121, 0.241196, -0.001053]);
                })
            .with_tag(GreenMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.291977, 0.692245, 0.041885]);
                })
            .with_tag(BlueMatrixColumnTag)
                .as_xyz_array(|xyz| {
                    xyz.set([0.157104, 0.066574, 0.784073]);
                })
            .with_tag(RedTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.39999, 0.94786, 0.05214, 0.07739, 0.04045]);
                })
            .with_tag(BlueTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.39999, 0.94786, 0.05214, 0.07739, 0.04045]);
                })
            .with_tag(GreenTRCTag)
                .as_parametric_curve(|para| {
                    para.set_parameters([2.39999, 0.94786, 0.05214, 0.07739, 0.04045]);
                })
            .with_tag(ChromaticAdaptationTag)
                .as_sf15_fixed_16_array(|array| {
                    array.set([
                         1.047882, 0.022919, -0.050201,
                         0.029587, 0.990479, -0.017059,
                        -0.009232, 0.015076,  0.751678
                    ]);
                })
    }
}

impl Default for DisplayProfile {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::tag::{
        tagdata::parametric_curve::ParametricCurveType, tags::RedTRCTag, TagSignature,
    };

    #[test]
    fn test_display_profile_from_display_p3() {
        let display_profile = DisplayProfile::from_rgb_space(
            colorimetry::rgb::RgbSpace::DisplayP3,
            RenderingIntent::RelativeColorimetric,
        );
        let bytes = display_profile.to_bytes().unwrap();
        let display_profile_2 =
            DisplayProfile::try_from(Profile::from_bytes(&bytes).unwrap()).unwrap();
        let ts: TagSignature = RedTRCTag.into();
        let t = display_profile_2.0.tags.get(&ts).unwrap();
        let parametric_curve_data = t.tag.data().as_parametric_curve().unwrap();
        let parametric_curve_values: ParametricCurveType = parametric_curve_data.into();
        assert_eq!(
            parametric_curve_values.values().as_slice(),
            [2.39999, 0.94786, 0.05214, 0.07739, 0.04045, 0.0, 0.0].as_slice()
        );

        println!("{display_profile_2}");
    }

    #[test]
    fn test_display_profile_from_rgb() {
        let display_profile = DisplayProfile::from_rgb_space(
            colorimetry::rgb::RgbSpace::Adobe,
            RenderingIntent::RelativeColorimetric,
        );
        let bytes = display_profile.to_bytes().unwrap();
        let display_profile_2 =
            DisplayProfile::try_from(Profile::from_bytes(&bytes).unwrap()).unwrap();
        let ts: TagSignature = RedTRCTag.into();
        let t = display_profile_2.0.tags.get(&ts).unwrap();
        let parametric_curve_data = t.tag.data().as_parametric_curve().unwrap();
        let parametric_curve_values: ParametricCurveType = parametric_curve_data.into();
        assert_eq!(
            parametric_curve_values.values().as_slice(),
            [2.19922, 0., 0., 0., 0., 0., 0.].as_slice()
        );

        println!("{display_profile_2}");
    }
}