/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Gamut mapping. //! use super::{AbsoluteColor, ColorSpace}; /// Gamut-mapping min precision const MIN_PRECISION: f32 = 1.0 / (i32::MAX as f32); pub mod raytrace; impl AbsoluteColor { /// 13.2.1. Binary Search Gamut Mapping with Local MINDE /// pub fn gamut_map_binary_search(&self, dest_color_space: ColorSpace) -> Self { const MIN_L: f32 = MIN_PRECISION; const MAX_L: f32 = 1.0; // 1. if destination has no gamut limits (XYZ-D65, XYZ-D50, Lab, LCH, // Oklab, OkLCh) convert origin to destination and return it as the // gamut mapped color if matches!( dest_color_space, ColorSpace::Lab | ColorSpace::Lch | ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::XyzD50 | ColorSpace::XyzD65 ) { return self.to_color_space(dest_color_space); } // 2. let origin_OkLCh be origin converted from origin color space to // the OkLCh color space let origin_oklch = self.to_color_space(ColorSpace::Oklch); // 3. if the Lightness of origin_OkLCh is greater than or equal to // 100%, convert `oklab(1 0 0 / origin.alpha)` to destination and // return it as the gamut mapped color if origin_oklch.components.0 >= MAX_L { return AbsoluteColor::new(ColorSpace::Oklab, 1.0, 0.0, 0.0, self.alpha) .to_color_space(dest_color_space); } // 4. if the Lightness of origin_OkLCh is less than than or equal to // 0%, convert `oklab(0 0 0 / origin.alpha)` to destination and // return it as the gamut mapped color if origin_oklch.components.0 <= MIN_L { return AbsoluteColor::new(ColorSpace::Oklab, 0.0, 0.0, 0.0, self.alpha) .to_color_space(dest_color_space); } // 5. let inGamut(color) be a function which returns true if, when // passed a color, that color is inside the gamut of destination. // For HSL and HWB, it returns true if the color is inside the // gamut of sRGB. // See [`Self::in_gamut`] and [`Self::in_gamut_for_dest_space`]. // 6. if inGamut(origin_OkLCh) is true, convert origin_OkLCh to // destination and return it as the gamut mapped color if origin_oklch.in_gamut_for_dest_space(dest_color_space) { return origin_oklch.to_color_space(dest_color_space); } // 7. otherwise, let delta(one, two) be a function which returns the // deltaEOK of color one compared to color two // See the [`delta_eok`] function. // 8. let JND be 0.02 const JND: f32 = 0.02; // 9. let epsilon be 0.0001 const EPSILON: f32 = 0.0001; // 10. let clip(color) be a function which converts color to // destination, clamps each component to the bounds of the // reference range for that component, and returns the result. // See [`Self::clip`] and [`Self::clip_to_dest_space`]. // 11. set current to origin_OkLCh let mut current_oklch = origin_oklch.clone(); // 12. set clipped to clip(current) let mut clipped = current_oklch.clip_to_dest_space(dest_color_space); // 13. set E to delta(clipped, current) let mut e = delta_eok(&clipped, ¤t_oklch); // 14. if E < JND, return clipped as the gamut mapped color if e < JND { return clipped; } // 15. set min to zero let mut min = 0.0; // 16. set max to the Oklch chroma of origin_Oklch. let mut max = origin_oklch.components.1; // 17. let min_inGamut be a boolean that represents when min is still // in gamut, and set it to true let mut min_in_gamut = true; // 18. while (max - min is greater than epsilon) repeat the following // steps. while max - min > EPSILON { // 18.1. set chroma to (min + max) / 2 let chroma = (min + max) / 2.0; // 18.2. set the chroma component of current to chroma current_oklch.components.1 = chroma; // 18.3. if min_inGamut is true and also if inGamut(current) is // true, set min to chroma and continue to repeat these steps. if min_in_gamut && current_oklch.in_gamut_for_dest_space(dest_color_space) { min = chroma; continue; } // 18.4. otherwise, if inGamut(current) is false carry out these // steps: // 18.4.1. set clipped to clip(current) clipped = current_oklch.clip_to_dest_space(dest_color_space); // 18.4.2. set E to delta(clipped, current) e = delta_eok(&clipped, ¤t_oklch); // 18.4.3. if E < JND if e < JND { // 18.4.3.1. if (JND - E < epsilon), return clipped as the // gamut mapped color if JND - e < EPSILON { return clipped; } // 18.4.3.2. otherwise: // 18.4.3.2.1. set min_inGamut to false min_in_gamut = false; // 18.4.3.2.2. set min to chroma min = chroma; } else { // 18.4.4. otherwise, set max to chroma and continue to repeat // these steps max = chroma; } } // 19. return clipped as the gamut mapped color clipped } /// Clamp this color to within the [0..1] range. /// NOTE this assumes RGB ranges and will not work for Lab, Oklab, or /// other color spaces with different ranges, or limitless ranges fn clip(&self) -> Self { let mut result = self.clone(); result.components = result.components.map(|c| c.clamp(0.0, 1.0)); result } /// ^10. let clip(color) be a function which converts color to destination, /// clamps each component to the bounds of the reference range for /// that component, and returns the result /// Clip/clamp this color to the supplied destination color space fn clip_to_dest_space(&self, dest_color_space: ColorSpace) -> Self { self.to_color_space(dest_color_space).clip() } /// Returns true if this color is within its gamut limits. fn in_gamut(&self) -> bool { macro_rules! in_range { ($c:expr) => {{ $c >= MIN_PRECISION && $c <= 1.0 }}; } match self.color_space { ColorSpace::Hsl | ColorSpace::Hwb => self.to_color_space(ColorSpace::Srgb).in_gamut(), ColorSpace::Srgb | ColorSpace::SrgbLinear | ColorSpace::DisplayP3 | ColorSpace::DisplayP3Linear | ColorSpace::A98Rgb | ColorSpace::ProphotoRgb | ColorSpace::Rec2020 => { in_range!(self.components.0) && in_range!(self.components.1) && in_range!(self.components.2) }, ColorSpace::Lab | ColorSpace::Lch | ColorSpace::Oklab | ColorSpace::Oklch | ColorSpace::XyzD50 | ColorSpace::XyzD65 => true, } } /// ^5. let inGamut(color) be a function which returns true if, when passed /// a color, that color is inside the gamut of destination. For HSL and /// HWB, it returns true if the color is inside the gamut of sRGB. /// Check if this color is in-gamut for the destination color space fn in_gamut_for_dest_space(&self, dest_color_space: ColorSpace) -> bool { if self.color_space == ColorSpace::Hsl || self.color_space == ColorSpace::Hwb { self.to_color_space(ColorSpace::Srgb).in_gamut() } else { self.to_color_space(dest_color_space).in_gamut() } } } /// Calculate deltaE OK (simple root sum of squares). /// fn delta_eok(reference: &AbsoluteColor, sample: &AbsoluteColor) -> f32 { // Delta is calculated in the oklab color space. let reference = reference.to_color_space(ColorSpace::Oklab); let sample = sample.to_color_space(ColorSpace::Oklab); let diff = reference.components - sample.components; let diff = diff * diff; (diff.0 + diff.1 + diff.2).sqrt() }