diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /servo/components/style/color | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'servo/components/style/color')
-rw-r--r-- | servo/components/style/color/convert.rs | 902 | ||||
-rw-r--r-- | servo/components/style/color/mix.rs | 558 | ||||
-rw-r--r-- | servo/components/style/color/mod.rs | 613 | ||||
-rw-r--r-- | servo/components/style/color/parsing.rs | 1246 |
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)) + } +} |