Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Color support functions.
/// cbindgen:ignore
mod color_function;
/// cbindgen:ignore
pub mod convert;
pub mod component;
pub mod mix;
pub mod parsing;
mod to_css;
use self::parsing::ChannelKeyword;
use component::ColorComponent;
use cssparser::color::PredefinedColorSpace;
/// The 3 components that make up a color. (Does not include the alpha component)
#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
#[repr(C)]
pub struct ColorComponents(pub f32, pub f32, pub f32);
impl ColorComponents {
/// Apply a function to each of the 3 components of the color.
#[must_use]
pub fn map(self, f: impl Fn(f32) -> f32) -> Self {
Self(f(self.0), f(self.1), f(self.2))
}
}
impl std::ops::Mul for ColorComponents {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Self(self.0 * rhs.0, self.1 * rhs.1, self.2 * rhs.2)
}
}
impl std::ops::Div for ColorComponents {
type Output = Self;
fn div(self, rhs: Self) -> Self::Output {
Self(self.0 / rhs.0, self.1 / rhs.1, self.2 / rhs.2)
}
}
/// A color space representation in the CSS specification.
///
#[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%)"
Hsl,
/// A color specified in the Hwb notation in the sRGB color space, e.g.
/// "hwb(740deg 20% 30%)"
Hwb,
/// A color specified in the Lab color format, e.g.
/// "lab(29.2345% 39.3825 20.0664)".
Lab,
/// A color specified in the Lch color format, e.g.
/// "lch(29.2345% 44.2 27)".
Lch,
/// A color specified in the Oklab color format, e.g.
/// "oklab(40.101% 0.1147 0.0453)".
Oklab,
/// A color specified in the Oklch color format, e.g.
/// "oklch(40.101% 0.12332 21.555)".
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)".
/// 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:
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 From<ColorComponent<f32>> for ComponentDetails {
fn from(value: ColorComponent<f32>) -> Self {
if let ColorComponent::Value(value) = value {
Self {
value,
is_none: false,
}
} else {
Self {
value: 0.0,
is_none: true,
}
}
}
}
impl AbsoluteColor {
/// A fully transparent color in the legacy syntax.
pub const TRANSPARENT_BLACK: Self = Self {
components: ColorComponents(0.0, 0.0, 0.0),
alpha: 0.0,
color_space: ColorSpace::Srgb,
flags: ColorFlags::IS_LEGACY_SRGB,
};
/// An opaque black color in the legacy syntax.
pub const BLACK: Self = Self {
components: ColorComponents(0.0, 0.0, 0.0),
alpha: 1.0,
color_space: ColorSpace::Srgb,
flags: ColorFlags::IS_LEGACY_SRGB,
};
/// An opaque white color in the legacy syntax.
pub const WHITE: Self = Self {
components: ColorComponents(1.0, 1.0, 1.0),
alpha: 1.0,
color_space: ColorSpace::Srgb,
flags: ColorFlags::IS_LEGACY_SRGB,
};
/// Create a new [`AbsoluteColor`] with the given [`ColorSpace`] and
/// components.
pub fn new(
color_space: ColorSpace,
c1: impl Into<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)
}
}
/// Return the value of a component by its channel keyword.
pub fn get_component_by_channel_keyword(
&self,
channel_keyword: ChannelKeyword,
) -> Result<Option<f32>, ()> {
if channel_keyword == ChannelKeyword::Alpha {
return Ok(self.alpha());
}
Ok(match self.color_space {
ColorSpace::Srgb => {
if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) {
match channel_keyword {
ChannelKeyword::R => self.c0().map(|v| v * 255.0),
ChannelKeyword::G => self.c1().map(|v| v * 255.0),
ChannelKeyword::B => self.c2().map(|v| v * 255.0),
_ => return Err(()),
}
} else {
match channel_keyword {
ChannelKeyword::R => self.c0(),
ChannelKeyword::G => self.c1(),
ChannelKeyword::B => self.c2(),
_ => return Err(()),
}
}
},
ColorSpace::Hsl => match channel_keyword {
ChannelKeyword::H => self.c0(),
ChannelKeyword::S => self.c1(),
ChannelKeyword::L => self.c2(),
_ => return Err(()),
},
ColorSpace::Hwb => match channel_keyword {
ChannelKeyword::H => self.c0(),
ChannelKeyword::W => self.c1(),
ChannelKeyword::B => self.c2(),
_ => return Err(()),
},
ColorSpace::Lab | ColorSpace::Oklab => match channel_keyword {
ChannelKeyword::L => self.c0(),
ChannelKeyword::A => self.c1(),
ChannelKeyword::B => self.c2(),
_ => return Err(()),
},
ColorSpace::Lch | ColorSpace::Oklch => match channel_keyword {
ChannelKeyword::L => self.c0(),
ChannelKeyword::C => self.c1(),
ChannelKeyword::H => self.c2(),
_ => return Err(()),
},
ColorSpace::SrgbLinear |
ColorSpace::DisplayP3 |
ColorSpace::A98Rgb |
ColorSpace::ProphotoRgb |
ColorSpace::Rec2020 => match channel_keyword {
ChannelKeyword::R => self.c0(),
ChannelKeyword::G => self.c1(),
ChannelKeyword::B => self.c2(),
_ => return Err(()),
},
ColorSpace::XyzD50 | ColorSpace::XyzD65 => match channel_keyword {
ChannelKeyword::X => self.c0(),
ChannelKeyword::Y => self.c1(),
ChannelKeyword::Z => self.c2(),
_ => return Err(()),
},
})
}
/// 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,
}
}
}