summaryrefslogtreecommitdiffstats
path: root/servo/components/style/color
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/color')
-rw-r--r--servo/components/style/color/convert.rs902
-rw-r--r--servo/components/style/color/mix.rs558
-rw-r--r--servo/components/style/color/mod.rs613
-rw-r--r--servo/components/style/color/parsing.rs1246
4 files changed, 3319 insertions, 0 deletions
diff --git a/servo/components/style/color/convert.rs b/servo/components/style/color/convert.rs
new file mode 100644
index 0000000000..a6274db39a
--- /dev/null
+++ b/servo/components/style/color/convert.rs
@@ -0,0 +1,902 @@
+/* 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 conversion algorithms.
+//!
+//! Algorithms, matrices and constants are from the [color-4] specification,
+//! unless otherwise specified:
+//!
+//! https://drafts.csswg.org/css-color-4/#color-conversion-code
+//!
+//! NOTE: Matrices has to be transposed from the examples in the spec for use
+//! with the `euclid` library.
+
+use crate::color::ColorComponents;
+use crate::values::normalize;
+
+type Transform = euclid::default::Transform3D<f32>;
+type Vector = euclid::default::Vector3D<f32>;
+
+/// Normalize hue into [0, 360).
+#[inline]
+pub fn normalize_hue(hue: f32) -> f32 {
+ hue - 360. * (hue / 360.).floor()
+}
+
+/// Calculate the hue from RGB components and return it along with the min and
+/// max RGB values.
+#[inline]
+fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) {
+ let max = red.max(green).max(blue);
+ let min = red.min(green).min(blue);
+
+ let delta = max - min;
+
+ let hue = if delta != 0.0 {
+ 60.0 * if max == red {
+ (green - blue) / delta + if green < blue { 6.0 } else { 0.0 }
+ } else if max == green {
+ (blue - red) / delta + 2.0
+ } else {
+ (red - green) / delta + 4.0
+ }
+ } else {
+ f32::NAN
+ };
+
+ (hue, min, max)
+}
+
+/// Convert from HSL notation to RGB notation.
+/// https://drafts.csswg.org/css-color-4/#hsl-to-rgb
+#[inline]
+pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents {
+ fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 {
+ let hue = normalize_hue(hue);
+
+ if hue * 6.0 < 360.0 {
+ t1 + (t2 - t1) * hue / 60.0
+ } else if hue * 2.0 < 360.0 {
+ t2
+ } else if hue * 3.0 < 720.0 {
+ t1 + (t2 - t1) * (240.0 - hue) / 60.0
+ } else {
+ t1
+ }
+ }
+
+ // Convert missing components to 0.0.
+ let ColorComponents(hue, saturation, lightness) = from.map(normalize);
+ let saturation = saturation / 100.0;
+ let lightness = lightness / 100.0;
+
+ let t2 = if lightness <= 0.5 {
+ lightness * (saturation + 1.0)
+ } else {
+ lightness + saturation - lightness * saturation
+ };
+ let t1 = lightness * 2.0 - t2;
+
+ ColorComponents(
+ hue_to_rgb(t1, t2, hue + 120.0),
+ hue_to_rgb(t1, t2, hue),
+ hue_to_rgb(t1, t2, hue - 120.0),
+ )
+}
+
+/// Convert from RGB notation to HSL notation.
+/// https://drafts.csswg.org/css-color-4/#rgb-to-hsl
+pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(red, green, blue) = *from;
+
+ let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
+
+ let lightness = (min + max) / 2.0;
+ let delta = max - min;
+
+ let saturation = if delta != 0.0 {
+ if lightness == 0.0 || lightness == 1.0 {
+ 0.0
+ } else {
+ (max - lightness) / lightness.min(1.0 - lightness)
+ }
+ } else {
+ 0.0
+ };
+
+ ColorComponents(hue, saturation * 100.0, lightness * 100.0)
+}
+
+/// Convert from HWB notation to RGB notation.
+/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb
+#[inline]
+pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents {
+ // Convert missing components to 0.0.
+ let ColorComponents(hue, whiteness, blackness) = from.map(normalize);
+
+ let whiteness = whiteness / 100.0;
+ let blackness = blackness / 100.0;
+
+ if whiteness + blackness >= 1.0 {
+ let gray = whiteness / (whiteness + blackness);
+ return ColorComponents(gray, gray, gray);
+ }
+
+ let x = 1.0 - whiteness - blackness;
+ hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness)
+}
+
+/// Convert from RGB notation to HWB notation.
+/// https://drafts.csswg.org/css-color-4/#rgb-to-hwb
+#[inline]
+pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(red, green, blue) = *from;
+
+ let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
+
+ let whiteness = min;
+ let blackness = 1.0 - max;
+
+ ColorComponents(hue, whiteness * 100.0, blackness * 100.0)
+}
+
+/// Convert from the rectangular orthogonal to the cylindrical polar coordinate
+/// system. This is used to convert (ok)lab to (ok)lch.
+/// <https://drafts.csswg.org/css-color-4/#lab-to-lch>
+#[inline]
+pub fn orthogonal_to_polar(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(lightness, a, b) = *from;
+
+ let chroma = (a * a + b * b).sqrt();
+
+ // Very small chroma values make the hue component powerless.
+ let hue = if chroma.abs() < 1.0e-6 {
+ f32::NAN
+ } else {
+ normalize_hue(b.atan2(a).to_degrees())
+ };
+
+ ColorComponents(lightness, chroma, hue)
+}
+
+/// Convert from the cylindrical polar to the rectangular orthogonal coordinate
+/// system. This is used to convert (ok)lch to (ok)lab.
+/// <https://drafts.csswg.org/css-color-4/#lch-to-lab>
+#[inline]
+pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(lightness, chroma, hue) = *from;
+
+ // A missing hue component results in an achromatic color.
+ if hue.is_nan() {
+ return ColorComponents(lightness, 0.0, 0.0);
+ }
+
+ let hue = hue.to_radians();
+ let a = chroma * hue.cos();
+ let b = chroma * hue.sin();
+
+ ColorComponents(lightness, a, b)
+}
+
+#[inline]
+fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents {
+ let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2));
+ ColorComponents(result.x, result.y, result.z)
+}
+
+fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents {
+ #[rustfmt::skip]
+ const MAT: Transform = Transform::new(
+ 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0,
+ 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0,
+ -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ transform(from, &MAT)
+}
+
+fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents {
+ #[rustfmt::skip]
+ const MAT: Transform = Transform::new(
+ 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0,
+ -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0,
+ 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ transform(from, &MAT)
+}
+
+/// A reference white that is used during color conversion.
+pub enum WhitePoint {
+ /// D50 white reference.
+ D50,
+ /// D65 white reference.
+ D65,
+}
+
+impl WhitePoint {
+ const fn values(&self) -> ColorComponents {
+ // <https://drafts.csswg.org/css-color-4/#color-conversion-code>
+ match self {
+ // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585]
+ WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602),
+ // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290]
+ WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784),
+ }
+ }
+}
+
+fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) {
+ match (from, to) {
+ (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components),
+ (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components),
+ _ => {},
+ }
+}
+
+/// A trait that allows conversion of color spaces to and from XYZ coordinate
+/// space with a specified white point.
+///
+/// Allows following the specified method of converting between color spaces:
+/// - Convert to values to sRGB linear light.
+/// - Convert to XYZ coordinate space.
+/// - Adjust white point to target white point.
+/// - Convert to sRGB linear light in target color space.
+/// - Convert to sRGB gamma encoded in target color space.
+///
+/// https://drafts.csswg.org/css-color-4/#color-conversion
+pub trait ColorSpaceConversion {
+ /// The white point that the implementer is represented in.
+ const WHITE_POINT: WhitePoint;
+
+ /// Convert the components from sRGB gamma encoded values to sRGB linear
+ /// light values.
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents;
+
+ /// Convert the components from sRGB linear light values to XYZ coordinate
+ /// space.
+ fn to_xyz(from: &ColorComponents) -> ColorComponents;
+
+ /// Convert the components from XYZ coordinate space to sRGB linear light
+ /// values.
+ fn from_xyz(from: &ColorComponents) -> ColorComponents;
+
+ /// Convert the components from sRGB linear light values to sRGB gamma
+ /// encoded values.
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents;
+}
+
+/// Convert the color components from the specified color space to XYZ and
+/// return the components and the white point they are in.
+pub fn to_xyz<From: ColorSpaceConversion>(from: &ColorComponents) -> (ColorComponents, WhitePoint) {
+ // Convert the color components where in-gamut values are in the range
+ // [0 - 1] to linear light (un-companded) form.
+ let result = From::to_linear_light(from);
+
+ // Convert the color components from the source color space to XYZ.
+ (From::to_xyz(&result), From::WHITE_POINT)
+}
+
+/// Convert the color components from XYZ at the given white point to the
+/// specified color space.
+pub fn from_xyz<To: ColorSpaceConversion>(
+ from: &ColorComponents,
+ white_point: WhitePoint,
+) -> ColorComponents {
+ let mut xyz = from.clone();
+
+ // Convert the white point if needed.
+ convert_white_point(white_point, To::WHITE_POINT, &mut xyz);
+
+ // Convert the color from XYZ to the target color space.
+ let result = To::from_xyz(&xyz);
+
+ // Convert the color components of linear-light values in the range
+ // [0 - 1] to a gamma corrected form.
+ To::to_gamma_encoded(&result)
+}
+
+/// The sRGB color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-sRGB
+pub struct Srgb;
+
+impl Srgb {
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0,
+ 0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0,
+ 0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0,
+ -1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0,
+ -0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for Srgb {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|value| {
+ let abs = value.abs();
+
+ if abs < 0.04045 {
+ value / 12.92
+ } else {
+ value.signum() * ((abs + 0.055) / 1.055).powf(2.4)
+ }
+ })
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|value| {
+ let abs = value.abs();
+
+ if abs > 0.0031308 {
+ value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055)
+ } else {
+ 12.92 * value
+ }
+ })
+ }
+}
+
+/// Color specified with hue, saturation and lightness components.
+pub struct Hsl;
+
+impl ColorSpaceConversion for Hsl {
+ const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_linear_light(&hsl_to_rgb(from))
+ }
+
+ #[inline]
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_xyz(from)
+ }
+
+ #[inline]
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::from_xyz(from)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ rgb_to_hsl(&Srgb::to_gamma_encoded(from))
+ }
+}
+
+/// Color specified with hue, whiteness and blackness components.
+pub struct Hwb;
+
+impl ColorSpaceConversion for Hwb {
+ const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_linear_light(&hwb_to_rgb(from))
+ }
+
+ #[inline]
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_xyz(from)
+ }
+
+ #[inline]
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::from_xyz(from)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ rgb_to_hwb(&Srgb::to_gamma_encoded(from))
+ }
+}
+
+/// The same as sRGB color space, except the transfer function is linear light.
+/// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear
+pub struct SrgbLinear;
+
+impl ColorSpaceConversion for SrgbLinear {
+ const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // Already in linear light form.
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_xyz(from)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ Srgb::from_xyz(from)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // Stay in linear light form.
+ from.clone()
+ }
+}
+
+/// The Display-P3 color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-display-p3
+pub struct DisplayP3;
+
+impl DisplayP3 {
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.48657094864821626, 0.22897456406974884, 0.0, 0.0,
+ 0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0,
+ 0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0,
+ -0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0,
+ -0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for DisplayP3 {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_linear_light(from)
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ Srgb::to_gamma_encoded(from)
+ }
+}
+
+/// The a98-rgb color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb
+pub struct A98Rgb;
+
+impl A98Rgb {
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0,
+ 0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0,
+ 0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0,
+ -0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0,
+ -0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for A98Rgb {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|v| v.signum() * v.abs().powf(2.19921875))
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ .map(|v| v.signum() * v.abs().powf(0.4547069271758437))
+ }
+}
+
+/// The ProPhoto RGB color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb
+pub struct ProphotoRgb;
+
+impl ProphotoRgb {
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.7977604896723027, 0.2880711282292934, 0.0, 0.0,
+ 0.13518583717574031, 0.7118432178101014, 0.0, 0.0,
+ 0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 1.3457989731028281, -0.5446224939028347, 0.0, 0.0,
+ -0.25558010007997534, 1.5082327413132781, 0.0, 0.0,
+ -0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for ProphotoRgb {
+ const WHITE_POINT: WhitePoint = WhitePoint::D50;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|value| {
+ const ET2: f32 = 16.0 / 512.0;
+
+ let abs = value.abs();
+
+ if abs <= ET2 {
+ value / 16.0
+ } else {
+ value.signum() * abs.powf(1.8)
+ }
+ })
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ const ET: f32 = 1.0 / 512.0;
+
+ from.clone().map(|v| {
+ let abs = v.abs();
+ if abs >= ET {
+ v.signum() * abs.powf(1.0 / 1.8)
+ } else {
+ 16.0 * v
+ }
+ })
+ }
+}
+
+/// The Rec.2020 color space.
+/// https://drafts.csswg.org/css-color-4/#predefined-rec2020
+pub struct Rec2020;
+
+impl Rec2020 {
+ const ALPHA: f32 = 1.09929682680944;
+ const BETA: f32 = 0.018053968510807;
+
+ #[rustfmt::skip]
+ const TO_XYZ: Transform = Transform::new(
+ 0.6369580483012913, 0.26270021201126703, 0.0, 0.0,
+ 0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0,
+ 0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const FROM_XYZ: Transform = Transform::new(
+ 1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0,
+ -0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0,
+ -0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for Rec2020 {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|value| {
+ let abs = value.abs();
+
+ if abs < Self::BETA * 4.5 {
+ value / 4.5
+ } else {
+ value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45)
+ }
+ })
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ transform(from, &Self::FROM_XYZ)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone().map(|v| {
+ let abs = v.abs();
+
+ if abs > Self::BETA {
+ v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0))
+ } else {
+ 4.5 * v
+ }
+ })
+ }
+}
+
+/// A color in the XYZ coordinate space with a D50 white reference.
+/// https://drafts.csswg.org/css-color-4/#predefined-xyz
+pub struct XyzD50;
+
+impl ColorSpaceConversion for XyzD50 {
+ const WHITE_POINT: WhitePoint = WhitePoint::D50;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+}
+
+/// A color in the XYZ coordinate space with a D65 white reference.
+/// https://drafts.csswg.org/css-color-4/#predefined-xyz
+pub struct XyzD65;
+
+impl ColorSpaceConversion for XyzD65 {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ from.clone()
+ }
+}
+
+/// The Lab color space.
+/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch
+pub struct Lab;
+
+impl Lab {
+ const KAPPA: f32 = 24389.0 / 27.0;
+ const EPSILON: f32 = 216.0 / 24389.0;
+}
+
+impl ColorSpaceConversion for Lab {
+ const WHITE_POINT: WhitePoint = WhitePoint::D50;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+
+ /// Convert a CIELAB color to XYZ as specified in [1] and [2].
+ ///
+ /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined
+ /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ let ColorComponents(lightness, a, b) = *from;
+
+ let f1 = (lightness + 16.0) / 116.0;
+ let f0 = f1 + a / 500.0;
+ let f2 = f1 - b / 200.0;
+
+ let f0_cubed = f0 * f0 * f0;
+ let x = if f0_cubed > Self::EPSILON {
+ f0_cubed
+ } else {
+ (116.0 * f0 - 16.0) / Self::KAPPA
+ };
+
+ let y = if lightness > Self::KAPPA * Self::EPSILON {
+ let v = (lightness + 16.0) / 116.0;
+ v * v * v
+ } else {
+ lightness / Self::KAPPA
+ };
+
+ let f2_cubed = f2 * f2 * f2;
+ let z = if f2_cubed > Self::EPSILON {
+ f2_cubed
+ } else {
+ (116.0 * f2 - 16.0) / Self::KAPPA
+ };
+
+ ColorComponents(x, y, z) * Self::WHITE_POINT.values()
+ }
+
+ /// Convert an XYZ color to LAB as specified in [1] and [2].
+ ///
+ /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab
+ /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ let adapted = *from / Self::WHITE_POINT.values();
+
+ // 4. Convert D50-adapted XYZ to Lab.
+ let ColorComponents(f0, f1, f2) = adapted.map(|v| {
+ if v > Self::EPSILON {
+ v.cbrt()
+ } else {
+ (Self::KAPPA * v + 16.0) / 116.0
+ }
+ });
+
+ let lightness = 116.0 * f1 - 16.0;
+ let a = 500.0 * (f0 - f1);
+ let b = 200.0 * (f1 - f2);
+
+ ColorComponents(lightness, a, b)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+}
+
+/// The Lch color space.
+/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch
+pub struct Lch;
+
+impl ColorSpaceConversion for Lch {
+ const WHITE_POINT: WhitePoint = Lab::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ // Convert LCH to Lab first.
+ let lab = polar_to_orthogonal(from);
+
+ // Then convert the Lab to XYZ.
+ Lab::to_xyz(&lab)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ // First convert the XYZ to LAB.
+ let lab = Lab::from_xyz(&from);
+
+ // Then convert the Lab to LCH.
+ orthogonal_to_polar(&lab)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+}
+
+/// The Oklab color space.
+/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
+pub struct Oklab;
+
+impl Oklab {
+ #[rustfmt::skip]
+ const XYZ_TO_LMS: Transform = Transform::new(
+ 0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0,
+ 0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0,
+ -0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const LMS_TO_OKLAB: Transform = Transform::new(
+ 0.2104542553, 1.9779984951, 0.0259040371, 0.0,
+ 0.7936177850, -2.4285922050, 0.7827717662, 0.0,
+ -0.0040720468, 0.4505937099, -0.8086757660, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const LMS_TO_XYZ: Transform = Transform::new(
+ 1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0,
+ -0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0,
+ 0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+
+ #[rustfmt::skip]
+ const OKLAB_TO_LMS: Transform = Transform::new(
+ 0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0,
+ 0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0,
+ 0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0,
+ 0.0, 0.0, 0.0, 1.0,
+ );
+}
+
+impl ColorSpaceConversion for Oklab {
+ const WHITE_POINT: WhitePoint = WhitePoint::D65;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ let lms = transform(&from, &Self::OKLAB_TO_LMS);
+ let lms = lms.map(|v| v * v * v);
+ transform(&lms, &Self::LMS_TO_XYZ)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ let lms = transform(&from, &Self::XYZ_TO_LMS);
+ let lms = lms.map(|v| v.cbrt());
+ transform(&lms, &Self::LMS_TO_OKLAB)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+}
+
+/// The Oklch color space.
+/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
+pub struct Oklch;
+
+impl ColorSpaceConversion for Oklch {
+ const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT;
+
+ fn to_linear_light(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+
+ fn to_xyz(from: &ColorComponents) -> ColorComponents {
+ // First convert OkLCH to Oklab.
+ let oklab = polar_to_orthogonal(from);
+
+ // Then convert Oklab to XYZ.
+ Oklab::to_xyz(&oklab)
+ }
+
+ fn from_xyz(from: &ColorComponents) -> ColorComponents {
+ // First convert XYZ to Oklab.
+ let lab = Oklab::from_xyz(&from);
+
+ // Then convert Oklab to OkLCH.
+ orthogonal_to_polar(&lab)
+ }
+
+ fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
+ // No need for conversion.
+ from.clone()
+ }
+}
diff --git a/servo/components/style/color/mix.rs b/servo/components/style/color/mix.rs
new file mode 100644
index 0000000000..bcc4628575
--- /dev/null
+++ b/servo/components/style/color/mix.rs
@@ -0,0 +1,558 @@
+/* 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 mixing/interpolation.
+
+use super::{AbsoluteColor, ColorFlags, ColorSpace};
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::color::ColorMixFlags;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A hue-interpolation-method as defined in [1].
+///
+/// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum HueInterpolationMethod {
+ /// https://drafts.csswg.org/css-color-4/#shorter
+ Shorter,
+ /// https://drafts.csswg.org/css-color-4/#longer
+ Longer,
+ /// https://drafts.csswg.org/css-color-4/#increasing
+ Increasing,
+ /// https://drafts.csswg.org/css-color-4/#decreasing
+ Decreasing,
+ /// https://drafts.csswg.org/css-color-4/#specified
+ Specified,
+}
+
+/// https://drafts.csswg.org/css-color-4/#color-interpolation-method
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToShmem,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+)]
+#[repr(C)]
+pub struct ColorInterpolationMethod {
+ /// The color-space the interpolation should be done in.
+ pub space: ColorSpace,
+ /// The hue interpolation method.
+ pub hue: HueInterpolationMethod,
+}
+
+impl ColorInterpolationMethod {
+ /// Returns the srgb interpolation method.
+ pub const fn srgb() -> Self {
+ Self {
+ space: ColorSpace::Srgb,
+ hue: HueInterpolationMethod::Shorter,
+ }
+ }
+
+ /// Return the oklab interpolation method used for default color
+ /// interpolcation.
+ pub const fn oklab() -> Self {
+ Self {
+ space: ColorSpace::Oklab,
+ hue: HueInterpolationMethod::Shorter,
+ }
+ }
+
+ /// Decides the best method for interpolating between the given colors.
+ /// https://drafts.csswg.org/css-color-4/#interpolation-space
+ pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self {
+ // The preferred color space to use for interpolating colors is Oklab.
+ // However, if either of the colors are in legacy rgb(), hsl() or hwb(),
+ // then interpolation is done in sRGB.
+ if !left.is_legacy_syntax() || !right.is_legacy_syntax() {
+ Self::oklab()
+ } else {
+ Self::srgb()
+ }
+ }
+}
+
+impl Parse for ColorInterpolationMethod {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_ident_matching("in")?;
+ let space = ColorSpace::parse(input)?;
+ // https://drafts.csswg.org/css-color-4/#hue-interpolation
+ // Unless otherwise specified, if no specific hue interpolation
+ // algorithm is selected by the host syntax, the default is shorter.
+ let hue = if space.is_polar() {
+ input
+ .try_parse(|input| -> Result<_, ParseError<'i>> {
+ let hue = HueInterpolationMethod::parse(input)?;
+ input.expect_ident_matching("hue")?;
+ Ok(hue)
+ })
+ .unwrap_or(HueInterpolationMethod::Shorter)
+ } else {
+ HueInterpolationMethod::Shorter
+ };
+ Ok(Self { space, hue })
+ }
+}
+
+impl ToCss for ColorInterpolationMethod {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("in ")?;
+ self.space.to_css(dest)?;
+ if self.hue != HueInterpolationMethod::Shorter {
+ dest.write_char(' ')?;
+ self.hue.to_css(dest)?;
+ dest.write_str(" hue")?;
+ }
+ Ok(())
+ }
+}
+
+/// Mix two colors into one.
+pub fn mix(
+ interpolation: ColorInterpolationMethod,
+ left_color: &AbsoluteColor,
+ mut left_weight: f32,
+ right_color: &AbsoluteColor,
+ mut right_weight: f32,
+ flags: ColorMixFlags,
+) -> AbsoluteColor {
+ // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm
+ let mut alpha_multiplier = 1.0;
+ if flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS) {
+ let sum = left_weight + right_weight;
+ if sum != 1.0 {
+ let scale = 1.0 / sum;
+ left_weight *= scale;
+ right_weight *= scale;
+ if sum < 1.0 {
+ alpha_multiplier = sum;
+ }
+ }
+ }
+
+ let result = mix_in(
+ interpolation.space,
+ left_color,
+ left_weight,
+ right_color,
+ right_weight,
+ interpolation.hue,
+ alpha_multiplier,
+ );
+
+ if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
+ // If the result *MUST* be in modern syntax, then make sure it is in a
+ // color space that allows the modern syntax. So hsl and hwb will be
+ // converted to srgb.
+ if result.is_legacy_syntax() {
+ result.to_color_space(ColorSpace::Srgb)
+ } else {
+ result
+ }
+ } else if left_color.is_legacy_syntax() && right_color.is_legacy_syntax() {
+ // If both sides of the mix is legacy then convert the result back into
+ // legacy.
+ result.into_srgb_legacy()
+ } else {
+ result
+ }
+}
+
+/// What the outcome of each component should be in a mix result.
+#[derive(Clone, Copy)]
+#[repr(u8)]
+enum ComponentMixOutcome {
+ /// Mix the left and right sides to give the result.
+ Mix,
+ /// Carry the left side forward to the result.
+ UseLeft,
+ /// Carry the right side forward to the result.
+ UseRight,
+ /// The resulting component should also be none.
+ None,
+}
+
+impl ComponentMixOutcome {
+ fn from_colors(
+ left: &AbsoluteColor,
+ right: &AbsoluteColor,
+ flags_to_check: ColorFlags,
+ ) -> Self {
+ match (
+ left.flags.contains(flags_to_check),
+ right.flags.contains(flags_to_check),
+ ) {
+ (true, true) => Self::None,
+ (true, false) => Self::UseRight,
+ (false, true) => Self::UseLeft,
+ (false, false) => Self::Mix,
+ }
+ }
+}
+
+/// Calculate the flags that should be carried forward a color before converting
+/// it to the interpolation color space according to:
+/// <https://drafts.csswg.org/css-color-4/#interpolation-missing>
+fn carry_forward_analogous_missing_components(
+ from: ColorSpace,
+ to: ColorSpace,
+ flags: ColorFlags,
+) -> ColorFlags {
+ use ColorFlags as F;
+ use ColorSpace as S;
+
+ if from == to {
+ return flags;
+ }
+
+ // Reds r, x
+ // Greens g, y
+ // Blues b, z
+ if from.is_rgb_or_xyz_like() && to.is_rgb_or_xyz_like() {
+ return flags;
+ }
+
+ let mut result = flags;
+
+ // Lightness L
+ if matches!(from, S::Lab | S::Lch | S::Oklab | S::Oklch) {
+ if matches!(to, S::Lab | S::Lch | S::Oklab | S::Oklch) {
+ result.set(F::C0_IS_NONE, flags.contains(F::C0_IS_NONE));
+ } else if matches!(to, S::Hsl) {
+ result.set(F::C2_IS_NONE, flags.contains(F::C0_IS_NONE));
+ }
+ } else if matches!(from, S::Hsl) && matches!(to, S::Lab | S::Lch | S::Oklab | S::Oklch) {
+ result.set(F::C0_IS_NONE, flags.contains(F::C2_IS_NONE));
+ }
+
+ // Colorfulness C, S
+ if matches!(from, S::Hsl | S::Lch | S::Oklch) && matches!(to, S::Hsl | S::Lch | S::Oklch) {
+ result.set(F::C1_IS_NONE, flags.contains(F::C1_IS_NONE));
+ }
+
+ // Hue H
+ if matches!(from, S::Hsl | S::Hwb) {
+ if matches!(to, S::Hsl | S::Hwb) {
+ result.set(F::C0_IS_NONE, flags.contains(F::C0_IS_NONE));
+ } else if matches!(to, S::Lch | S::Oklch) {
+ result.set(F::C2_IS_NONE, flags.contains(F::C0_IS_NONE));
+ }
+ } else if matches!(from, S::Lch | S::Oklch) {
+ if matches!(to, S::Hsl | S::Hwb) {
+ result.set(F::C0_IS_NONE, flags.contains(F::C2_IS_NONE));
+ } else if matches!(to, S::Lch | S::Oklch) {
+ result.set(F::C2_IS_NONE, flags.contains(F::C2_IS_NONE));
+ }
+ }
+
+ // Opponent a, a
+ // Opponent b, b
+ if matches!(from, S::Lab | S::Oklab) && matches!(to, S::Lab | S::Oklab) {
+ result.set(F::C1_IS_NONE, flags.contains(F::C1_IS_NONE));
+ result.set(F::C2_IS_NONE, flags.contains(F::C2_IS_NONE));
+ }
+
+ result
+}
+
+fn mix_in(
+ color_space: ColorSpace,
+ left_color: &AbsoluteColor,
+ left_weight: f32,
+ right_color: &AbsoluteColor,
+ right_weight: f32,
+ hue_interpolation: HueInterpolationMethod,
+ alpha_multiplier: f32,
+) -> AbsoluteColor {
+ // Convert both colors into the interpolation color space.
+ let mut left = left_color.to_color_space(color_space);
+ left.flags =
+ carry_forward_analogous_missing_components(left_color.color_space, color_space, left.flags);
+ let mut right = right_color.to_color_space(color_space);
+ right.flags = carry_forward_analogous_missing_components(
+ right_color.color_space,
+ color_space,
+ right.flags,
+ );
+
+ let outcomes = [
+ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE),
+ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE),
+ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE),
+ ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE),
+ ];
+
+ // Convert both sides into just components.
+ let left = left.raw_components();
+ let right = right.raw_components();
+
+ let (result, result_flags) = interpolate_premultiplied(
+ &left,
+ left_weight,
+ &right,
+ right_weight,
+ color_space.hue_index(),
+ hue_interpolation,
+ &outcomes,
+ );
+
+ let alpha = if alpha_multiplier != 1.0 {
+ result[3] * alpha_multiplier
+ } else {
+ result[3]
+ };
+
+ // FIXME: In rare cases we end up with 0.999995 in the alpha channel,
+ // so we reduce the precision to avoid serializing to
+ // rgba(?, ?, ?, 1). This is not ideal, so we should look into
+ // ways to avoid it. Maybe pre-multiply all color components and
+ // then divide after calculations?
+ let alpha = (alpha * 1000.0).round() / 1000.0;
+
+ let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], alpha);
+
+ result.flags = result_flags;
+
+ result
+}
+
+fn interpolate_premultiplied_component(
+ left: f32,
+ left_weight: f32,
+ left_alpha: f32,
+ right: f32,
+ right_weight: f32,
+ right_alpha: f32,
+) -> f32 {
+ left * left_weight * left_alpha + right * right_weight * right_alpha
+}
+
+// Normalize hue into [0, 360)
+#[inline]
+fn normalize_hue(v: f32) -> f32 {
+ v - 360. * (v / 360.).floor()
+}
+
+fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) {
+ // Adjust the hue angle as per
+ // https://drafts.csswg.org/css-color/#hue-interpolation.
+ //
+ // If both hue angles are NAN, they should be set to 0. Otherwise, if a
+ // single hue angle is NAN, it should use the other hue angle.
+ if left.is_nan() {
+ if right.is_nan() {
+ *left = 0.;
+ *right = 0.;
+ } else {
+ *left = *right;
+ }
+ } else if right.is_nan() {
+ *right = *left;
+ }
+
+ if hue_interpolation == HueInterpolationMethod::Specified {
+ // Angles are not adjusted. They are interpolated like any other
+ // component.
+ return;
+ }
+
+ *left = normalize_hue(*left);
+ *right = normalize_hue(*right);
+
+ match hue_interpolation {
+ // https://drafts.csswg.org/css-color/#shorter
+ HueInterpolationMethod::Shorter => {
+ let delta = *right - *left;
+
+ if delta > 180. {
+ *left += 360.;
+ } else if delta < -180. {
+ *right += 360.;
+ }
+ },
+ // https://drafts.csswg.org/css-color/#longer
+ HueInterpolationMethod::Longer => {
+ let delta = *right - *left;
+ if 0. < delta && delta < 180. {
+ *left += 360.;
+ } else if -180. < delta && delta <= 0. {
+ *right += 360.;
+ }
+ },
+ // https://drafts.csswg.org/css-color/#increasing
+ HueInterpolationMethod::Increasing => {
+ if *right < *left {
+ *right += 360.;
+ }
+ },
+ // https://drafts.csswg.org/css-color/#decreasing
+ HueInterpolationMethod::Decreasing => {
+ if *left < *right {
+ *left += 360.;
+ }
+ },
+ HueInterpolationMethod::Specified => unreachable!("Handled above"),
+ }
+}
+
+fn interpolate_hue(
+ mut left: f32,
+ left_weight: f32,
+ mut right: f32,
+ right_weight: f32,
+ hue_interpolation: HueInterpolationMethod,
+) -> f32 {
+ adjust_hue(&mut left, &mut right, hue_interpolation);
+ left * left_weight + right * right_weight
+}
+
+struct InterpolatedAlpha {
+ /// The adjusted left alpha value.
+ left: f32,
+ /// The adjusted right alpha value.
+ right: f32,
+ /// The interpolated alpha value.
+ interpolated: f32,
+ /// Whether the alpha component should be `none`.
+ is_none: bool,
+}
+
+fn interpolate_alpha(
+ left: f32,
+ left_weight: f32,
+ right: f32,
+ right_weight: f32,
+ outcome: ComponentMixOutcome,
+) -> InterpolatedAlpha {
+ // <https://drafts.csswg.org/css-color-4/#interpolation-missing>
+ let mut result = match outcome {
+ ComponentMixOutcome::Mix => {
+ let interpolated = left * left_weight + right * right_weight;
+ InterpolatedAlpha {
+ left,
+ right,
+ interpolated,
+ is_none: false,
+ }
+ },
+ ComponentMixOutcome::UseLeft => InterpolatedAlpha {
+ left,
+ right: left,
+ interpolated: left,
+ is_none: false,
+ },
+ ComponentMixOutcome::UseRight => InterpolatedAlpha {
+ left: right,
+ right,
+ interpolated: right,
+ is_none: false,
+ },
+ ComponentMixOutcome::None => InterpolatedAlpha {
+ left: 1.0,
+ right: 1.0,
+ interpolated: 0.0,
+ is_none: true,
+ },
+ };
+
+ // Clip all alpha values to [0.0..1.0].
+ result.left = result.left.clamp(0.0, 1.0);
+ result.right = result.right.clamp(0.0, 1.0);
+ result.interpolated = result.interpolated.clamp(0.0, 1.0);
+
+ result
+}
+
+fn interpolate_premultiplied(
+ left: &[f32; 4],
+ left_weight: f32,
+ right: &[f32; 4],
+ right_weight: f32,
+ hue_index: Option<usize>,
+ hue_interpolation: HueInterpolationMethod,
+ outcomes: &[ComponentMixOutcome; 4],
+) -> ([f32; 4], ColorFlags) {
+ let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]);
+ let mut flags = if alpha.is_none {
+ ColorFlags::ALPHA_IS_NONE
+ } else {
+ ColorFlags::empty()
+ };
+
+ let mut result = [0.; 4];
+
+ for i in 0..3 {
+ match outcomes[i] {
+ ComponentMixOutcome::Mix => {
+ let is_hue = hue_index == Some(i);
+ result[i] = if is_hue {
+ normalize_hue(interpolate_hue(
+ left[i],
+ left_weight,
+ right[i],
+ right_weight,
+ hue_interpolation,
+ ))
+ } else {
+ let interpolated = interpolate_premultiplied_component(
+ left[i],
+ left_weight,
+ alpha.left,
+ right[i],
+ right_weight,
+ alpha.right,
+ );
+
+ if alpha.interpolated == 0.0 {
+ interpolated
+ } else {
+ interpolated / alpha.interpolated
+ }
+ };
+ },
+ ComponentMixOutcome::UseLeft => result[i] = left[i],
+ ComponentMixOutcome::UseRight => result[i] = right[i],
+ ComponentMixOutcome::None => {
+ result[i] = 0.0;
+ match i {
+ 0 => flags.insert(ColorFlags::C0_IS_NONE),
+ 1 => flags.insert(ColorFlags::C1_IS_NONE),
+ 2 => flags.insert(ColorFlags::C2_IS_NONE),
+ _ => unreachable!(),
+ }
+ },
+ }
+ }
+ result[3] = alpha.interpolated;
+
+ (result, flags)
+}
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)
+ },
+ }
+ }
+}
diff --git a/servo/components/style/color/parsing.rs b/servo/components/style/color/parsing.rs
new file mode 100644
index 0000000000..f60b44c5b6
--- /dev/null
+++ b/servo/components/style/color/parsing.rs
@@ -0,0 +1,1246 @@
+/* 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 http://mozilla.org/MPL/2.0/. */
+
+#![deny(missing_docs)]
+
+//! Fairly complete css-color implementation.
+//! Relative colors, color-mix, system colors, and other such things require better calc() support
+//! and integration.
+
+use super::{
+ convert::{hsl_to_rgb, hwb_to_rgb, normalize_hue},
+ ColorComponents,
+};
+use crate::values::normalize;
+use cssparser::color::{
+ clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, serialize_color_alpha,
+ PredefinedColorSpace, OPAQUE,
+};
+use cssparser::{match_ignore_ascii_case, CowRcStr, ParseError, Parser, ToCss, Token};
+#[cfg(feature = "serde")]
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+use std::f32::consts::PI;
+use std::fmt;
+use std::str::FromStr;
+
+/// Return the named color with the given name.
+///
+/// Matching is case-insensitive in the ASCII range.
+/// CSS escaping (if relevant) should be resolved before calling this function.
+/// (For example, the value of an `Ident` token is fine.)
+#[inline]
+pub fn parse_color_keyword<Output>(ident: &str) -> Result<Output, ()>
+where
+ Output: FromParsedColor,
+{
+ Ok(match_ignore_ascii_case! { ident ,
+ "transparent" => Output::from_rgba(0, 0, 0, 0.0),
+ "currentcolor" => Output::from_current_color(),
+ _ => {
+ let (r, g, b) = cssparser::color::parse_named_color(ident)?;
+ Output::from_rgba(r, g, b, OPAQUE)
+ }
+ })
+}
+
+/// Parse a CSS color using the specified [`ColorParser`] and return a new color
+/// value on success.
+pub fn parse_color_with<'i, 't, P>(
+ color_parser: &P,
+ input: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let location = input.current_source_location();
+ let token = input.next()?;
+ match *token {
+ Token::Hash(ref value) | Token::IDHash(ref value) => {
+ parse_hash_color(value.as_bytes()).map(|(r, g, b, a)| P::Output::from_rgba(r, g, b, a))
+ },
+ Token::Ident(ref value) => parse_color_keyword(value),
+ Token::Function(ref name) => {
+ let name = name.clone();
+ return input.parse_nested_block(|arguments| {
+ parse_color_function(color_parser, name, arguments)
+ });
+ },
+ _ => Err(()),
+ }
+ .map_err(|()| location.new_unexpected_token_error(token.clone()))
+}
+
+/// Parse one of the color functions: rgba(), lab(), color(), etc.
+#[inline]
+fn parse_color_function<'i, 't, P>(
+ color_parser: &P,
+ name: CowRcStr<'i>,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let color = match_ignore_ascii_case! { &name,
+ "rgb" | "rgba" => parse_rgb(color_parser, arguments),
+
+ "hsl" | "hsla" => parse_hsl(color_parser, arguments),
+
+ "hwb" => parse_hwb(color_parser, arguments),
+
+ // for L: 0% = 0.0, 100% = 100.0
+ // for a and b: -100% = -125, 100% = 125
+ "lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab),
+
+ // for L: 0% = 0.0, 100% = 100.0
+ // for C: 0% = 0, 100% = 150
+ "lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch),
+
+ // for L: 0% = 0.0, 100% = 1.0
+ // for a and b: -100% = -0.4, 100% = 0.4
+ "oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab),
+
+ // for L: 0% = 0.0, 100% = 1.0
+ // for C: 0% = 0.0 100% = 0.4
+ "oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklch),
+
+ "color" => parse_color_with_color_space(color_parser, arguments),
+
+ _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))),
+ }?;
+
+ arguments.expect_exhausted()?;
+
+ Ok(color)
+}
+
+/// Parse the alpha component by itself from either number or percentage,
+/// clipping the result to [0.0..1.0].
+#[inline]
+fn parse_alpha_component<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<f32, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ // Percent reference range for alpha: 0% = 0.0, 100% = 1.0
+ let alpha = color_parser
+ .parse_number_or_percentage(arguments)?
+ .to_number(1.0);
+ Ok(normalize(alpha).clamp(0.0, OPAQUE))
+}
+
+fn parse_legacy_alpha<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<f32, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ Ok(if !arguments.is_exhausted() {
+ arguments.expect_comma()?;
+ parse_alpha_component(color_parser, arguments)?
+ } else {
+ OPAQUE
+ })
+}
+
+fn parse_modern_alpha<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<Option<f32>, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ if !arguments.is_exhausted() {
+ arguments.expect_delim('/')?;
+ parse_none_or(arguments, |p| parse_alpha_component(color_parser, p))
+ } else {
+ Ok(Some(OPAQUE))
+ }
+}
+
+#[inline]
+fn parse_rgb<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?;
+
+ // If the first component is not "none" and is followed by a comma, then we
+ // are parsing the legacy syntax.
+ let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok();
+
+ let (red, green, blue, alpha) = if is_legacy_syntax {
+ let (red, green, blue) = match maybe_red.unwrap() {
+ NumberOrPercentage::Number { value } => {
+ let red = clamp_floor_256_f32(value);
+ let green = clamp_floor_256_f32(color_parser.parse_number(arguments)?);
+ arguments.expect_comma()?;
+ let blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?);
+ (red, green, blue)
+ },
+ NumberOrPercentage::Percentage { unit_value } => {
+ let red = clamp_unit_f32(unit_value);
+ let green = clamp_unit_f32(color_parser.parse_percentage(arguments)?);
+ arguments.expect_comma()?;
+ let blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?);
+ (red, green, blue)
+ },
+ };
+
+ let alpha = parse_legacy_alpha(color_parser, arguments)?;
+
+ (red, green, blue, alpha)
+ } else {
+ #[inline]
+ fn get_component_value(c: Option<NumberOrPercentage>) -> u8 {
+ c.map(|c| match c {
+ NumberOrPercentage::Number { value } => clamp_floor_256_f32(value),
+ NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value),
+ })
+ .unwrap_or(0)
+ }
+
+ let red = get_component_value(maybe_red);
+
+ let green = get_component_value(parse_none_or(arguments, |p| {
+ color_parser.parse_number_or_percentage(p)
+ })?);
+
+ let blue = get_component_value(parse_none_or(arguments, |p| {
+ color_parser.parse_number_or_percentage(p)
+ })?);
+
+ let alpha = parse_modern_alpha(color_parser, arguments)?.unwrap_or(0.0);
+
+ (red, green, blue, alpha)
+ };
+
+ Ok(P::Output::from_rgba(red, green, blue, alpha))
+}
+
+/// Parses hsl syntax.
+///
+/// <https://drafts.csswg.org/css-color/#the-hsl-notation>
+#[inline]
+fn parse_hsl<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ // Percent reference range for S and L: 0% = 0.0, 100% = 100.0
+ const LIGHTNESS_RANGE: f32 = 100.0;
+ const SATURATION_RANGE: f32 = 100.0;
+
+ let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?;
+
+ // If the hue is not "none" and is followed by a comma, then we are parsing
+ // the legacy syntax.
+ let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok();
+
+ let saturation: Option<f32>;
+ let lightness: Option<f32>;
+
+ let alpha = if is_legacy_syntax {
+ saturation = Some(color_parser.parse_percentage(arguments)? * SATURATION_RANGE);
+ arguments.expect_comma()?;
+ lightness = Some(color_parser.parse_percentage(arguments)? * LIGHTNESS_RANGE);
+ Some(parse_legacy_alpha(color_parser, arguments)?)
+ } else {
+ saturation = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?
+ .map(|v| v.to_number(SATURATION_RANGE));
+ lightness = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?
+ .map(|v| v.to_number(LIGHTNESS_RANGE));
+ parse_modern_alpha(color_parser, arguments)?
+ };
+
+ let hue = maybe_hue.map(|h| normalize_hue(h.degrees()));
+ let saturation = saturation.map(|s| s.clamp(0.0, SATURATION_RANGE));
+ let lightness = lightness.map(|l| l.clamp(0.0, LIGHTNESS_RANGE));
+
+ Ok(P::Output::from_hsl(hue, saturation, lightness, alpha))
+}
+
+/// Parses hwb syntax.
+///
+/// <https://drafts.csswg.org/css-color/#the-hbw-notation>
+#[inline]
+fn parse_hwb<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ // Percent reference range for W and B: 0% = 0.0, 100% = 100.0
+ const WHITENESS_RANGE: f32 = 100.0;
+ const BLACKNESS_RANGE: f32 = 100.0;
+
+ let (hue, whiteness, blackness, alpha) = parse_components(
+ color_parser,
+ arguments,
+ P::parse_angle_or_number,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ )?;
+
+ let hue = hue.map(|h| normalize_hue(h.degrees()));
+ let whiteness = whiteness.map(|w| w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE));
+ let blackness = blackness.map(|b| b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE));
+
+ Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha))
+}
+
+type IntoColorFn<Output> =
+ fn(l: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) -> Output;
+
+#[inline]
+fn parse_lab_like<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+ lightness_range: f32,
+ a_b_range: f32,
+ into_color: IntoColorFn<P::Output>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let (lightness, a, b, alpha) = parse_components(
+ color_parser,
+ arguments,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ )?;
+
+ let lightness = lightness.map(|l| l.to_number(lightness_range));
+ let a = a.map(|a| a.to_number(a_b_range));
+ let b = b.map(|b| b.to_number(a_b_range));
+
+ Ok(into_color(lightness, a, b, alpha))
+}
+
+#[inline]
+fn parse_lch_like<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+ lightness_range: f32,
+ chroma_range: f32,
+ into_color: IntoColorFn<P::Output>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let (lightness, chroma, hue, alpha) = parse_components(
+ color_parser,
+ arguments,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ P::parse_angle_or_number,
+ )?;
+
+ let lightness = lightness.map(|l| l.to_number(lightness_range));
+ let chroma = chroma.map(|c| c.to_number(chroma_range));
+ let hue = hue.map(|h| normalize_hue(h.degrees()));
+
+ Ok(into_color(lightness, chroma, hue, alpha))
+}
+
+/// Parse the color() function.
+#[inline]
+fn parse_color_with_color_space<'i, 't, P>(
+ color_parser: &P,
+ arguments: &mut Parser<'i, 't>,
+) -> Result<P::Output, ParseError<'i, P::Error>>
+where
+ P: ColorParser<'i>,
+{
+ let color_space = {
+ let location = arguments.current_source_location();
+
+ let ident = arguments.expect_ident()?;
+ PredefinedColorSpace::from_str(ident)
+ .map_err(|_| location.new_unexpected_token_error(Token::Ident(ident.clone())))?
+ };
+
+ let (c1, c2, c3, alpha) = parse_components(
+ color_parser,
+ arguments,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ P::parse_number_or_percentage,
+ )?;
+
+ let c1 = c1.map(|c| c.to_number(1.0));
+ let c2 = c2.map(|c| c.to_number(1.0));
+ let c3 = c3.map(|c| c.to_number(1.0));
+
+ Ok(P::Output::from_color_function(
+ color_space,
+ c1,
+ c2,
+ c3,
+ alpha,
+ ))
+}
+
+type ComponentParseResult<'i, R1, R2, R3, Error> =
+ Result<(Option<R1>, Option<R2>, Option<R3>, Option<f32>), ParseError<'i, Error>>;
+
+/// Parse the color components and alpha with the modern [color-4] syntax.
+pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>(
+ color_parser: &P,
+ input: &mut Parser<'i, 't>,
+ f1: F1,
+ f2: F2,
+ f3: F3,
+) -> ComponentParseResult<'i, R1, R2, R3, P::Error>
+where
+ P: ColorParser<'i>,
+ F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R1, ParseError<'i, P::Error>>,
+ F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R2, ParseError<'i, P::Error>>,
+ F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R3, ParseError<'i, P::Error>>,
+{
+ let r1 = parse_none_or(input, |p| f1(color_parser, p))?;
+ let r2 = parse_none_or(input, |p| f2(color_parser, p))?;
+ let r3 = parse_none_or(input, |p| f3(color_parser, p))?;
+
+ let alpha = parse_modern_alpha(color_parser, input)?;
+
+ Ok((r1, r2, r3, alpha))
+}
+
+fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result<Option<T>, E>
+where
+ F: FnOnce(&mut Parser<'i, 't>) -> Result<T, E>,
+{
+ match input.try_parse(|p| p.expect_ident_matching("none")) {
+ Ok(_) => Ok(None),
+ Err(_) => Ok(Some(thing(input)?)),
+ }
+}
+
+/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and
+/// floating point values.
+struct ModernComponent<'a>(&'a Option<f32>);
+
+impl<'a> ToCss for ModernComponent<'a> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ if let Some(value) = self.0 {
+ if value.is_finite() {
+ value.to_css(dest)
+ } else if value.is_nan() {
+ dest.write_str("calc(NaN)")
+ } else {
+ debug_assert!(value.is_infinite());
+ if value.is_sign_negative() {
+ dest.write_str("calc(-infinity)")
+ } else {
+ dest.write_str("calc(infinity)")
+ }
+ }
+ } else {
+ dest.write_str("none")
+ }
+ }
+}
+
+/// A color with red, green, blue, and alpha components, in a byte each.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct RgbaLegacy {
+ /// The red component.
+ pub red: u8,
+ /// The green component.
+ pub green: u8,
+ /// The blue component.
+ pub blue: u8,
+ /// The alpha component.
+ pub alpha: f32,
+}
+
+impl RgbaLegacy {
+ /// Constructs a new RGBA value from float components. It expects the red,
+ /// green, blue and alpha channels in that order, and all values will be
+ /// clamped to the 0.0 ... 1.0 range.
+ #[inline]
+ pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
+ Self::new(
+ clamp_unit_f32(red),
+ clamp_unit_f32(green),
+ clamp_unit_f32(blue),
+ alpha.clamp(0.0, OPAQUE),
+ )
+ }
+
+ /// Same thing, but with `u8` values instead of floats in the 0 to 1 range.
+ #[inline]
+ pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
+ Self {
+ red,
+ green,
+ blue,
+ alpha,
+ }
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for RgbaLegacy {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.red, self.green, self.blue, self.alpha).serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for RgbaLegacy {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (r, g, b, a) = Deserialize::deserialize(deserializer)?;
+ Ok(RgbaLegacy::new(r, g, b, a))
+ }
+}
+
+impl ToCss for RgbaLegacy {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let has_alpha = self.alpha != OPAQUE;
+
+ dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?;
+ self.red.to_css(dest)?;
+ dest.write_str(", ")?;
+ self.green.to_css(dest)?;
+ dest.write_str(", ")?;
+ self.blue.to_css(dest)?;
+
+ // Legacy syntax does not allow none components.
+ serialize_color_alpha(dest, Some(self.alpha), true)?;
+
+ dest.write_char(')')
+ }
+}
+
+/// Color specified by hue, saturation and lightness components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Hsl {
+ /// The hue component.
+ pub hue: Option<f32>,
+ /// The saturation component.
+ pub saturation: Option<f32>,
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+impl Hsl {
+ /// Construct a new HSL color from it's components.
+ pub fn new(
+ hue: Option<f32>,
+ saturation: Option<f32>,
+ lightness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ hue,
+ saturation,
+ lightness,
+ alpha,
+ }
+ }
+}
+
+impl ToCss for Hsl {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ // HSL serializes to RGB, so we have to convert it.
+ let ColorComponents(red, green, blue) = hsl_to_rgb(&ColorComponents(
+ self.hue.unwrap_or(0.0) / 360.0,
+ self.saturation.unwrap_or(0.0),
+ self.lightness.unwrap_or(0.0),
+ ));
+
+ RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Hsl {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for Hsl {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?;
+ Ok(Self::new(lightness, a, b, alpha))
+ }
+}
+
+/// Color specified by hue, whiteness and blackness components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Hwb {
+ /// The hue component.
+ pub hue: Option<f32>,
+ /// The whiteness component.
+ pub whiteness: Option<f32>,
+ /// The blackness component.
+ pub blackness: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+impl Hwb {
+ /// Construct a new HWB color from it's components.
+ pub fn new(
+ hue: Option<f32>,
+ whiteness: Option<f32>,
+ blackness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ hue,
+ whiteness,
+ blackness,
+ alpha,
+ }
+ }
+}
+
+impl ToCss for Hwb {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ // HWB serializes to RGB, so we have to convert it.
+ let ColorComponents(red, green, blue) = hwb_to_rgb(&ColorComponents(
+ self.hue.unwrap_or(0.0) / 360.0,
+ self.whiteness.unwrap_or(0.0),
+ self.blackness.unwrap_or(0.0),
+ ));
+
+ RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl Serialize for Hwb {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer)
+ }
+}
+
+#[cfg(feature = "serde")]
+impl<'de> Deserialize<'de> for Hwb {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?;
+ Ok(Self::new(lightness, whiteness, blackness, alpha))
+ }
+}
+
+// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro,
+// because it causes cbindgen to ignore them.
+
+/// Color specified by lightness, a- and b-axis components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Lab {
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The a-axis component.
+ pub a: Option<f32>,
+ /// The b-axis component.
+ pub b: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+/// Color specified by lightness, a- and b-axis components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Oklab {
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The a-axis component.
+ pub a: Option<f32>,
+ /// The b-axis component.
+ pub b: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+macro_rules! impl_lab_like {
+ ($cls:ident, $fname:literal) => {
+ impl $cls {
+ /// Construct a new Lab color format with lightness, a, b and alpha components.
+ pub fn new(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ lightness,
+ a,
+ b,
+ alpha,
+ }
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ impl Serialize for $cls {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.lightness, self.a, self.b, self.alpha).serialize(serializer)
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ impl<'de> Deserialize<'de> for $cls {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?;
+ Ok(Self::new(lightness, a, b, alpha))
+ }
+ }
+
+ impl ToCss for $cls {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str($fname)?;
+ dest.write_str("(")?;
+ ModernComponent(&self.lightness).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.a).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.b).to_css(dest)?;
+ serialize_color_alpha(dest, self.alpha, false)?;
+ dest.write_char(')')
+ }
+ }
+ };
+}
+
+impl_lab_like!(Lab, "lab");
+impl_lab_like!(Oklab, "oklab");
+
+// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro,
+// because it causes cbindgen to ignore them.
+
+/// Color specified by lightness, chroma and hue components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Lch {
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The chroma component.
+ pub chroma: Option<f32>,
+ /// The hue component.
+ pub hue: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+/// Color specified by lightness, chroma and hue components.
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct Oklch {
+ /// The lightness component.
+ pub lightness: Option<f32>,
+ /// The chroma component.
+ pub chroma: Option<f32>,
+ /// The hue component.
+ pub hue: Option<f32>,
+ /// The alpha component.
+ pub alpha: Option<f32>,
+}
+
+macro_rules! impl_lch_like {
+ ($cls:ident, $fname:literal) => {
+ impl $cls {
+ /// Construct a new color with lightness, chroma and hue components.
+ pub fn new(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ lightness,
+ chroma,
+ hue,
+ alpha,
+ }
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ impl Serialize for $cls {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ (self.lightness, self.chroma, self.hue, self.alpha).serialize(serializer)
+ }
+ }
+
+ #[cfg(feature = "serde")]
+ impl<'de> Deserialize<'de> for $cls {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let (lightness, chroma, hue, alpha) = Deserialize::deserialize(deserializer)?;
+ Ok(Self::new(lightness, chroma, hue, alpha))
+ }
+ }
+
+ impl ToCss for $cls {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str($fname)?;
+ dest.write_str("(")?;
+ ModernComponent(&self.lightness).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.chroma).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.hue).to_css(dest)?;
+ serialize_color_alpha(dest, self.alpha, false)?;
+ dest.write_char(')')
+ }
+ }
+ };
+}
+
+impl_lch_like!(Lch, "lch");
+impl_lch_like!(Oklch, "oklch");
+
+/// A color specified by the color() function.
+/// <https://drafts.csswg.org/css-color-4/#color-function>
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub struct ColorFunction {
+ /// The color space for this color.
+ pub color_space: PredefinedColorSpace,
+ /// The first component of the color. Either red or x.
+ pub c1: Option<f32>,
+ /// The second component of the color. Either green or y.
+ pub c2: Option<f32>,
+ /// The third component of the color. Either blue or z.
+ pub c3: Option<f32>,
+ /// The alpha component of the color.
+ pub alpha: Option<f32>,
+}
+
+impl ColorFunction {
+ /// Construct a new color function definition with the given color space and
+ /// color components.
+ pub fn new(
+ color_space: PredefinedColorSpace,
+ c1: Option<f32>,
+ c2: Option<f32>,
+ c3: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Self {
+ color_space,
+ c1,
+ c2,
+ c3,
+ alpha,
+ }
+ }
+}
+
+impl ToCss for ColorFunction {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_str("color(")?;
+ self.color_space.to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.c1).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.c2).to_css(dest)?;
+ dest.write_char(' ')?;
+ ModernComponent(&self.c3).to_css(dest)?;
+
+ serialize_color_alpha(dest, self.alpha, false)?;
+
+ dest.write_char(')')
+ }
+}
+
+/// Describes one of the value <color> values according to the CSS
+/// specification.
+///
+/// Most components are `Option<_>`, so when the value is `None`, that component
+/// serializes to the "none" keyword.
+///
+/// <https://drafts.csswg.org/css-color-4/#color-type>
+#[derive(Clone, Copy, PartialEq, Debug)]
+pub enum Color {
+ /// The 'currentcolor' keyword.
+ CurrentColor,
+ /// Specify sRGB colors directly by their red/green/blue/alpha chanels.
+ Rgba(RgbaLegacy),
+ /// Specifies a color in sRGB using hue, saturation and lightness components.
+ Hsl(Hsl),
+ /// Specifies a color in sRGB using hue, whiteness and blackness components.
+ Hwb(Hwb),
+ /// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue
+ /// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB
+ /// rectangular coordinate model.
+ Lab(Lab),
+ /// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the
+ /// CIE LCH cylindrical coordinate model.
+ Lch(Lch),
+ /// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue
+ /// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab
+ /// rectangular coordinate model.
+ Oklab(Oklab),
+ /// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using
+ /// the OKLCH cylindrical coordinate model.
+ Oklch(Oklch),
+ /// Specifies a color in a predefined color space.
+ ColorFunction(ColorFunction),
+}
+
+impl ToCss for Color {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ Color::CurrentColor => dest.write_str("currentcolor"),
+ Color::Rgba(rgba) => rgba.to_css(dest),
+ Color::Hsl(hsl) => hsl.to_css(dest),
+ Color::Hwb(hwb) => hwb.to_css(dest),
+ Color::Lab(lab) => lab.to_css(dest),
+ Color::Lch(lch) => lch.to_css(dest),
+ Color::Oklab(lab) => lab.to_css(dest),
+ Color::Oklch(lch) => lch.to_css(dest),
+ Color::ColorFunction(color_function) => color_function.to_css(dest),
+ }
+ }
+}
+
+/// Either a number or a percentage.
+pub enum NumberOrPercentage {
+ /// `<number>`.
+ Number {
+ /// The numeric value parsed, as a float.
+ value: f32,
+ },
+ /// `<percentage>`
+ Percentage {
+ /// The value as a float, divided by 100 so that the nominal range is
+ /// 0.0 to 1.0.
+ unit_value: f32,
+ },
+}
+
+impl NumberOrPercentage {
+ /// Return the value as a number. Percentages will be adjusted to the range
+ /// [0..percent_basis].
+ pub fn to_number(&self, percentage_basis: f32) -> f32 {
+ match *self {
+ Self::Number { value } => value,
+ Self::Percentage { unit_value } => unit_value * percentage_basis,
+ }
+ }
+}
+
+/// Either an angle or a number.
+pub enum AngleOrNumber {
+ /// `<number>`.
+ Number {
+ /// The numeric value parsed, as a float.
+ value: f32,
+ },
+ /// `<angle>`
+ Angle {
+ /// The value as a number of degrees.
+ degrees: f32,
+ },
+}
+
+impl AngleOrNumber {
+ /// Return the angle in degrees. `AngleOrNumber::Number` is returned as
+ /// degrees, because it is the canonical unit.
+ pub fn degrees(&self) -> f32 {
+ match *self {
+ AngleOrNumber::Number { value } => value,
+ AngleOrNumber::Angle { degrees } => degrees,
+ }
+ }
+}
+
+/// A trait that can be used to hook into how `cssparser` parses color
+/// components, with the intention of implementing more complicated behavior.
+///
+/// For example, this is used by Servo to support calc() in color.
+pub trait ColorParser<'i> {
+ /// The type that the parser will construct on a successful parse.
+ type Output: FromParsedColor;
+
+ /// A custom error type that can be returned from the parsing functions.
+ type Error: 'i;
+
+ /// Parse an `<angle>` or `<number>`.
+ ///
+ /// Returns the result in degrees.
+ fn parse_angle_or_number<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> {
+ let location = input.current_source_location();
+ Ok(match *input.next()? {
+ Token::Number { value, .. } => AngleOrNumber::Number { value },
+ Token::Dimension {
+ value: v, ref unit, ..
+ } => {
+ let degrees = match_ignore_ascii_case! { unit,
+ "deg" => v,
+ "grad" => v * 360. / 400.,
+ "rad" => v * 360. / (2. * PI),
+ "turn" => v * 360.,
+ _ => {
+ return Err(location.new_unexpected_token_error(Token::Ident(unit.clone())))
+ }
+ };
+
+ AngleOrNumber::Angle { degrees }
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ })
+ }
+
+ /// Parse a `<percentage>` value.
+ ///
+ /// Returns the result in a number from 0.0 to 1.0.
+ fn parse_percentage<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<f32, ParseError<'i, Self::Error>> {
+ input.expect_percentage().map_err(From::from)
+ }
+
+ /// Parse a `<number>` value.
+ fn parse_number<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<f32, ParseError<'i, Self::Error>> {
+ input.expect_number().map_err(From::from)
+ }
+
+ /// Parse a `<number>` value or a `<percentage>` value.
+ fn parse_number_or_percentage<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> {
+ let location = input.current_source_location();
+ Ok(match *input.next()? {
+ Token::Number { value, .. } => NumberOrPercentage::Number { value },
+ Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ })
+ }
+}
+
+/// Default implementation of a [`ColorParser`]
+pub struct DefaultColorParser;
+
+impl<'i> ColorParser<'i> for DefaultColorParser {
+ type Output = Color;
+ type Error = ();
+}
+
+impl Color {
+ /// Parse a <color> value, per CSS Color Module Level 3.
+ ///
+ /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet.
+ pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i, ()>> {
+ parse_color_with(&DefaultColorParser, input)
+ }
+}
+
+/// This trait is used by the [`ColorParser`] to construct colors of any type.
+pub trait FromParsedColor {
+ /// Construct a new color from the CSS `currentcolor` keyword.
+ fn from_current_color() -> Self;
+
+ /// Construct a new color from red, green, blue and alpha components.
+ fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self;
+
+ /// Construct a new color from hue, saturation, lightness and alpha components.
+ fn from_hsl(
+ hue: Option<f32>,
+ saturation: Option<f32>,
+ lightness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color from hue, blackness, whiteness and alpha components.
+ fn from_hwb(
+ hue: Option<f32>,
+ whiteness: Option<f32>,
+ blackness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color from the `lab` notation.
+ fn from_lab(lightness: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>)
+ -> Self;
+
+ /// Construct a new color from the `lch` notation.
+ fn from_lch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color from the `oklab` notation.
+ fn from_oklab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color from the `oklch` notation.
+ fn from_oklch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+
+ /// Construct a new color with a predefined color space.
+ fn from_color_function(
+ color_space: PredefinedColorSpace,
+ c1: Option<f32>,
+ c2: Option<f32>,
+ c3: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self;
+}
+
+impl FromParsedColor for Color {
+ #[inline]
+ fn from_current_color() -> Self {
+ Color::CurrentColor
+ }
+
+ #[inline]
+ fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self {
+ Color::Rgba(RgbaLegacy::new(red, green, blue, alpha))
+ }
+
+ fn from_hsl(
+ hue: Option<f32>,
+ saturation: Option<f32>,
+ lightness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Hsl(Hsl::new(hue, saturation, lightness, alpha))
+ }
+
+ fn from_hwb(
+ hue: Option<f32>,
+ blackness: Option<f32>,
+ whiteness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha))
+ }
+
+ #[inline]
+ fn from_lab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Lab(Lab::new(lightness, a, b, alpha))
+ }
+
+ #[inline]
+ fn from_lch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Lch(Lch::new(lightness, chroma, hue, alpha))
+ }
+
+ #[inline]
+ fn from_oklab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Oklab(Oklab::new(lightness, a, b, alpha))
+ }
+
+ #[inline]
+ fn from_oklch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::Oklch(Oklch::new(lightness, chroma, hue, alpha))
+ }
+
+ #[inline]
+ fn from_color_function(
+ color_space: PredefinedColorSpace,
+ c1: Option<f32>,
+ c2: Option<f32>,
+ c3: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha))
+ }
+}