tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

convert.rs (29743B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
      4 
      5 //! Color conversion algorithms.
      6 //!
      7 //! Algorithms, matrices and constants are from the [color-4] specification,
      8 //! unless otherwise specified:
      9 //!
     10 //! https://drafts.csswg.org/css-color-4/#color-conversion-code
     11 //!
     12 //! NOTE: Matrices has to be transposed from the examples in the spec for use
     13 //! with the `euclid` library.
     14 
     15 use crate::color::ColorComponents;
     16 use crate::values::normalize;
     17 
     18 type Transform = euclid::default::Transform3D<f32>;
     19 type Vector = euclid::default::Vector3D<f32>;
     20 
     21 /// Normalize hue into [0, 360).
     22 #[inline]
     23 pub fn normalize_hue(hue: f32) -> f32 {
     24    hue - 360. * (hue / 360.).floor()
     25 }
     26 
     27 /// Calculate the hue from RGB components and return it along with the min and
     28 /// max RGB values.
     29 #[inline]
     30 fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) {
     31    let max = red.max(green).max(blue);
     32    let min = red.min(green).min(blue);
     33 
     34    let delta = max - min;
     35 
     36    let hue = if delta != 0.0 {
     37        60.0 * if max == red {
     38            (green - blue) / delta + if green < blue { 6.0 } else { 0.0 }
     39        } else if max == green {
     40            (blue - red) / delta + 2.0
     41        } else {
     42            (red - green) / delta + 4.0
     43        }
     44    } else {
     45        f32::NAN
     46    };
     47 
     48    (hue, min, max)
     49 }
     50 
     51 /// Convert from HSL notation to RGB notation.
     52 /// https://drafts.csswg.org/css-color-4/#hsl-to-rgb
     53 #[inline]
     54 pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents {
     55    fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 {
     56        let hue = normalize_hue(hue);
     57 
     58        if hue * 6.0 < 360.0 {
     59            t1 + (t2 - t1) * hue / 60.0
     60        } else if hue * 2.0 < 360.0 {
     61            t2
     62        } else if hue * 3.0 < 720.0 {
     63            t1 + (t2 - t1) * (240.0 - hue) / 60.0
     64        } else {
     65            t1
     66        }
     67    }
     68 
     69    // Convert missing components to 0.0.
     70    let ColorComponents(hue, saturation, lightness) = from.map(normalize);
     71    let saturation = saturation / 100.0;
     72    let lightness = lightness / 100.0;
     73 
     74    let t2 = if lightness <= 0.5 {
     75        lightness * (saturation + 1.0)
     76    } else {
     77        lightness + saturation - lightness * saturation
     78    };
     79    let t1 = lightness * 2.0 - t2;
     80 
     81    ColorComponents(
     82        hue_to_rgb(t1, t2, hue + 120.0),
     83        hue_to_rgb(t1, t2, hue),
     84        hue_to_rgb(t1, t2, hue - 120.0),
     85    )
     86 }
     87 
     88 /// Convert from RGB notation to HSL notation.
     89 /// https://drafts.csswg.org/css-color-4/#rgb-to-hsl
     90 pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents {
     91    let ColorComponents(red, green, blue) = *from;
     92 
     93    let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
     94 
     95    let lightness = (min + max) / 2.0;
     96    let delta = max - min;
     97 
     98    let saturation = if delta != 0.0 {
     99        if lightness == 0.0 || lightness == 1.0 {
    100            0.0
    101        } else {
    102            (max - lightness) / lightness.min(1.0 - lightness)
    103        }
    104    } else {
    105        0.0
    106    };
    107 
    108    ColorComponents(hue, saturation * 100.0, lightness * 100.0)
    109 }
    110 
    111 /// Convert from HWB notation to RGB notation.
    112 /// https://drafts.csswg.org/css-color-4/#hwb-to-rgb
    113 #[inline]
    114 pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents {
    115    // Convert missing components to 0.0.
    116    let ColorComponents(hue, whiteness, blackness) = from.map(normalize);
    117 
    118    let whiteness = whiteness / 100.0;
    119    let blackness = blackness / 100.0;
    120 
    121    if whiteness + blackness >= 1.0 {
    122        let gray = whiteness / (whiteness + blackness);
    123        return ColorComponents(gray, gray, gray);
    124    }
    125 
    126    let x = 1.0 - whiteness - blackness;
    127    hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness)
    128 }
    129 
    130 /// Convert from RGB notation to HWB notation.
    131 /// https://drafts.csswg.org/css-color-4/#rgb-to-hwb
    132 #[inline]
    133 pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents {
    134    let ColorComponents(red, green, blue) = *from;
    135 
    136    let (hue, min, max) = rgb_to_hue_min_max(red, green, blue);
    137 
    138    let whiteness = min;
    139    let blackness = 1.0 - max;
    140 
    141    ColorComponents(hue, whiteness * 100.0, blackness * 100.0)
    142 }
    143 
    144 /// Calculate an epsilon for a specified range.
    145 #[inline]
    146 pub fn epsilon_for_range(min: f32, max: f32) -> f32 {
    147    (max - min) / 1.0e5
    148 }
    149 
    150 /// Convert from the rectangular orthogonal to the cylindrical polar coordinate
    151 /// system. This is used to convert (ok)lab to (ok)lch.
    152 /// <https://drafts.csswg.org/css-color-4/#lab-to-lch>
    153 #[inline]
    154 pub fn orthogonal_to_polar(from: &ColorComponents, e: f32) -> ColorComponents {
    155    let ColorComponents(lightness, a, b) = *from;
    156 
    157    let chroma = (a * a + b * b).sqrt();
    158 
    159    let hue = if a.abs() < e && b.abs() < e {
    160        // For extremely small values of a and b ... the reported hue angle
    161        // swinging about wildly and being essentially random ... this means
    162        // the hue is powerless, and treated as missing when converted into LCH
    163        // or Oklch.
    164        f32::NAN
    165    } else if chroma.abs() < e {
    166        // Very small chroma values make the hue component powerless.
    167        f32::NAN
    168    } else {
    169        normalize_hue(b.atan2(a).to_degrees())
    170    };
    171 
    172    ColorComponents(lightness, chroma, hue)
    173 }
    174 
    175 /// Convert from the cylindrical polar to the rectangular orthogonal coordinate
    176 /// system. This is used to convert (ok)lch to (ok)lab.
    177 /// <https://drafts.csswg.org/css-color-4/#lch-to-lab>
    178 #[inline]
    179 pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents {
    180    let ColorComponents(lightness, chroma, hue) = *from;
    181 
    182    // A missing hue component results in an achromatic color.
    183    if hue.is_nan() {
    184        return ColorComponents(lightness, 0.0, 0.0);
    185    }
    186 
    187    let hue = hue.to_radians();
    188    let a = chroma * hue.cos();
    189    let b = chroma * hue.sin();
    190 
    191    ColorComponents(lightness, a, b)
    192 }
    193 
    194 #[inline]
    195 fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents {
    196    let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2));
    197    ColorComponents(result.x, result.y, result.z)
    198 }
    199 
    200 fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents {
    201    #[rustfmt::skip]
    202    const MAT: Transform = Transform::new(
    203         1.0479298208405488,    0.029627815688159344, -0.009243058152591178, 0.0,
    204         0.022946793341019088,  0.990434484573249,     0.015055144896577895, 0.0,
    205        -0.05019222954313557,  -0.01707382502938514,   0.7518742899580008,   0.0,
    206         0.0,                   0.0,                   0.0,                  1.0,
    207    );
    208 
    209    transform(from, &MAT)
    210 }
    211 
    212 fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents {
    213    #[rustfmt::skip]
    214    const MAT: Transform = Transform::new(
    215         0.9554734527042182,   -0.028369706963208136,  0.012314001688319899, 0.0,
    216        -0.023098536874261423,  1.0099954580058226,   -0.020507696433477912, 0.0,
    217         0.0632593086610217,    0.021041398966943008,  1.3303659366080753,   0.0,
    218         0.0,                   0.0,                   0.0,                  1.0,
    219    );
    220 
    221    transform(from, &MAT)
    222 }
    223 
    224 /// A reference white that is used during color conversion.
    225 pub enum WhitePoint {
    226    /// D50 white reference.
    227    D50,
    228    /// D65 white reference.
    229    D65,
    230 }
    231 
    232 impl WhitePoint {
    233    const fn values(&self) -> ColorComponents {
    234        // <https://drafts.csswg.org/css-color-4/#color-conversion-code>
    235        match self {
    236            // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585]
    237            WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602),
    238            // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290]
    239            WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784),
    240        }
    241    }
    242 }
    243 
    244 fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) {
    245    match (from, to) {
    246        (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components),
    247        (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components),
    248        _ => {},
    249    }
    250 }
    251 
    252 /// A trait that allows conversion of color spaces to and from XYZ coordinate
    253 /// space with a specified white point.
    254 ///
    255 /// Allows following the specified method of converting between color spaces:
    256 /// - Convert to values to sRGB linear light.
    257 /// - Convert to XYZ coordinate space.
    258 /// - Adjust white point to target white point.
    259 /// - Convert to sRGB linear light in target color space.
    260 /// - Convert to sRGB gamma encoded in target color space.
    261 ///
    262 /// https://drafts.csswg.org/css-color-4/#color-conversion
    263 pub trait ColorSpaceConversion {
    264    /// The white point that the implementer is represented in.
    265    const WHITE_POINT: WhitePoint;
    266 
    267    /// Convert the components from sRGB gamma encoded values to sRGB linear
    268    /// light values.
    269    fn to_linear_light(from: &ColorComponents) -> ColorComponents;
    270 
    271    /// Convert the components from sRGB linear light values to XYZ coordinate
    272    /// space.
    273    fn to_xyz(from: &ColorComponents) -> ColorComponents;
    274 
    275    /// Convert the components from XYZ coordinate space to sRGB linear light
    276    /// values.
    277    fn from_xyz(from: &ColorComponents) -> ColorComponents;
    278 
    279    /// Convert the components from sRGB linear light values to sRGB gamma
    280    /// encoded values.
    281    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents;
    282 }
    283 
    284 /// Convert the color components from the specified color space to XYZ and
    285 /// return the components and the white point they are in.
    286 pub fn to_xyz<From: ColorSpaceConversion>(from: &ColorComponents) -> (ColorComponents, WhitePoint) {
    287    // Convert the color components where in-gamut values are in the range
    288    // [0 - 1] to linear light (un-companded) form.
    289    let result = From::to_linear_light(from);
    290 
    291    // Convert the color components from the source color space to XYZ.
    292    (From::to_xyz(&result), From::WHITE_POINT)
    293 }
    294 
    295 /// Convert the color components from XYZ at the given white point to the
    296 /// specified color space.
    297 pub fn from_xyz<To: ColorSpaceConversion>(
    298    from: &ColorComponents,
    299    white_point: WhitePoint,
    300 ) -> ColorComponents {
    301    let mut xyz = from.clone();
    302 
    303    // Convert the white point if needed.
    304    convert_white_point(white_point, To::WHITE_POINT, &mut xyz);
    305 
    306    // Convert the color from XYZ to the target color space.
    307    let result = To::from_xyz(&xyz);
    308 
    309    // Convert the color components of linear-light values in the range
    310    // [0 - 1] to a gamma corrected form.
    311    To::to_gamma_encoded(&result)
    312 }
    313 
    314 /// The sRGB color space.
    315 /// https://drafts.csswg.org/css-color-4/#predefined-sRGB
    316 pub struct Srgb;
    317 
    318 impl Srgb {
    319    #[rustfmt::skip]
    320    const TO_XYZ: Transform = Transform::new(
    321        0.4123907992659595,  0.21263900587151036, 0.01933081871559185, 0.0,
    322        0.35758433938387796, 0.7151686787677559,  0.11919477979462599, 0.0,
    323        0.1804807884018343,  0.07219231536073371, 0.9505321522496606,  0.0,
    324        0.0,                 0.0,                 0.0,                 1.0,
    325    );
    326 
    327    #[rustfmt::skip]
    328    const FROM_XYZ: Transform = Transform::new(
    329         3.2409699419045213, -0.9692436362808798,  0.05563007969699361, 0.0,
    330        -1.5373831775700935,  1.8759675015077206, -0.20397695888897657, 0.0,
    331        -0.4986107602930033,  0.04155505740717561, 1.0569715142428786,  0.0,
    332         0.0,                 0.0,                 0.0,                 1.0,
    333    );
    334 }
    335 
    336 impl ColorSpaceConversion for Srgb {
    337    const WHITE_POINT: WhitePoint = WhitePoint::D65;
    338 
    339    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    340        from.clone().map(|value| {
    341            let abs = value.abs();
    342 
    343            if abs < 0.04045 {
    344                value / 12.92
    345            } else {
    346                value.signum() * ((abs + 0.055) / 1.055).powf(2.4)
    347            }
    348        })
    349    }
    350 
    351    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    352        transform(from, &Self::TO_XYZ)
    353    }
    354 
    355    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    356        transform(from, &Self::FROM_XYZ)
    357    }
    358 
    359    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    360        from.clone().map(|value| {
    361            let abs = value.abs();
    362 
    363            if abs > 0.0031308 {
    364                value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055)
    365            } else {
    366                12.92 * value
    367            }
    368        })
    369    }
    370 }
    371 
    372 /// Color specified with hue, saturation and lightness components.
    373 pub struct Hsl;
    374 
    375 impl ColorSpaceConversion for Hsl {
    376    const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
    377 
    378    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    379        Srgb::to_linear_light(&hsl_to_rgb(from))
    380    }
    381 
    382    #[inline]
    383    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    384        Srgb::to_xyz(from)
    385    }
    386 
    387    #[inline]
    388    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    389        Srgb::from_xyz(from)
    390    }
    391 
    392    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    393        rgb_to_hsl(&Srgb::to_gamma_encoded(from))
    394    }
    395 }
    396 
    397 /// Color specified with hue, whiteness and blackness components.
    398 pub struct Hwb;
    399 
    400 impl ColorSpaceConversion for Hwb {
    401    const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
    402 
    403    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    404        Srgb::to_linear_light(&hwb_to_rgb(from))
    405    }
    406 
    407    #[inline]
    408    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    409        Srgb::to_xyz(from)
    410    }
    411 
    412    #[inline]
    413    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    414        Srgb::from_xyz(from)
    415    }
    416 
    417    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    418        rgb_to_hwb(&Srgb::to_gamma_encoded(from))
    419    }
    420 }
    421 
    422 /// The same as sRGB color space, except the transfer function is linear light.
    423 /// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear
    424 pub struct SrgbLinear;
    425 
    426 impl ColorSpaceConversion for SrgbLinear {
    427    const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT;
    428 
    429    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    430        // Already in linear light form.
    431        from.clone()
    432    }
    433 
    434    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    435        Srgb::to_xyz(from)
    436    }
    437 
    438    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    439        Srgb::from_xyz(from)
    440    }
    441 
    442    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    443        // Stay in linear light form.
    444        from.clone()
    445    }
    446 }
    447 
    448 /// The Display-P3 color space.
    449 /// https://drafts.csswg.org/css-color-4/#predefined-display-p3
    450 pub struct DisplayP3;
    451 
    452 impl DisplayP3 {
    453    #[rustfmt::skip]
    454    const TO_XYZ: Transform = Transform::new(
    455        0.48657094864821626, 0.22897456406974884, 0.0,                  0.0,
    456        0.26566769316909294, 0.6917385218365062,  0.045113381858902575, 0.0,
    457        0.1982172852343625,  0.079286914093745,   1.0439443689009757,   0.0,
    458        0.0,                 0.0,                 0.0,                  1.0,
    459    );
    460 
    461    #[rustfmt::skip]
    462    const FROM_XYZ: Transform = Transform::new(
    463         2.4934969119414245,  -0.829488969561575,    0.035845830243784335, 0.0,
    464        -0.9313836179191236,   1.7626640603183468,  -0.07617238926804171,  0.0,
    465        -0.40271078445071684,  0.02362468584194359,  0.9568845240076873,   0.0,
    466         0.0,                  0.0,                  0.0,                  1.0,
    467    );
    468 }
    469 
    470 impl ColorSpaceConversion for DisplayP3 {
    471    const WHITE_POINT: WhitePoint = WhitePoint::D65;
    472 
    473    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    474        Srgb::to_linear_light(from)
    475    }
    476 
    477    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    478        transform(from, &Self::TO_XYZ)
    479    }
    480 
    481    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    482        transform(from, &Self::FROM_XYZ)
    483    }
    484 
    485    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    486        Srgb::to_gamma_encoded(from)
    487    }
    488 }
    489 
    490 /// The Display-P3-linear color space. This is basically display-p3 without gamma encoding.
    491 pub struct DisplayP3Linear;
    492 impl ColorSpaceConversion for DisplayP3Linear {
    493    const WHITE_POINT: WhitePoint = DisplayP3::WHITE_POINT;
    494 
    495    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    496        *from
    497    }
    498 
    499    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    500        DisplayP3::to_xyz(from)
    501    }
    502 
    503    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    504        DisplayP3::from_xyz(from)
    505    }
    506 
    507    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    508        *from
    509    }
    510 }
    511 
    512 /// The a98-rgb color space.
    513 /// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb
    514 pub struct A98Rgb;
    515 
    516 impl A98Rgb {
    517    #[rustfmt::skip]
    518    const TO_XYZ: Transform = Transform::new(
    519        0.5766690429101308,  0.29734497525053616, 0.027031361386412378, 0.0,
    520        0.18555823790654627, 0.627363566255466,   0.07068885253582714,  0.0,
    521        0.18822864623499472, 0.07529145849399789, 0.9913375368376389,   0.0,
    522        0.0,                 0.0,                 0.0,                  1.0,
    523    );
    524 
    525    #[rustfmt::skip]
    526    const FROM_XYZ: Transform = Transform::new(
    527         2.041587903810746,  -0.9692436362808798,   0.013444280632031024, 0.0,
    528        -0.5650069742788596,  1.8759675015077206,  -0.11836239223101824,  0.0,
    529        -0.3447313507783295,  0.04155505740717561,  1.0151749943912054,   0.0,
    530         0.0,                 0.0,                  0.0,                  1.0,
    531    );
    532 }
    533 
    534 impl ColorSpaceConversion for A98Rgb {
    535    const WHITE_POINT: WhitePoint = WhitePoint::D65;
    536 
    537    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    538        from.clone().map(|v| v.signum() * v.abs().powf(2.19921875))
    539    }
    540 
    541    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    542        transform(from, &Self::TO_XYZ)
    543    }
    544 
    545    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    546        transform(from, &Self::FROM_XYZ)
    547    }
    548 
    549    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    550        from.clone()
    551            .map(|v| v.signum() * v.abs().powf(0.4547069271758437))
    552    }
    553 }
    554 
    555 /// The ProPhoto RGB color space.
    556 /// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb
    557 pub struct ProphotoRgb;
    558 
    559 impl ProphotoRgb {
    560    #[rustfmt::skip]
    561    const TO_XYZ: Transform = Transform::new(
    562        0.7977604896723027,  0.2880711282292934,     0.0,                0.0,
    563        0.13518583717574031, 0.7118432178101014,     0.0,                0.0,
    564        0.0313493495815248,  0.00008565396060525902, 0.8251046025104601, 0.0,
    565        0.0,                 0.0,                    0.0,                1.0,
    566    );
    567 
    568    #[rustfmt::skip]
    569    const FROM_XYZ: Transform = Transform::new(
    570         1.3457989731028281,  -0.5446224939028347,  0.0,                0.0,
    571        -0.25558010007997534,  1.5082327413132781,  0.0,                0.0,
    572        -0.05110628506753401,  0.02053603239147973, 1.2119675456389454, 0.0,
    573         0.0,                  0.0,                 0.0,                1.0,
    574    );
    575 }
    576 
    577 impl ColorSpaceConversion for ProphotoRgb {
    578    const WHITE_POINT: WhitePoint = WhitePoint::D50;
    579 
    580    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    581        from.clone().map(|value| {
    582            const ET2: f32 = 16.0 / 512.0;
    583 
    584            let abs = value.abs();
    585 
    586            if abs <= ET2 {
    587                value / 16.0
    588            } else {
    589                value.signum() * abs.powf(1.8)
    590            }
    591        })
    592    }
    593 
    594    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    595        transform(from, &Self::TO_XYZ)
    596    }
    597 
    598    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    599        transform(from, &Self::FROM_XYZ)
    600    }
    601 
    602    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    603        const ET: f32 = 1.0 / 512.0;
    604 
    605        from.clone().map(|v| {
    606            let abs = v.abs();
    607            if abs >= ET {
    608                v.signum() * abs.powf(1.0 / 1.8)
    609            } else {
    610                16.0 * v
    611            }
    612        })
    613    }
    614 }
    615 
    616 /// The Rec.2020 color space.
    617 /// https://drafts.csswg.org/css-color-4/#predefined-rec2020
    618 pub struct Rec2020;
    619 
    620 impl Rec2020 {
    621    const ALPHA: f32 = 1.09929682680944;
    622    const BETA: f32 = 0.018053968510807;
    623 
    624    #[rustfmt::skip]
    625    const TO_XYZ: Transform = Transform::new(
    626        0.6369580483012913,  0.26270021201126703,  0.0,                  0.0,
    627        0.14461690358620838, 0.677998071518871,    0.028072693049087508, 0.0,
    628        0.16888097516417205, 0.059301716469861945, 1.0609850577107909,   0.0,
    629        0.0,                 0.0,                  0.0,                  1.0,
    630    );
    631 
    632    #[rustfmt::skip]
    633    const FROM_XYZ: Transform = Transform::new(
    634         1.7166511879712676, -0.666684351832489,    0.017639857445310915, 0.0,
    635        -0.3556707837763924,  1.616481236634939,   -0.042770613257808655, 0.0,
    636        -0.2533662813736598,  0.01576854581391113,  0.942103121235474,    0.0,
    637         0.0,                 0.0,                  0.0,                  1.0,
    638    );
    639 }
    640 
    641 impl ColorSpaceConversion for Rec2020 {
    642    const WHITE_POINT: WhitePoint = WhitePoint::D65;
    643 
    644    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    645        from.clone().map(|value| {
    646            let abs = value.abs();
    647 
    648            if abs < Self::BETA * 4.5 {
    649                value / 4.5
    650            } else {
    651                value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45)
    652            }
    653        })
    654    }
    655 
    656    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    657        transform(from, &Self::TO_XYZ)
    658    }
    659 
    660    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    661        transform(from, &Self::FROM_XYZ)
    662    }
    663 
    664    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    665        from.clone().map(|v| {
    666            let abs = v.abs();
    667 
    668            if abs > Self::BETA {
    669                v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0))
    670            } else {
    671                4.5 * v
    672            }
    673        })
    674    }
    675 }
    676 
    677 /// A color in the XYZ coordinate space with a D50 white reference.
    678 /// https://drafts.csswg.org/css-color-4/#predefined-xyz
    679 pub struct XyzD50;
    680 
    681 impl ColorSpaceConversion for XyzD50 {
    682    const WHITE_POINT: WhitePoint = WhitePoint::D50;
    683 
    684    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    685        from.clone()
    686    }
    687 
    688    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    689        from.clone()
    690    }
    691 
    692    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    693        from.clone()
    694    }
    695 
    696    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    697        from.clone()
    698    }
    699 }
    700 
    701 /// A color in the XYZ coordinate space with a D65 white reference.
    702 /// https://drafts.csswg.org/css-color-4/#predefined-xyz
    703 pub struct XyzD65;
    704 
    705 impl ColorSpaceConversion for XyzD65 {
    706    const WHITE_POINT: WhitePoint = WhitePoint::D65;
    707 
    708    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    709        from.clone()
    710    }
    711 
    712    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    713        from.clone()
    714    }
    715 
    716    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    717        from.clone()
    718    }
    719 
    720    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    721        from.clone()
    722    }
    723 }
    724 
    725 /// The Lab color space.
    726 /// https://drafts.csswg.org/css-color-4/#specifying-lab-lch
    727 pub struct Lab;
    728 
    729 impl Lab {
    730    const KAPPA: f32 = 24389.0 / 27.0;
    731    const EPSILON: f32 = 216.0 / 24389.0;
    732 }
    733 
    734 impl ColorSpaceConversion for Lab {
    735    const WHITE_POINT: WhitePoint = WhitePoint::D50;
    736 
    737    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    738        // No need for conversion.
    739        from.clone()
    740    }
    741 
    742    /// Convert a CIELAB color to XYZ as specified in [1] and [2].
    743    ///
    744    /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined
    745    /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code
    746    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    747        let ColorComponents(lightness, a, b) = *from;
    748 
    749        let f1 = (lightness + 16.0) / 116.0;
    750        let f0 = f1 + a / 500.0;
    751        let f2 = f1 - b / 200.0;
    752 
    753        let f0_cubed = f0 * f0 * f0;
    754        let x = if f0_cubed > Self::EPSILON {
    755            f0_cubed
    756        } else {
    757            (116.0 * f0 - 16.0) / Self::KAPPA
    758        };
    759 
    760        let y = if lightness > Self::KAPPA * Self::EPSILON {
    761            let v = (lightness + 16.0) / 116.0;
    762            v * v * v
    763        } else {
    764            lightness / Self::KAPPA
    765        };
    766 
    767        let f2_cubed = f2 * f2 * f2;
    768        let z = if f2_cubed > Self::EPSILON {
    769            f2_cubed
    770        } else {
    771            (116.0 * f2 - 16.0) / Self::KAPPA
    772        };
    773 
    774        ColorComponents(x, y, z) * Self::WHITE_POINT.values()
    775    }
    776 
    777    /// Convert an XYZ color to LAB as specified in [1] and [2].
    778    ///
    779    /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab
    780    /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code
    781    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    782        let adapted = *from / Self::WHITE_POINT.values();
    783 
    784        // 4. Convert D50-adapted XYZ to Lab.
    785        let ColorComponents(f0, f1, f2) = adapted.map(|v| {
    786            if v > Self::EPSILON {
    787                v.cbrt()
    788            } else {
    789                (Self::KAPPA * v + 16.0) / 116.0
    790            }
    791        });
    792 
    793        let lightness = 116.0 * f1 - 16.0;
    794        let a = 500.0 * (f0 - f1);
    795        let b = 200.0 * (f1 - f2);
    796 
    797        ColorComponents(lightness, a, b)
    798    }
    799 
    800    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    801        // No need for conversion.
    802        from.clone()
    803    }
    804 }
    805 
    806 /// The Lch color space.
    807 /// https://drafts.csswg.org/css-color-4/#specifying-lab-lch
    808 pub struct Lch;
    809 
    810 impl ColorSpaceConversion for Lch {
    811    const WHITE_POINT: WhitePoint = Lab::WHITE_POINT;
    812 
    813    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    814        // No need for conversion.
    815        from.clone()
    816    }
    817 
    818    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    819        // Convert LCH to Lab first.
    820        let lab = polar_to_orthogonal(from);
    821 
    822        // Then convert the Lab to XYZ.
    823        Lab::to_xyz(&lab)
    824    }
    825 
    826    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    827        // First convert the XYZ to LAB.
    828        let lab = Lab::from_xyz(&from);
    829 
    830        // Then convert the Lab to LCH.
    831        orthogonal_to_polar(&lab, epsilon_for_range(0.0, 100.0))
    832    }
    833 
    834    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    835        // No need for conversion.
    836        from.clone()
    837    }
    838 }
    839 
    840 /// The Oklab color space.
    841 /// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
    842 pub struct Oklab;
    843 
    844 impl Oklab {
    845    #[rustfmt::skip]
    846    const XYZ_TO_LMS: Transform = Transform::new(
    847         0.8190224432164319,  0.0329836671980271,  0.048177199566046255, 0.0,
    848         0.3619062562801221,  0.9292868468965546,  0.26423952494422764,  0.0,
    849        -0.12887378261216414, 0.03614466816999844, 0.6335478258136937,   0.0,
    850         0.0,                 0.0,                 0.0,                  1.0,
    851    );
    852 
    853    #[rustfmt::skip]
    854    const LMS_TO_OKLAB: Transform = Transform::new(
    855         0.2104542553,  1.9779984951,  0.0259040371, 0.0,
    856         0.7936177850, -2.4285922050,  0.7827717662, 0.0,
    857        -0.0040720468,  0.4505937099, -0.8086757660, 0.0,
    858         0.0,           0.0,           0.0,          1.0,
    859    );
    860 
    861    #[rustfmt::skip]
    862    const LMS_TO_XYZ: Transform = Transform::new(
    863         1.2268798733741557,  -0.04057576262431372, -0.07637294974672142, 0.0,
    864        -0.5578149965554813,   1.1122868293970594,  -0.4214933239627914,  0.0,
    865         0.28139105017721583, -0.07171106666151701,  1.5869240244272418,  0.0,
    866         0.0,                  0.0,                  0.0,                 1.0,
    867    );
    868 
    869    #[rustfmt::skip]
    870    const OKLAB_TO_LMS: Transform = Transform::new(
    871        0.99999999845051981432,  1.0000000088817607767,    1.0000000546724109177,   0.0,
    872        0.39633779217376785678, -0.1055613423236563494,   -0.089484182094965759684, 0.0,
    873        0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399,   0.0,
    874        0.0,                     0.0,                      0.0,                     1.0,
    875    );
    876 }
    877 
    878 impl ColorSpaceConversion for Oklab {
    879    const WHITE_POINT: WhitePoint = WhitePoint::D65;
    880 
    881    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    882        // No need for conversion.
    883        from.clone()
    884    }
    885 
    886    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    887        let lms = transform(&from, &Self::OKLAB_TO_LMS);
    888        let lms = lms.map(|v| v * v * v);
    889        transform(&lms, &Self::LMS_TO_XYZ)
    890    }
    891 
    892    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    893        let lms = transform(&from, &Self::XYZ_TO_LMS);
    894        let lms = lms.map(|v| v.cbrt());
    895        transform(&lms, &Self::LMS_TO_OKLAB)
    896    }
    897 
    898    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    899        // No need for conversion.
    900        from.clone()
    901    }
    902 }
    903 
    904 /// The Oklch color space.
    905 /// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch
    906 pub struct Oklch;
    907 
    908 impl ColorSpaceConversion for Oklch {
    909    const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT;
    910 
    911    fn to_linear_light(from: &ColorComponents) -> ColorComponents {
    912        // No need for conversion.
    913        from.clone()
    914    }
    915 
    916    fn to_xyz(from: &ColorComponents) -> ColorComponents {
    917        // First convert OkLCH to Oklab.
    918        let oklab = polar_to_orthogonal(from);
    919 
    920        // Then convert Oklab to XYZ.
    921        Oklab::to_xyz(&oklab)
    922    }
    923 
    924    fn from_xyz(from: &ColorComponents) -> ColorComponents {
    925        // First convert XYZ to Oklab.
    926        let lab = Oklab::from_xyz(&from);
    927 
    928        // Then convert Oklab to OkLCH.
    929        orthogonal_to_polar(&lab, epsilon_for_range(0.0, 1.0))
    930    }
    931 
    932    fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents {
    933        // No need for conversion.
    934        from.clone()
    935    }
    936 }