/* 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/. */ //! Color support functions. /// cbindgen:ignore mod color_function; /// cbindgen:ignore pub mod convert; pub mod component; pub mod mix; pub mod parsing; mod to_css; use component::ColorComponent; use cssparser::color::PredefinedColorSpace; /// The 3 components that make up a color. (Does not include the alpha component) #[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] #[repr(C)] pub struct ColorComponents(pub f32, pub f32, pub f32); impl ColorComponents { /// Apply a function to each of the 3 components of the color. #[must_use] pub fn map(self, f: impl Fn(f32) -> f32) -> Self { Self(f(self.0), f(self.1), f(self.2)) } } impl std::ops::Mul for ColorComponents { type Output = Self; fn mul(self, rhs: Self) -> Self::Output { Self(self.0 * rhs.0, self.1 * rhs.1, self.2 * rhs.2) } } impl std::ops::Div for ColorComponents { type Output = Self; fn div(self, rhs: Self) -> Self::Output { Self(self.0 / rhs.0, self.1 / rhs.1, self.2 / rhs.2) } } /// A color space representation in the CSS specification. /// /// https://drafts.csswg.org/css-color-4/#typedef-color-space #[derive( Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToAnimatedValue, ToComputedValue, ToCss, ToResolvedValue, ToShmem, )] #[repr(u8)] pub enum ColorSpace { /// A color specified in the sRGB color space with either the rgb/rgba(..) /// functions or the newer color(srgb ..) function. If the color(..) /// function is used, the AS_COLOR_FUNCTION flag will be set. Examples: /// "color(srgb 0.691 0.139 0.259)", "rgb(176, 35, 66)" Srgb = 0, /// A color specified in the Hsl notation in the sRGB color space, e.g. /// "hsl(289.18 93.136% 65.531%)" /// https://drafts.csswg.org/css-color-4/#the-hsl-notation Hsl, /// A color specified in the Hwb notation in the sRGB color space, e.g. /// "hwb(740deg 20% 30%)" /// https://drafts.csswg.org/css-color-4/#the-hwb-notation Hwb, /// A color specified in the Lab color format, e.g. /// "lab(29.2345% 39.3825 20.0664)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors Lab, /// A color specified in the Lch color format, e.g. /// "lch(29.2345% 44.2 27)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors Lch, /// A color specified in the Oklab color format, e.g. /// "oklab(40.101% 0.1147 0.0453)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors Oklab, /// A color specified in the Oklch color format, e.g. /// "oklch(40.101% 0.12332 21.555)". /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors Oklch, /// A color specified with the color(..) function and the "srgb-linear" /// color space, e.g. "color(srgb-linear 0.435 0.017 0.055)". SrgbLinear, /// A color specified with the color(..) function and the "display-p3" /// color space, e.g. "color(display-p3 0.84 0.19 0.72)". DisplayP3, /// A color specified with the color(..) function and the "a98-rgb" color /// space, e.g. "color(a98-rgb 0.44091 0.49971 0.37408)". A98Rgb, /// A color specified with the color(..) function and the "prophoto-rgb" /// color space, e.g. "color(prophoto-rgb 0.36589 0.41717 0.31333)". ProphotoRgb, /// A color specified with the color(..) function and the "rec2020" color /// space, e.g. "color(rec2020 0.42210 0.47580 0.35605)". Rec2020, /// A color specified with the color(..) function and the "xyz-d50" color /// space, e.g. "color(xyz-d50 0.2005 0.14089 0.4472)". XyzD50, /// A color specified with the color(..) function and the "xyz-d65" or "xyz" /// color space, e.g. "color(xyz-d65 0.21661 0.14602 0.59452)". /// NOTE: https://drafts.csswg.org/css-color-4/#resolving-color-function-values /// specifies that `xyz` is an alias for the `xyz-d65` color space. #[parse(aliases = "xyz")] XyzD65, } impl ColorSpace { /// Returns whether this is a ``. #[inline] pub fn is_rectangular(&self) -> bool { !self.is_polar() } /// Returns whether this is a ``. #[inline] pub fn is_polar(&self) -> bool { matches!(self, Self::Hsl | Self::Hwb | Self::Lch | Self::Oklch) } /// Returns true if the color has RGB or XYZ components. #[inline] pub fn is_rgb_or_xyz_like(&self) -> bool { match self { Self::Srgb | Self::SrgbLinear | Self::DisplayP3 | Self::A98Rgb | Self::ProphotoRgb | Self::Rec2020 | Self::XyzD50 | Self::XyzD65 => true, _ => false, } } /// Returns an index of the hue component in the color space, otherwise /// `None`. #[inline] pub fn hue_index(&self) -> Option { match self { Self::Hsl | Self::Hwb => Some(0), Self::Lch | Self::Oklch => Some(2), _ => { debug_assert!(!self.is_polar()); None }, } } } /// Flags used when serializing colors. #[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] #[repr(C)] pub struct ColorFlags(u8); bitflags! { impl ColorFlags : u8 { /// Whether the 1st color component is `none`. const C0_IS_NONE = 1 << 0; /// Whether the 2nd color component is `none`. const C1_IS_NONE = 1 << 1; /// Whether the 3rd color component is `none`. const C2_IS_NONE = 1 << 2; /// Whether the alpha component is `none`. const ALPHA_IS_NONE = 1 << 3; /// Marks that this color is in the legacy color format. This flag is /// only valid for the `Srgb` color space. const IS_LEGACY_SRGB = 1 << 4; } } /// An absolutely specified color, using either rgb(), rgba(), lab(), lch(), /// oklab(), oklch() or color(). #[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] #[repr(C)] pub struct AbsoluteColor { /// The 3 components that make up colors in any color space. pub components: ColorComponents, /// The alpha component of the color. pub alpha: f32, /// The current color space that the components represent. pub color_space: ColorSpace, /// Extra flags used durring serialization of this color. pub flags: ColorFlags, } /// Given an [`AbsoluteColor`], return the 4 float components as the type given, /// e.g.: /// /// ```rust /// let srgb = AbsoluteColor::new(ColorSpace::Srgb, 1.0, 0.0, 0.0, 0.0); /// let floats = color_components_as!(&srgb, [f32; 4]); // [1.0, 0.0, 0.0, 0.0] /// ``` macro_rules! color_components_as { ($c:expr, $t:ty) => {{ // This macro is not an inline function, because we can't use the // generic type ($t) in a constant expression as per: // https://github.com/rust-lang/rust/issues/76560 const_assert_eq!(std::mem::size_of::<$t>(), std::mem::size_of::<[f32; 4]>()); const_assert_eq!(std::mem::align_of::<$t>(), std::mem::align_of::<[f32; 4]>()); const_assert!(std::mem::size_of::() >= std::mem::size_of::<$t>()); const_assert_eq!( std::mem::align_of::(), std::mem::align_of::<$t>() ); std::mem::transmute::<&ColorComponents, &$t>(&$c.components) }}; } /// Holds details about each component passed into creating a new [`AbsoluteColor`]. pub struct ComponentDetails { value: f32, is_none: bool, } impl From for ComponentDetails { fn from(value: f32) -> Self { Self { value, is_none: false, } } } impl From for ComponentDetails { fn from(value: u8) -> Self { Self { value: value as f32 / 255.0, is_none: false, } } } impl From> for ComponentDetails { fn from(value: Option) -> Self { if let Some(value) = value { Self { value, is_none: false, } } else { Self { value: 0.0, is_none: true, } } } } impl From> for ComponentDetails { fn from(value: ColorComponent) -> Self { if let ColorComponent::Value(value) = value { Self { value, is_none: false, } } else { Self { value: 0.0, is_none: true, } } } } impl AbsoluteColor { /// A fully transparent color in the legacy syntax. pub const TRANSPARENT_BLACK: Self = Self { components: ColorComponents(0.0, 0.0, 0.0), alpha: 0.0, color_space: ColorSpace::Srgb, flags: ColorFlags::IS_LEGACY_SRGB, }; /// An opaque black color in the legacy syntax. pub const BLACK: Self = Self { components: ColorComponents(0.0, 0.0, 0.0), alpha: 1.0, color_space: ColorSpace::Srgb, flags: ColorFlags::IS_LEGACY_SRGB, }; /// An opaque white color in the legacy syntax. pub const WHITE: Self = Self { components: ColorComponents(1.0, 1.0, 1.0), alpha: 1.0, color_space: ColorSpace::Srgb, flags: ColorFlags::IS_LEGACY_SRGB, }; /// Create a new [`AbsoluteColor`] with the given [`ColorSpace`] and /// components. pub fn new( color_space: ColorSpace, c1: impl Into, c2: impl Into, c3: impl Into, alpha: impl Into, ) -> Self { let mut flags = ColorFlags::empty(); macro_rules! cd { ($c:expr,$flag:expr) => {{ let component_details = $c.into(); if component_details.is_none { flags |= $flag; } component_details.value }}; } let mut components = ColorComponents( cd!(c1, ColorFlags::C0_IS_NONE), cd!(c2, ColorFlags::C1_IS_NONE), cd!(c3, ColorFlags::C2_IS_NONE), ); let alpha = cd!(alpha, ColorFlags::ALPHA_IS_NONE); // Lightness for Lab and Lch is clamped to [0..100]. if matches!(color_space, ColorSpace::Lab | ColorSpace::Lch) { components.0 = components.0.clamp(0.0, 100.0); } // Lightness for Oklab and Oklch is clamped to [0..1]. if matches!(color_space, ColorSpace::Oklab | ColorSpace::Oklch) { components.0 = components.0.clamp(0.0, 1.0); } // Chroma must not be less than 0. if matches!(color_space, ColorSpace::Lch | ColorSpace::Oklch) { components.1 = components.1.max(0.0); } // Alpha is always clamped to [0..1]. let alpha = alpha.clamp(0.0, 1.0); Self { components, alpha, color_space, flags, } } /// Convert this color into the sRGB color space and set it to the legacy /// syntax. #[inline] #[must_use] pub fn into_srgb_legacy(self) -> Self { let mut result = if !matches!(self.color_space, ColorSpace::Srgb) { self.to_color_space(ColorSpace::Srgb) } else { self }; // Explicitly set the flags to IS_LEGACY_SRGB only to clear out the // *_IS_NONE flags, because the legacy syntax doesn't allow "none". result.flags = ColorFlags::IS_LEGACY_SRGB; result } /// Create a new [`AbsoluteColor`] from rgba legacy syntax values in the sRGB color space. pub fn srgb_legacy(red: u8, green: u8, blue: u8, alpha: f32) -> Self { let mut result = Self::new(ColorSpace::Srgb, red, green, blue, alpha); result.flags = ColorFlags::IS_LEGACY_SRGB; result } /// Return all the components of the color in an array. (Includes alpha) #[inline] pub fn raw_components(&self) -> &[f32; 4] { unsafe { color_components_as!(self, [f32; 4]) } } /// Returns true if this color is in the legacy color syntax. #[inline] pub fn is_legacy_syntax(&self) -> bool { // rgb(), rgba(), hsl(), hsla(), hwb(), hwba() match self.color_space { ColorSpace::Srgb => self.flags.contains(ColorFlags::IS_LEGACY_SRGB), ColorSpace::Hsl | ColorSpace::Hwb => true, _ => false, } } /// Returns true if this color is fully transparent. #[inline] pub fn is_transparent(&self) -> bool { self.flags.contains(ColorFlags::ALPHA_IS_NONE) || self.alpha == 0.0 } /// Return an optional first component. #[inline] pub fn c0(&self) -> Option { if self.flags.contains(ColorFlags::C0_IS_NONE) { None } else { Some(self.components.0) } } /// Return an optional second component. #[inline] pub fn c1(&self) -> Option { if self.flags.contains(ColorFlags::C1_IS_NONE) { None } else { Some(self.components.1) } } /// Return an optional second component. #[inline] pub fn c2(&self) -> Option { if self.flags.contains(ColorFlags::C2_IS_NONE) { None } else { Some(self.components.2) } } /// Return an optional alpha component. #[inline] pub fn alpha(&self) -> Option { if self.flags.contains(ColorFlags::ALPHA_IS_NONE) { None } else { Some(self.alpha) } } /// Convert this color to the specified color space. pub fn to_color_space(&self, color_space: ColorSpace) -> Self { use ColorSpace::*; if self.color_space == color_space { return self.clone(); } // Conversion functions doesn't handle NAN component values, so they are // converted to 0.0. They do however need to know if a component is // missing, so we use NAN as the marker for that. macro_rules! missing_to_nan { ($c:expr) => {{ if let Some(v) = $c { crate::values::normalize(v) } else { f32::NAN } }}; } let components = ColorComponents( missing_to_nan!(self.c0()), missing_to_nan!(self.c1()), missing_to_nan!(self.c2()), ); let result = match (self.color_space, color_space) { // We have simplified conversions that do not need to convert to XYZ // first. This improves performance, because it skips at least 2 // matrix multiplications and reduces float rounding errors. (Srgb, Hsl) => convert::rgb_to_hsl(&components), (Srgb, Hwb) => convert::rgb_to_hwb(&components), (Hsl, Srgb) => convert::hsl_to_rgb(&components), (Hwb, Srgb) => convert::hwb_to_rgb(&components), (Lab, Lch) | (Oklab, Oklch) => convert::orthogonal_to_polar(&components), (Lch, Lab) | (Oklch, Oklab) => convert::polar_to_orthogonal(&components), // All other conversions need to convert to XYZ first. _ => { let (xyz, white_point) = match self.color_space { Lab => convert::to_xyz::(&components), Lch => convert::to_xyz::(&components), Oklab => convert::to_xyz::(&components), Oklch => convert::to_xyz::(&components), Srgb => convert::to_xyz::(&components), Hsl => convert::to_xyz::(&components), Hwb => convert::to_xyz::(&components), SrgbLinear => convert::to_xyz::(&components), DisplayP3 => convert::to_xyz::(&components), A98Rgb => convert::to_xyz::(&components), ProphotoRgb => convert::to_xyz::(&components), Rec2020 => convert::to_xyz::(&components), XyzD50 => convert::to_xyz::(&components), XyzD65 => convert::to_xyz::(&components), }; match color_space { Lab => convert::from_xyz::(&xyz, white_point), Lch => convert::from_xyz::(&xyz, white_point), Oklab => convert::from_xyz::(&xyz, white_point), Oklch => convert::from_xyz::(&xyz, white_point), Srgb => convert::from_xyz::(&xyz, white_point), Hsl => convert::from_xyz::(&xyz, white_point), Hwb => convert::from_xyz::(&xyz, white_point), SrgbLinear => convert::from_xyz::(&xyz, white_point), DisplayP3 => convert::from_xyz::(&xyz, white_point), A98Rgb => convert::from_xyz::(&xyz, white_point), ProphotoRgb => convert::from_xyz::(&xyz, white_point), Rec2020 => convert::from_xyz::(&xyz, white_point), XyzD50 => convert::from_xyz::(&xyz, white_point), XyzD65 => convert::from_xyz::(&xyz, white_point), } }, }; // A NAN value coming from a conversion function means the the component // is missing, so we convert it to None. macro_rules! nan_to_missing { ($v:expr) => {{ if $v.is_nan() { None } else { Some($v) } }}; } Self::new( color_space, nan_to_missing!(result.0), nan_to_missing!(result.1), nan_to_missing!(result.2), self.alpha(), ) } } impl From for ColorSpace { fn from(value: PredefinedColorSpace) -> Self { match value { PredefinedColorSpace::Srgb => ColorSpace::Srgb, PredefinedColorSpace::SrgbLinear => ColorSpace::SrgbLinear, PredefinedColorSpace::DisplayP3 => ColorSpace::DisplayP3, PredefinedColorSpace::A98Rgb => ColorSpace::A98Rgb, PredefinedColorSpace::ProphotoRgb => ColorSpace::ProphotoRgb, PredefinedColorSpace::Rec2020 => ColorSpace::Rec2020, PredefinedColorSpace::XyzD50 => ColorSpace::XyzD50, PredefinedColorSpace::XyzD65 => ColorSpace::XyzD65, } } }