palette/
color_difference.rs

1//! Algorithms for calculating the difference between colors.
2//!
3//! ## Selecting an algorithm
4//!
5//! Different distance/difference algorithms and formulas are good for different
6//! situations. Some are faster but less accurate and some may only be suitable
7//! for certain color spaces. This table may help navigating the options a bit
8//! by summarizing the difference between the traits in this module.
9//!
10//! **Disclaimer:** _This is not an actual benchmark! It's always best to test
11//! and evaluate the differences in an actual application, when possible._
12//!
13//! Property explanations:
14//! - **Complexity:** Low complexity options are generally faster than high
15//!   complexity options.
16//! - **Accuracy:** How the numerical difference compares to the perceived
17//!   difference. May differ with the color space.
18//!
19//! | Trait | Complexity | Accuracy | Notes |
20//! |-------|------------|----------|-------|
21//! | [`Ciede2000`] | High | High for small differences, lower for large differences | The de-facto standard, but requires complex calculations to compensate for increased errors in certain areas of the CIE L\*a\*b\* (CIELAB) space.
22//! | [`ImprovedCiede2000`] | High | High for small differences, lower for large differences | A general improvement of [`Ciede2000`], using a formula by Huang et al.
23//! | [`DeltaE`] | Usually low | Medium to high | The formula differs between color spaces and may not always be the best. Other formulas, such as [`Ciede2000`], may be preferred for some spaces.
24//! | [`ImprovedDeltaE`] | Usually low to medium | Medium to high | A general improvement of [`DeltaE`], using a formula by Huang et al.
25//! | [`EuclideanDistance`] | Low | Medium to high for perceptually uniform spaces, otherwise low | Can be good enough for perceptually uniform spaces or as a "quick and dirty" check.
26//! | [`HyAb`] | Low | High accuracy for medium to large differences. Less accurate than CIEDE2000 for small differences, but still performs well and is much less computationally expensive. | Similar to Euclidean distance, but separates lightness and chroma more. Limited to Cartesian spaces with a lightness axis and a chroma plane.
27//! | [`Wcag21RelativeContrast`] | Low | Low and only compares lightness | Meant for checking contrasts in computer graphics (such as between text and background colors), assuming sRGB. Mostly useful as a hint or for checking WCAG 2.1 compliance, considering the criticism it has received.
28
29use core::ops::{Add, BitAnd, BitOr, Div, Mul};
30
31use crate::{
32    angle::RealAngle,
33    bool_mask::{HasBoolMask, LazySelect},
34    convert::IntoColorUnclamped,
35    num::{
36        Abs, Arithmetics, Exp, Hypot, MinMax, One, PartialCmp, Powf, Powi, Real, Sqrt,
37        Trigonometry, Zero,
38    },
39    white_point::D65,
40    Lab, Lch, LinLuma,
41};
42
43/// A trait for calculating the color difference between two colors.
44#[deprecated(
45    since = "0.7.2",
46    note = "replaced by `palette::color_difference::Ciede2000`"
47)]
48pub trait ColorDifference {
49    /// The type of the calculated color difference.
50    type Scalar;
51
52    /// Return the difference or distance between two colors.
53    #[must_use]
54    fn get_color_difference(self, other: Self) -> Self::Scalar;
55}
56
57/// Calculate the CIEDE2000 Δ*E\** (Delta E) color difference between two
58/// colors.
59///
60/// CIEDE2000 is a formula by the CIE that calculates a distance metric, Δ*E\**
61/// (also known as Delta E), as an estimate of perceived color distance or
62/// difference. CIEDE2000 is an improvement over Δ*E* (see [`DeltaE`]) for CIE
63/// L\*a\*b\* and CIE L\*C\*h° (see [`Lab`] and [`Lch`]).
64///
65/// There is a "just noticeable difference" between two colors when the Δ*E\**
66/// is roughly greater than 1. Thus, the color difference is more suited for
67/// calculating small distances between colors as opposed to large differences.
68#[doc(alias = "ColorDifference")]
69pub trait Ciede2000 {
70    /// The type for the Δ*E\** (Delta E).
71    type Scalar;
72
73    /// Calculate the CIEDE2000 Δ*E\** (Delta E) color difference between `self` and `other`.
74    #[must_use]
75    fn difference(self, other: Self) -> Self::Scalar;
76}
77
78/// Calculate the CIEDE2000 Δ*E'* (improved IEDE2000 Δ*E\**) color difference.
79///
80/// The "improved CIEDE2000" uses the output of [`Ciede2000`] and enhances it
81/// according to *Power functions improving the performance of color-difference
82/// formulas* by Huang et al.
83pub trait ImprovedCiede2000: Ciede2000 {
84    /// Calculate the CIEDE2000 Δ*E'* (improved IEDE2000 Δ*E\**) color
85    /// difference between `self` and `other`.
86    #[must_use]
87    fn improved_difference(self, other: Self) -> Self::Scalar;
88}
89
90impl<C> ImprovedCiede2000 for C
91where
92    C: Ciede2000,
93    C::Scalar: Real + Mul<C::Scalar, Output = C::Scalar> + Powf,
94{
95    #[inline]
96    fn improved_difference(self, other: Self) -> Self::Scalar {
97        // Coefficients from "Power functions improving the performance of
98        // color-difference formulas" by Huang et al.
99        // https://opg.optica.org/oe/fulltext.cfm?uri=oe-23-1-597&id=307643
100        C::Scalar::from_f64(1.43) * self.difference(other).powf(C::Scalar::from_f64(0.7))
101    }
102}
103
104/// Container of components necessary to calculate CIEDE color difference
105pub(crate) struct LabColorDiff<T> {
106    /// Lab color lightness
107    pub l: T,
108    /// Lab color a* value
109    pub a: T,
110    /// Lab color b* value
111    pub b: T,
112    /// Lab color chroma value
113    pub chroma: T,
114}
115
116impl<Wp, T> From<Lab<Wp, T>> for LabColorDiff<T>
117where
118    T: Hypot + Clone,
119{
120    #[inline]
121    fn from(color: Lab<Wp, T>) -> Self {
122        // Color difference calculation requires Lab and chroma components. This
123        // function handles the conversion into those components which are then
124        // passed to `get_ciede_difference()` where calculation is completed.
125        LabColorDiff {
126            l: color.l,
127            a: color.a.clone(),
128            b: color.b.clone(),
129            chroma: color.a.hypot(color.b),
130        }
131    }
132}
133
134impl<Wp, T> From<Lch<Wp, T>> for LabColorDiff<T>
135where
136    T: Clone,
137    Lch<Wp, T>: IntoColorUnclamped<Lab<Wp, T>>,
138{
139    #[inline]
140    fn from(color: Lch<Wp, T>) -> Self {
141        let chroma = color.chroma.clone();
142        let Lab { l, a, b, .. } = color.into_color_unclamped();
143
144        LabColorDiff { l, a, b, chroma }
145    }
146}
147
148/// Calculate the CIEDE2000 color difference for two colors in Lab color space.
149/// There is a "just noticeable difference" between two colors when the delta E
150/// is roughly greater than 1. Thus, the color difference is more suited for
151/// calculating small distances between colors as opposed to large differences.
152#[rustfmt::skip]
153pub(crate) fn get_ciede2000_difference<T>(this: LabColorDiff<T>, other: LabColorDiff<T>) -> T
154where
155    T: Real
156        + RealAngle
157        + One
158        + Zero
159        + Trigonometry
160        + Abs
161        + Sqrt
162        + Powi
163        + Exp
164        + Arithmetics
165        + PartialCmp
166        + Clone,
167    T::Mask: LazySelect<T> + BitAnd<Output = T::Mask> + BitOr<Output = T::Mask>
168{
169    let c_bar = (this.chroma + other.chroma) / T::from_f64(2.0);
170    let c_bar_pow_seven = c_bar.powi(7);
171    let twenty_five_pow_seven = T::from_f64(6103515625.0);
172    let pi_over_180 = T::from_f64(core::f64::consts::PI / 180.0);
173
174    let g = T::from_f64(0.5)
175        * (T::one() - (c_bar_pow_seven.clone() / (c_bar_pow_seven + &twenty_five_pow_seven)).sqrt());
176    let a_one_prime = this.a * (T::one() + &g);
177    let a_two_prime = other.a * (T::one() + g);
178    let c_one_prime = (a_one_prime.clone() * &a_one_prime + this.b.clone() * &this.b).sqrt();
179    let c_two_prime = (a_two_prime.clone() * &a_two_prime + other.b.clone() * &other.b).sqrt();
180
181    let calc_h_prime = |b: T, a_prime: T| -> T {
182        lazy_select! {
183            if b.eq(&T::zero()) & a_prime.eq(&T::zero()) => T::zero(),
184            else => {
185                let result = T::radians_to_degrees(b.atan2(a_prime));
186                lazy_select! {
187                    if result.lt(&T::zero()) => result.clone() + T::from_f64(360.0),
188                    else => result.clone(),
189                }
190            },
191        }
192    };
193    let h_one_prime = calc_h_prime(this.b, a_one_prime);
194    let h_two_prime = calc_h_prime(other.b, a_two_prime);
195
196    let h_prime_diff = h_two_prime.clone() - &h_one_prime;
197    let h_prime_abs_diff = h_prime_diff.clone().abs();
198
199    let delta_h_prime: T = lazy_select! {
200        if c_one_prime.eq(&T::zero()) | c_two_prime.eq(&T::zero()) => T::zero(),
201        if h_prime_abs_diff.lt_eq(&T::from_f64(180.0)) => h_prime_diff.clone(),
202        if h_two_prime.lt_eq(&h_one_prime) => h_prime_diff.clone() + T::from_f64(360.0),
203        else => h_prime_diff.clone() - T::from_f64(360.0),
204    };
205
206    let delta_big_h_prime = T::from_f64(2.0)
207        * (c_one_prime.clone() * &c_two_prime).sqrt()
208        * (delta_h_prime / T::from_f64(2.0) * &pi_over_180).sin();
209    let h_prime_sum = h_one_prime + h_two_prime;
210    let h_bar_prime = lazy_select! {
211        if c_one_prime.eq(&T::zero()) | c_two_prime.eq(&T::zero()) => h_prime_sum.clone(),
212        if h_prime_abs_diff.gt(&T::from_f64(180.0)) => {
213            (h_prime_sum.clone() + T::from_f64(360.0)) / T::from_f64(2.0)
214        },
215        else => h_prime_sum.clone() / T::from_f64(2.0),
216    };
217
218    let l_bar = (this.l.clone() + &other.l) / T::from_f64(2.0);
219    let c_bar_prime = (c_one_prime.clone() + &c_two_prime) / T::from_f64(2.0);
220
221    let t: T = T::one()
222        - T::from_f64(0.17) * ((h_bar_prime.clone() - T::from_f64(30.0)) * &pi_over_180).cos()
223        + T::from_f64(0.24) * ((h_bar_prime.clone() * T::from_f64(2.0)) * &pi_over_180).cos()
224        + T::from_f64(0.32) * ((h_bar_prime.clone() * T::from_f64(3.0) + T::from_f64(6.0)) * &pi_over_180).cos()
225        - T::from_f64(0.20) * ((h_bar_prime.clone() * T::from_f64(4.0) - T::from_f64(63.0)) * &pi_over_180).cos();
226    let s_l = T::one()
227        + ((T::from_f64(0.015) * (l_bar.clone() - T::from_f64(50.0)) * (l_bar.clone() - T::from_f64(50.0)))
228            / ((l_bar.clone() - T::from_f64(50.0)) * (l_bar - T::from_f64(50.0)) + T::from_f64(20.0)).sqrt());
229    let s_c = T::one() + T::from_f64(0.045) * &c_bar_prime;
230    let s_h = T::one() + T::from_f64(0.015) * &c_bar_prime * t;
231
232    let delta_theta = T::from_f64(30.0)
233        * (-(((h_bar_prime.clone() - T::from_f64(275.0)) / T::from_f64(25.0))
234            * ((h_bar_prime - T::from_f64(275.0)) / T::from_f64(25.0))))
235        .exp();
236    let c_bar_prime_pow_seven = c_bar_prime.powi(7);
237    let r_c: T = T::from_f64(2.0)
238        * (c_bar_prime_pow_seven.clone() / (c_bar_prime_pow_seven + twenty_five_pow_seven)).sqrt();
239    let r_t = -r_c * (T::from_f64(2.0) * delta_theta * pi_over_180).sin();
240
241    let k_l = T::one();
242    let k_c = T::one();
243    let k_h = T::one();
244    let delta_l_prime = other.l - this.l;
245    let delta_c_prime = c_two_prime - c_one_prime;
246
247    ((delta_l_prime.clone() / (k_l.clone() * &s_l)) * (delta_l_prime / (k_l * s_l))
248        + (delta_c_prime.clone() / (k_c.clone() * &s_c)) * (delta_c_prime.clone() / (k_c.clone() * &s_c))
249        + (delta_big_h_prime.clone() / (k_h.clone() * &s_h)) * (delta_big_h_prime.clone() / (k_h.clone() * &s_h))
250        + (r_t * delta_c_prime * delta_big_h_prime) / (k_c * s_c * k_h * s_h))
251        .sqrt()
252}
253
254/// Calculate the distance between two colors as if they were coordinates in
255/// Euclidean space.
256///
257/// Euclidean distance is not always a good measurement of visual color
258/// difference, depending on the color space. Some spaces, like [`Lab`] and
259/// [`Oklab`][crate::Oklab], will give a fairly uniform result, while other
260/// spaces, such as [`Rgb`][crate::rgb::Rgb], will give much less uniform
261/// results. Despite that, it's still appropriate for some applications.
262pub trait EuclideanDistance: Sized {
263    /// The type for the distance value.
264    type Scalar;
265
266    /// Calculate the Euclidean distance from `self` to `other`.
267    #[must_use]
268    fn distance(self, other: Self) -> Self::Scalar
269    where
270        Self::Scalar: Sqrt,
271    {
272        self.distance_squared(other).sqrt()
273    }
274
275    /// Calculate the squared Euclidean distance from `self` to `other`.
276    ///
277    /// This is typically a faster option than [`Self::distance`] for some
278    /// cases, such as when comparing two distances.
279    #[must_use]
280    fn distance_squared(self, other: Self) -> Self::Scalar;
281}
282
283/// Calculate and check the WCAG 2.1 relative contrast and relative luminance.
284///
285/// W3C's Web Content Accessibility Guidelines (WCAG) 2.1 suggest a method to
286/// calculate accessible contrast ratios of text and background colors for those
287/// with low vision or color vision deficiencies, and for contrast of colors
288/// used in user interface graphics objects.
289///
290/// These criteria come with a couple of caveats:
291/// * sRGB is assumed as the presentation color space, which is why it's only
292///   implemented for a limited set of [`Rgb`][crate::rgb::Rgb] and
293///   [`Luma`][crate::Luma] spaces.
294/// * The contrast ratio is not considered entirely consistent with the
295///   perceived contrast. WCAG 3.x is supposed to provide a better measurement.
296///
297/// Because of the inconsistency with perceived contrast, these methods are more
298/// suitable as hints and for mechanical verification of standards compliance,
299/// than for accurate analysis. Remember to not only rely on the numbers, but to
300/// also test your interfaces with actual people in actual situations for the
301/// best results.
302///
303/// The following example checks the contrast ratio of two colors in sRGB
304/// format:
305///
306/// ```rust
307/// use std::str::FromStr;
308/// use palette::{Srgb, color_difference::Wcag21RelativeContrast};
309/// # fn main() -> Result<(), palette::rgb::FromHexError> {
310///
311/// // the rustdoc "DARK" theme background and text colors
312/// let background: Srgb<f32> = Srgb::from(0x353535).into_format();
313/// let foreground = Srgb::from_str("#ddd")?.into_format();
314///
315/// assert!(background.has_enhanced_contrast_text(foreground));
316/// # Ok(())
317/// # }
318/// ```
319pub trait Wcag21RelativeContrast: Sized {
320    /// The scalar type used for luminance and contrast.
321    type Scalar: Real
322        + Add<Self::Scalar, Output = Self::Scalar>
323        + Div<Self::Scalar, Output = Self::Scalar>
324        + PartialCmp
325        + MinMax;
326
327    /// Returns the WCAG 2.1 [relative
328    /// luminance](https://www.w3.org/TR/WCAG21/#dfn-relative-luminance) of
329    /// `self`.
330    ///
331    /// The relative luminance is a value between 0 and 1, where 0 is the
332    /// darkest black and 1 is the lightest white. This is the same as clamped
333    /// [`LinLuma`], meaning that the typical implementation of this method
334    /// would be `self.into_color()`.
335    #[must_use]
336    fn relative_luminance(self) -> LinLuma<D65, Self::Scalar>;
337
338    /// Returns the WCAG 2.1 relative luminance contrast between `self` and
339    /// `other`.
340    ///
341    /// A return value of, for example, 4 represents a contrast ratio of 4:1
342    /// between the lightest and darkest of the two colors. The range is from
343    /// 1:1 to 21:1, and a higher contrast ratio is generally desirable.
344    ///
345    /// This method is independent of the order of the colors, so
346    /// `a.relative_contrast(b)` and `b.relative_contrast(a)` would return the
347    /// same value.
348    #[must_use]
349    #[inline]
350    fn relative_contrast(self, other: Self) -> Self::Scalar {
351        let (min_luma, max_luma) = self
352            .relative_luminance()
353            .luma
354            .min_max(other.relative_luminance().luma);
355
356        (Self::Scalar::from_f64(0.05) + max_luma) / (Self::Scalar::from_f64(0.05) + min_luma)
357    }
358
359    /// Verify the contrast between two colors satisfies SC 1.4.3. Contrast is
360    /// at least 4.5:1 (Level AA).
361    ///
362    /// This applies for meaningful text, such as body text. Font sizes of 18
363    /// points or lager, or 14 points when bold, are considered large and can be
364    /// checked with
365    /// [`has_min_contrast_large_text`][Wcag21RelativeContrast::has_min_contrast_large_text]
366    /// instead.
367    ///
368    /// [Success Criterion 1.4.3 Contrast (Minimum) (Level
369    /// AA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum)
370    #[must_use]
371    #[inline]
372    fn has_min_contrast_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
373        self.relative_contrast(other)
374            .gt_eq(&Self::Scalar::from_f64(4.5))
375    }
376
377    /// Verify the contrast between two colors satisfies SC 1.4.3 for large
378    /// text. Contrast is at least 3:1 (Level AA).
379    ///
380    /// This applies for meaningful large text, such as headings. Font sizes of
381    /// 18 points or lager, or 14 points when bold, are considered large.
382    ///
383    /// [Success Criterion 1.4.3 Contrast (Minimum) (Level
384    /// AA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum)
385    #[must_use]
386    #[inline]
387    fn has_min_contrast_large_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
388        self.relative_contrast(other)
389            .gt_eq(&Self::Scalar::from_f64(3.0))
390    }
391
392    /// Verify the contrast between two colors satisfies SC 1.4.6. Contrast is
393    /// at least 7:1 (Level AAA).
394    ///
395    /// This applies for meaningful text, such as body text. Font sizes of 18
396    /// points or lager, or 14 points when bold, are considered large and can be
397    /// checked with
398    /// [`has_enhanced_contrast_large_text`][Wcag21RelativeContrast::has_enhanced_contrast_large_text]
399    /// instead.
400    ///
401    /// [Success Criterion 1.4.6 Contrast (Enhanced) (Level
402    /// AAA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced)
403    #[must_use]
404    #[inline]
405    fn has_enhanced_contrast_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
406        self.relative_contrast(other)
407            .gt_eq(&Self::Scalar::from_f64(7.0))
408    }
409
410    /// Verify the contrast between two colors satisfies SC 1.4.6 for large
411    /// text. Contrast is at least 4.5:1 (Level AAA).
412    ///
413    /// This applies for meaningful large text, such as headings. Font sizes of
414    /// 18 points or lager, or 14 points when bold, are considered large.
415    ///
416    /// [Success Criterion 1.4.6 Contrast (Enhanced) (Level
417    /// AAA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced)
418    #[must_use]
419    #[inline]
420    fn has_enhanced_contrast_large_text(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
421        self.relative_contrast(other)
422            .gt_eq(&Self::Scalar::from_f64(4.5))
423    }
424
425    /// Verify the contrast between two colors satisfies SC 1.4.11 for graphical
426    /// objects. Contrast is at least 3:1 (Level AA).
427    ///
428    /// This applies for any graphical object that aren't text, such as
429    /// meaningful images and interactive user interface elements.
430    ///
431    /// [Success Criterion 1.4.11 Non-text Contrast (Level
432    /// AA)](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html)
433    #[must_use]
434    #[inline]
435    fn has_min_contrast_graphics(self, other: Self) -> <Self::Scalar as HasBoolMask>::Mask {
436        self.relative_contrast(other)
437            .gt_eq(&Self::Scalar::from_f64(3.0))
438    }
439}
440
441/// Calculate a combination of Euclidean and Manhattan/City-block distance
442/// between two colors.
443///
444/// The HyAB distance was suggested as an alternative to CIEDE2000 for large
445/// color differences in [Distance metrics for very large color
446/// differences](http://markfairchild.org/PDFs/PAP40.pdf) (in [Color Res Appl.
447/// 2019;1–16](https://doi.org/10.1002/col.22451)) by Saeedeh Abasi, Mohammad
448/// Amani Tehran and Mark D. Fairchild. It's originally meant for [CIE L\*a\*b\*
449/// (CIELAB)][crate::Lab], but this trait is also implemented for other color
450/// spaces that have similar semantics, although **without the same quality
451/// guarantees**.
452///
453/// The hybrid distance is the sum of the absolute lightness difference and the
454/// distance on the chroma plane. This makes the lightness and chroma
455/// differences more independent from each other, which is meant to correspond
456/// more to how humans perceive the two qualities.
457pub trait HyAb {
458    /// The type for the distance value.
459    type Scalar;
460
461    /// Calculate the hybrid distance between `self` and `other`.
462    ///
463    /// This returns the sum of the absolute lightness difference and the
464    /// distance on the chroma plane.
465    #[must_use]
466    fn hybrid_distance(self, other: Self) -> Self::Scalar;
467}
468
469/// Calculate the Δ*E* color difference between two colors.
470///
471/// This represents the original Δ*E* formula for a color space. It's often a
472/// Euclidean distance for perceptually uniform color spaces and may not always
473/// be the best option. See the [`color_difference`](self) module for more
474/// details and options.
475pub trait DeltaE {
476    /// The type for the distance value.
477    type Scalar;
478
479    /// Calculate the Δ*E* color difference metric for `self` and `other`,
480    /// according to the color space's specification.
481    #[must_use]
482    fn delta_e(self, other: Self) -> Self::Scalar;
483}
484
485/// Calculate the Δ*E'* (improved Δ*E*) color difference between two colors.
486///
487/// The Δ*E'* uses the output of [`DeltaE`] and enhances it according to *Power
488/// functions improving the performance of color-difference formulas* by Huang
489/// et al. Only spaces with specified coefficients implement this trait.
490pub trait ImprovedDeltaE: DeltaE {
491    /// Calculate the Δ*E'* (improved Δ*E*) color difference metric for `self`
492    /// and `other`, according to the color space's specification and later
493    /// improvements by Huang et al.
494    #[must_use]
495    fn improved_delta_e(self, other: Self) -> Self::Scalar;
496}
497
498#[cfg(feature = "approx")]
499#[cfg(test)]
500mod test {
501    use core::str::FromStr;
502
503    use super::{HyAb, Wcag21RelativeContrast};
504    use crate::{FromColor, Lab, Srgb};
505
506    #[test]
507    fn relative_contrast() {
508        let white = Srgb::new(1.0f32, 1.0, 1.0);
509        let black = Srgb::new(0.0, 0.0, 0.0);
510
511        assert_relative_eq!(white.relative_contrast(white), 1.0);
512        assert_relative_eq!(white.relative_contrast(black), 21.0);
513        assert_relative_eq!(
514            white.relative_contrast(black),
515            black.relative_contrast(white)
516        );
517
518        let c1 = Srgb::from_str("#600").unwrap().into_format();
519
520        assert_relative_eq!(c1.relative_contrast(white), 13.41, epsilon = 0.01);
521        assert_relative_eq!(c1.relative_contrast(black), 1.56, epsilon = 0.01);
522
523        assert!(c1.has_min_contrast_text(white));
524        assert!(c1.has_min_contrast_large_text(white));
525        assert!(c1.has_enhanced_contrast_text(white));
526        assert!(c1.has_enhanced_contrast_large_text(white));
527        assert!(c1.has_min_contrast_graphics(white));
528
529        assert!(!c1.has_min_contrast_text(black));
530        assert!(!c1.has_min_contrast_large_text(black));
531        assert!(!c1.has_enhanced_contrast_text(black));
532        assert!(!c1.has_enhanced_contrast_large_text(black));
533        assert!(!c1.has_min_contrast_graphics(black));
534
535        let c1 = Srgb::from_str("#066").unwrap().into_format();
536
537        assert_relative_eq!(c1.relative_contrast(white), 6.79, epsilon = 0.01);
538        assert_relative_eq!(c1.relative_contrast(black), 3.09, epsilon = 0.01);
539
540        let c1 = Srgb::from_str("#9f9").unwrap().into_format();
541
542        assert_relative_eq!(c1.relative_contrast(white), 1.22, epsilon = 0.01);
543        assert_relative_eq!(c1.relative_contrast(black), 17.11, epsilon = 0.01);
544    }
545
546    #[test]
547    fn hyab() {
548        // From https://github.com/Evercoder/culori/blob/cd1fe08a12fa9ddfcf6b2e82914733d23ac117d0/test/difference.test.js#L186
549        let red = Lab::<_, f64>::from_color(Srgb::from(0xff0000).into_linear());
550        let green = Lab::<_, f64>::from_color(Srgb::from(0x008000).into_linear());
551        assert_relative_eq!(
552            red.hybrid_distance(green),
553            139.93576718451553,
554            epsilon = 0.000001
555        );
556    }
557}