diff --git a/components/style/color/convert.rs b/components/style/color/convert.rs index 5c44079a7c1..3cf04ec02e2 100644 --- a/components/style/color/convert.rs +++ b/components/style/color/convert.rs @@ -68,6 +68,7 @@ fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 { } /// 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 { let ColorComponents(hue, saturation, lightness) = *from; @@ -87,26 +88,26 @@ pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents { } /// Convert from RGB notation to HSL notation. -/// https://drafts.csswg.org/css-color/#rgb-to-hsl +/// 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 light = (min + max) / 2.0; + let lightness = (min + max) / 2.0; let delta = max - min; - let sat = if delta != 0.0 { - if light == 0.0 || light == 1.0 { + let saturation = if delta != 0.0 { + if lightness == 0.0 || lightness == 1.0 { 0.0 } else { - (max - light) / light.min(1.0 - light) + (max - lightness) / lightness.min(1.0 - lightness) } } else { 0.0 }; - ColorComponents(hue, sat, light) + ColorComponents(hue, saturation, lightness) } /// Convert from HWB notation to RGB notation. @@ -138,6 +139,30 @@ pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents { ColorComponents(hue, whiteness, blackness) } +/// Convert from Lab to Lch. This calculation works for both Lab and Olab. +/// +#[inline] +pub fn lab_to_lch(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, a, b) = *from; + + let hue = normalize_hue(b.atan2(a) * 180.0 / PI); + let chroma = (a.powf(2.0) + b.powf(2.0)).sqrt(); + + ColorComponents(lightness, chroma, hue) +} + +/// Convert from Lch to Lab. This calculation works for both Lch and Oklch. +/// +#[inline] +pub fn lch_to_lab(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, chroma, hue) = *from; + + let a = chroma * (hue * PI / 180.0).cos(); + let b = chroma * (hue * PI / 180.0).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)); @@ -305,6 +330,56 @@ impl ColorSpaceConversion for Srgb { } } +/// 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/csswg-drafts/css-color-4/#predefined-sRGB-linear pub struct SrgbLinear; diff --git a/components/style/color/mod.rs b/components/style/color/mod.rs index 3e49c1d2545..635a2ca8b2c 100644 --- a/components/style/color/mod.rs +++ b/components/style/color/mod.rs @@ -26,11 +26,6 @@ impl ColorComponents { /// A color space representation in the CSS specification. /// /// https://drafts.csswg.org/css-color-4/#typedef-color-space -/// -/// NOTE: Right now HSL and HWB colors can not be constructed by the user. They -/// are converted to RGB in the parser. The parser should return the -/// HSL/HWB values as is to avoid unnescessary conversions to/from RGB. -/// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1817035 #[derive( Clone, Copy, @@ -138,7 +133,15 @@ bitflags! { pub struct SerializationFlags : u8 { /// If set, serializes sRGB colors into `color(srgb ...)` instead of /// `rgba(...)`. - const AS_COLOR_FUNCTION = 0x01; + const AS_COLOR_FUNCTION = 1 << 0; + /// Whether the 1st color component is `none`. + const C1_IS_NONE = 1 << 1; + /// Whether the 2nd color component is `none`. + const C2_IS_NONE = 1 << 2; + /// Whether the 3rd color component is `none`. + const C3_IS_NONE = 1 << 3; + /// Whether the alpha component is `none`. + const ALPHA_IS_NONE = 1 << 4; } } @@ -259,20 +262,69 @@ impl AbsoluteColor { return self.clone(); } + // We have simplified conversions that do not need to convert to XYZ + // first. This improves performance, because it skips 2 matrix + // multiplications and reduces float rounding errors. + match (self.color_space, color_space) { + (Srgb, Hsl) => { + return Self::new( + color_space, + convert::rgb_to_hsl(&self.components), + self.alpha, + ); + }, + + (Srgb, Hwb) => { + return Self::new( + color_space, + convert::rgb_to_hwb(&self.components), + self.alpha, + ); + }, + + (Hsl, Srgb) => { + return Self::new( + color_space, + convert::hsl_to_rgb(&self.components), + self.alpha, + ); + }, + + (Hwb, Srgb) => { + return Self::new( + color_space, + convert::hwb_to_rgb(&self.components), + self.alpha, + ); + }, + + (Lab, Lch) | (Oklab, Oklch) => { + return Self::new( + color_space, + convert::lab_to_lch(&self.components), + self.alpha, + ); + }, + + (Lch, Lab) | (Oklch, Oklab) => { + return Self::new( + color_space, + convert::lch_to_lab(&self.components), + self.alpha, + ); + }, + + _ => {}, + } + let (xyz, white_point) = match self.color_space { - Hsl => { - let rgb = convert::hsl_to_rgb(&self.components); - convert::to_xyz::(&rgb) - }, - Hwb => { - let rgb = convert::hwb_to_rgb(&self.components); - convert::to_xyz::(&rgb) - }, Lab => convert::to_xyz::(&self.components), Lch => convert::to_xyz::(&self.components), Oklab => convert::to_xyz::(&self.components), Oklch => convert::to_xyz::(&self.components), Srgb => convert::to_xyz::(&self.components), + Hsl => convert::to_xyz::(&self.components), + Hwb => convert::to_xyz::(&self.components), SrgbLinear => convert::to_xyz::(&self.components), DisplayP3 => convert::to_xyz::(&self.components), A98Rgb => convert::to_xyz::(&self.components), @@ -283,19 +335,13 @@ impl AbsoluteColor { }; let result = match color_space { - Hsl => { - let rgb = convert::from_xyz::(&xyz, white_point); - convert::rgb_to_hsl(&rgb) - }, - Hwb => { - let rgb = convert::from_xyz::(&xyz, white_point); - convert::rgb_to_hwb(&rgb) - }, Lab => convert::from_xyz::(&xyz, white_point), Lch => convert::from_xyz::(&xyz, white_point), Oklab => convert::from_xyz::(&xyz, white_point), Oklch => convert::from_xyz::(&xyz, white_point), Srgb => convert::from_xyz::(&xyz, white_point), + Hsl => convert::from_xyz::(&xyz, white_point), + Hwb => convert::from_xyz::(&xyz, white_point), SrgbLinear => convert::from_xyz::(&xyz, white_point), DisplayP3 => convert::from_xyz::(&xyz, white_point), A98Rgb => convert::from_xyz::(&xyz, white_point), @@ -329,6 +375,21 @@ impl ToCss for AbsoluteColor { where W: Write, { + macro_rules! value_or_none { + ($v:expr,$flag:tt) => {{ + if self.flags.contains(SerializationFlags::$flag) { + None + } else { + Some($v) + } + }}; + } + + let maybe_c1 = value_or_none!(self.components.0, C1_IS_NONE); + let maybe_c2 = value_or_none!(self.components.1, C2_IS_NONE); + let maybe_c3 = value_or_none!(self.components.2, C3_IS_NONE); + let maybe_alpha = value_or_none!(self.alpha, ALPHA_IS_NONE); + match self.color_space { ColorSpace::Hsl => { let rgb = convert::hsl_to_rgb(&self.components); @@ -342,30 +403,28 @@ impl ToCss for AbsoluteColor { }, ColorSpace::Srgb if !self.flags.contains(SerializationFlags::AS_COLOR_FUNCTION) => { + // Althought we are passing Option<_> in here, the to_css fn + // knows that the "none" keyword is not supported in the + // rgb/rgba legacy syntax. cssparser::ToCss::to_css( - &cssparser::RGBA::from_floats( - self.components.0, - self.components.1, - self.components.2, - self.alpha(), - ), + &cssparser::RGBA::from_floats(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), dest, ) }, ColorSpace::Lab => cssparser::ToCss::to_css( - unsafe { color_components_as!(self, cssparser::Lab) }, + &cssparser::Lab::new(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), dest, ), ColorSpace::Lch => cssparser::ToCss::to_css( - unsafe { color_components_as!(self, cssparser::Lch) }, + &cssparser::Lch::new(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), dest, ), ColorSpace::Oklab => cssparser::ToCss::to_css( - unsafe { color_components_as!(self, cssparser::Oklab) }, + &cssparser::Oklab::new(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), dest, ), ColorSpace::Oklch => cssparser::ToCss::to_css( - unsafe { color_components_as!(self, cssparser::Oklch) }, + &cssparser::Oklch::new(maybe_c1, maybe_c2, maybe_c3, maybe_alpha), dest, ), _ => { @@ -393,10 +452,10 @@ impl ToCss for AbsoluteColor { let color_function = cssparser::ColorFunction { color_space, - c1: self.components.0, - c2: self.components.1, - c3: self.components.2, - alpha: self.alpha, + c1: maybe_c1, + c2: maybe_c2, + c3: maybe_c3, + alpha: maybe_alpha, }; let color = cssparser::Color::ColorFunction(color_function); cssparser::ToCss::to_css(&color, dest) diff --git a/components/style/values/specified/color.rs b/components/style/values/specified/color.rs index f914216afbe..121287a66ce 100644 --- a/components/style/values/specified/color.rs +++ b/components/style/values/specified/color.rs @@ -14,7 +14,7 @@ use crate::values::generics::color::{GenericCaretColor, GenericColorMix, Generic use crate::values::specified::calc::CalcNode; use crate::values::specified::Percentage; use crate::values::CustomIdent; -use cssparser::{AngleOrNumber, Color as CSSParserColor, Parser, Token, RGBA}; +use cssparser::{AngleOrNumber, Color as CSSParserColor, Parser, Token}; use cssparser::{BasicParseErrorKind, NumberOrPercentage, ParseErrorKind}; use itoa; use std::fmt::{self, Write}; @@ -409,9 +409,35 @@ impl SystemColor { } #[inline] -fn new_absolute(color_space: ColorSpace, c1: f32, c2: f32, c3: f32, alpha: f32) -> Color { +fn new_absolute( + color_space: ColorSpace, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, +) -> Color { + let mut flags = SerializationFlags::empty(); + + macro_rules! c { + ($v:expr,$flag:tt) => {{ + if let Some(value) = $v { + value + } else { + flags |= SerializationFlags::$flag; + 0.0 + } + }}; + } + + let c1 = c!(c1, C1_IS_NONE); + let c2 = c!(c2, C2_IS_NONE); + let c3 = c!(c3, C3_IS_NONE); + let alpha = c!(alpha, ALPHA_IS_NONE); + + let mut color = AbsoluteColor::new(color_space, ColorComponents(c1, c2, c3), alpha); + color.flags |= flags; Color::Absolute(Box::new(Absolute { - color: AbsoluteColor::new(color_space, ColorComponents(c1, c2, c3), alpha), + color, authored: None, })) } @@ -421,38 +447,76 @@ impl cssparser::FromParsedColor for Color { Color::CurrentColor } - fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + fn from_rgba(red: Option, green: Option, blue: Option, alpha: Option) -> Self { new_absolute( ColorSpace::Srgb, - red as f32 / 255.0, - green as f32 / 255.0, - blue as f32 / 255.0, + red.map(|r| r as f32 / 255.0), + green.map(|g| g as f32 / 255.0), + blue.map(|b| b as f32 / 255.0), alpha, ) } - fn from_lab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self { + fn from_hsl( + hue: Option, + saturation: Option, + lightness: Option, + alpha: Option, + ) -> Self { + new_absolute(ColorSpace::Hsl, hue, saturation, lightness, alpha) + } + + fn from_hwb( + hue: Option, + whiteness: Option, + blackness: Option, + alpha: Option, + ) -> Self { + new_absolute(ColorSpace::Hwb, hue, whiteness, blackness, alpha) + } + + fn from_lab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { new_absolute(ColorSpace::Lab, lightness, a, b, alpha) } - fn from_lch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self { + fn from_lch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { new_absolute(ColorSpace::Lch, lightness, chroma, hue, alpha) } - fn from_oklab(lightness: f32, a: f32, b: f32, alpha: f32) -> Self { + fn from_oklab( + lightness: Option, + a: Option, + b: Option, + alpha: Option, + ) -> Self { new_absolute(ColorSpace::Oklab, lightness, a, b, alpha) } - fn from_oklch(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Self { + fn from_oklch( + lightness: Option, + chroma: Option, + hue: Option, + alpha: Option, + ) -> Self { new_absolute(ColorSpace::Oklch, lightness, chroma, hue, alpha) } fn from_color_function( color_space: cssparser::PredefinedColorSpace, - c1: f32, - c2: f32, - c3: f32, - alpha: f32, + c1: Option, + c2: Option, + c3: Option, + alpha: Option, ) -> Self { let mut result = new_absolute(color_space.into(), c1, c2, c3, alpha); if let Color::Absolute(ref mut absolute) = result { @@ -569,14 +633,17 @@ impl Color { Ok(mut color) => { if let Color::Absolute(ref mut absolute) = color { let enabled = { - let is_srgb = matches!(absolute.color.color_space, ColorSpace::Srgb); + let is_legacy_color = matches!( + absolute.color.color_space, + ColorSpace::Srgb | ColorSpace::Hsl + ); let is_color_function = absolute .color .flags .contains(SerializationFlags::AS_COLOR_FUNCTION); let pref_enabled = allow_more_color_4(); - (is_srgb && !is_color_function) || pref_enabled + (is_legacy_color && !is_color_function) || pref_enabled }; if !enabled { return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); @@ -731,23 +798,14 @@ impl Color { if !allow_quirks.allowed(context.quirks_mode) { return Err(e); } - Color::parse_quirky_color(input) - .map(|rgba| { - Color::from_absolute_color(AbsoluteColor::srgb( - rgba.red as f32 / 255.0, - rgba.green as f32 / 255.0, - rgba.blue as f32 / 255.0, - rgba.alpha, // alpha value is already a float and in range [0..1] - )) - }) - .map_err(|_| e) + Color::parse_quirky_color(input).map_err(|_| e) }) } /// Parse a value. /// /// - fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { let location = input.current_source_location(); let (value, unit) = match *input.next()? { Token::Number { @@ -763,7 +821,7 @@ impl Color { if ident.len() != 3 && ident.len() != 6 { return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); } - return RGBA::parse_hash(ident.as_bytes()).map_err(|()| { + return cssparser::parse_hash_color(ident.as_bytes()).map_err(|()| { location.new_custom_error(StyleParseErrorKind::UnspecifiedError) }); }, @@ -808,7 +866,7 @@ impl Color { .unwrap(); } debug_assert_eq!(written, 6); - RGBA::parse_hash(&serialization) + cssparser::parse_hash_color(&serialization) .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) } }