summaryrefslogtreecommitdiffstats
path: root/servo/components/style/color/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/color/mod.rs')
-rw-r--r--servo/components/style/color/mod.rs613
1 files changed, 613 insertions, 0 deletions
diff --git a/servo/components/style/color/mod.rs b/servo/components/style/color/mod.rs
new file mode 100644
index 0000000000..797a1cb00f
--- /dev/null
+++ b/servo/components/style/color/mod.rs
@@ -0,0 +1,613 @@
+/* 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
+pub mod convert;
+pub mod mix;
+pub mod parsing;
+
+use cssparser::color::PredefinedColorSpace;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// 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 `<rectangular-color-space>`.
+ #[inline]
+ pub fn is_rectangular(&self) -> bool {
+ !self.is_polar()
+ }
+
+ /// Returns whether this is a `<polar-color-space>`.
+ #[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<usize> {
+ 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::<AbsoluteColor>() >= std::mem::size_of::<$t>());
+ const_assert_eq!(
+ std::mem::align_of::<AbsoluteColor>(),
+ 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<f32> for ComponentDetails {
+ fn from(value: f32) -> Self {
+ Self {
+ value,
+ is_none: false,
+ }
+ }
+}
+
+impl From<u8> for ComponentDetails {
+ fn from(value: u8) -> Self {
+ Self {
+ value: value as f32 / 255.0,
+ is_none: false,
+ }
+ }
+}
+
+impl From<Option<f32>> for ComponentDetails {
+ fn from(value: Option<f32>) -> Self {
+ if let Some(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<ComponentDetails>,
+ c2: impl Into<ComponentDetails>,
+ c3: impl Into<ComponentDetails>,
+ alpha: impl Into<ComponentDetails>,
+ ) -> 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<f32> {
+ 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<f32> {
+ 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<f32> {
+ 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<f32> {
+ 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::<convert::Lab>(&components),
+ Lch => convert::to_xyz::<convert::Lch>(&components),
+ Oklab => convert::to_xyz::<convert::Oklab>(&components),
+ Oklch => convert::to_xyz::<convert::Oklch>(&components),
+ Srgb => convert::to_xyz::<convert::Srgb>(&components),
+ Hsl => convert::to_xyz::<convert::Hsl>(&components),
+ Hwb => convert::to_xyz::<convert::Hwb>(&components),
+ SrgbLinear => convert::to_xyz::<convert::SrgbLinear>(&components),
+ DisplayP3 => convert::to_xyz::<convert::DisplayP3>(&components),
+ A98Rgb => convert::to_xyz::<convert::A98Rgb>(&components),
+ ProphotoRgb => convert::to_xyz::<convert::ProphotoRgb>(&components),
+ Rec2020 => convert::to_xyz::<convert::Rec2020>(&components),
+ XyzD50 => convert::to_xyz::<convert::XyzD50>(&components),
+ XyzD65 => convert::to_xyz::<convert::XyzD65>(&components),
+ };
+
+ match color_space {
+ Lab => convert::from_xyz::<convert::Lab>(&xyz, white_point),
+ Lch => convert::from_xyz::<convert::Lch>(&xyz, white_point),
+ Oklab => convert::from_xyz::<convert::Oklab>(&xyz, white_point),
+ Oklch => convert::from_xyz::<convert::Oklch>(&xyz, white_point),
+ Srgb => convert::from_xyz::<convert::Srgb>(&xyz, white_point),
+ Hsl => convert::from_xyz::<convert::Hsl>(&xyz, white_point),
+ Hwb => convert::from_xyz::<convert::Hwb>(&xyz, white_point),
+ SrgbLinear => convert::from_xyz::<convert::SrgbLinear>(&xyz, white_point),
+ DisplayP3 => convert::from_xyz::<convert::DisplayP3>(&xyz, white_point),
+ A98Rgb => convert::from_xyz::<convert::A98Rgb>(&xyz, white_point),
+ ProphotoRgb => convert::from_xyz::<convert::ProphotoRgb>(&xyz, white_point),
+ Rec2020 => convert::from_xyz::<convert::Rec2020>(&xyz, white_point),
+ XyzD50 => convert::from_xyz::<convert::XyzD50>(&xyz, white_point),
+ XyzD65 => convert::from_xyz::<convert::XyzD65>(&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<PredefinedColorSpace> 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,
+ }
+ }
+}
+
+impl ToCss for AbsoluteColor {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match self.color_space {
+ ColorSpace::Srgb if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) => {
+ // The "none" keyword is not supported in the rgb/rgba legacy syntax.
+ cssparser::ToCss::to_css(
+ &parsing::RgbaLegacy::from_floats(
+ self.components.0,
+ self.components.1,
+ self.components.2,
+ self.alpha,
+ ),
+ dest,
+ )
+ },
+ ColorSpace::Hsl | ColorSpace::Hwb => self.into_srgb_legacy().to_css(dest),
+ ColorSpace::Lab => cssparser::ToCss::to_css(
+ &parsing::Lab::new(self.c0(), self.c1(), self.c2(), self.alpha()),
+ dest,
+ ),
+ ColorSpace::Lch => cssparser::ToCss::to_css(
+ &parsing::Lch::new(self.c0(), self.c1(), self.c2(), self.alpha()),
+ dest,
+ ),
+ ColorSpace::Oklab => cssparser::ToCss::to_css(
+ &parsing::Oklab::new(self.c0(), self.c1(), self.c2(), self.alpha()),
+ dest,
+ ),
+ ColorSpace::Oklch => cssparser::ToCss::to_css(
+ &parsing::Oklch::new(self.c0(), self.c1(), self.c2(), self.alpha()),
+ dest,
+ ),
+ _ => {
+ let color_space = match self.color_space {
+ ColorSpace::Srgb => {
+ debug_assert!(
+ !self.flags.contains(ColorFlags::IS_LEGACY_SRGB),
+ "legacy srgb is not a color function"
+ );
+ PredefinedColorSpace::Srgb
+ },
+ ColorSpace::SrgbLinear => PredefinedColorSpace::SrgbLinear,
+ ColorSpace::DisplayP3 => PredefinedColorSpace::DisplayP3,
+ ColorSpace::A98Rgb => PredefinedColorSpace::A98Rgb,
+ ColorSpace::ProphotoRgb => PredefinedColorSpace::ProphotoRgb,
+ ColorSpace::Rec2020 => PredefinedColorSpace::Rec2020,
+ ColorSpace::XyzD50 => PredefinedColorSpace::XyzD50,
+ ColorSpace::XyzD65 => PredefinedColorSpace::XyzD65,
+
+ _ => {
+ unreachable!("other color spaces do not support color() syntax")
+ },
+ };
+
+ let color_function = parsing::ColorFunction {
+ color_space,
+ c1: self.c0(),
+ c2: self.c1(),
+ c3: self.c2(),
+ alpha: self.alpha(),
+ };
+ let color = parsing::Color::ColorFunction(color_function);
+ cssparser::ToCss::to_css(&color, dest)
+ },
+ }
+ }
+}