Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement WCAG contrast ratio calculations #164

Merged
merged 1 commit into from Jan 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 19 additions & 2 deletions palette/src/hsl.rs
Expand Up @@ -8,9 +8,11 @@ use core::ops::{Add, AddAssign, Sub, SubAssign};
use encoding::pixel::RawPixel;
use encoding::{Linear, Srgb};
use rgb::{Rgb, RgbSpace};
use {clamp, contrast_ratio, from_f64};
use {Alpha, Hsv, RgbHue, Xyz};
use {
clamp, from_f64, Alpha, Component, FloatComponent, FromColor, FromF64, GetHue, Hsv, Hue,
IntoColor, Limited, Mix, Pixel, RgbHue, Saturate, Shade, Xyz,
Component, FloatComponent, FromColor, FromF64, GetHue, Hue, IntoColor, Limited, Mix, Pixel,
RelativeContrast, Saturate, Shade,
};

/// Linear HSL with an alpha component. See the [`Hsla` implementation in
Expand Down Expand Up @@ -622,6 +624,21 @@ where
}
}

impl<S, T> RelativeContrast for Hsl<S, T>
where
T: FloatComponent,
S: RgbSpace,
{
type Scalar = T;

fn get_contrast_ratio(&self, other: &Self) -> T {
let luma1 = self.into_luma();
let luma2 = other.into_luma();

contrast_ratio(luma1.luma, luma2.luma)
}
}

#[cfg(test)]
mod test {
use super::Hsl;
Expand Down
23 changes: 19 additions & 4 deletions palette/src/hsv.rs
Expand Up @@ -8,11 +8,11 @@ use core::ops::{Add, AddAssign, Sub, SubAssign};
use encoding::pixel::RawPixel;
use encoding::{Linear, Srgb};
use rgb::{Rgb, RgbSpace};
use {clamp, from_f64};
use {Alpha, Hsl, Hwb, Xyz};
use {clamp, contrast_ratio, from_f64};
use {Alpha, Hsl, Hwb, RgbHue, Xyz};
use {
Component, FloatComponent, FromColor, FromF64, GetHue, Hue, Limited, Mix, Pixel, RgbHue,
Saturate, Shade,
Component, FloatComponent, FromColor, FromF64, GetHue, Hue, IntoColor, Limited, Mix, Pixel,
RelativeContrast, Saturate, Shade,
};

/// Linear HSV with an alpha component. See the [`Hsva` implementation in
Expand Down Expand Up @@ -637,6 +637,21 @@ where
}
}

impl<S, T> RelativeContrast for Hsv<S, T>
where
T: FloatComponent,
S: RgbSpace,
{
type Scalar = T;

fn get_contrast_ratio(&self, other: &Self) -> T {
let luma1 = self.into_luma();
let luma2 = other.into_luma();

contrast_ratio(luma1.luma, luma2.luma)
}
}

#[cfg(test)]
mod test {
use super::Hsv;
Expand Down
21 changes: 19 additions & 2 deletions palette/src/hwb.rs
Expand Up @@ -8,9 +8,11 @@ use core::ops::{Add, AddAssign, Sub, SubAssign};
use encoding::pixel::RawPixel;
use encoding::Srgb;
use rgb::RgbSpace;
use {clamp, contrast_ratio};
use {Alpha, Hsv, RgbHue, Xyz};
use {
clamp, Alpha, Component, FloatComponent, FromColor, FromF64, GetHue, Hsv, Hue, IntoColor,
Limited, Mix, Pixel, RgbHue, Shade, Xyz,
Component, FloatComponent, FromColor, FromF64, GetHue, Hue, IntoColor, Limited, Mix, Pixel,
RelativeContrast, Shade,
};

/// Linear HWB with an alpha component. See the [`Hwba` implementation in
Expand Down Expand Up @@ -583,6 +585,21 @@ where
}
}

impl<S, T> RelativeContrast for Hwb<S, T>
where
T: FloatComponent,
S: RgbSpace,
{
type Scalar = T;

fn get_contrast_ratio(&self, other: &Self) -> T {
let luma1 = self.into_luma();
let luma2 = other.into_luma();

contrast_ratio(luma1.luma, luma2.luma)
}
}

#[cfg(test)]
mod test {
use super::Hwb;
Expand Down
22 changes: 20 additions & 2 deletions palette/src/lab.rs
Expand Up @@ -3,9 +3,12 @@ use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};

use encoding::pixel::RawPixel;
use white_point::{WhitePoint, D65};
use {clamp, from_f64};
use {clamp, contrast_ratio, from_f64};
use {Alpha, LabHue, Lch, Xyz};
use {Component, ComponentWise, FloatComponent, GetHue, Limited, Mix, Pixel, Shade};
use {
Component, ComponentWise, FloatComponent, GetHue, IntoColor, Limited, Mix, Pixel,
RelativeContrast, Shade,
};

use color_difference::ColorDifference;
use color_difference::{get_ciede_difference, LabColorDiff};
Expand Down Expand Up @@ -624,6 +627,21 @@ where
}
}

impl<Wp, T> RelativeContrast for Lab<Wp, T>
where
Wp: WhitePoint,
T: FloatComponent,
{
type Scalar = T;

fn get_contrast_ratio(&self, other: &Self) -> T {
let luma1 = self.into_luma();
let luma2 = other.into_luma();

contrast_ratio(luma1.luma, luma2.luma)
}
}

#[cfg(test)]
mod test {
use super::Lab;
Expand Down
22 changes: 19 additions & 3 deletions palette/src/lch.rs
Expand Up @@ -5,10 +5,11 @@ use color_difference::ColorDifference;
use color_difference::{get_ciede_difference, LabColorDiff};
use encoding::pixel::RawPixel;
use white_point::{WhitePoint, D65};
use {clamp, from_f64};
use {Alpha, Hue, Lab, LabHue, Xyz};
use {clamp, contrast_ratio, from_f64};
use {Alpha, Lab, LabHue, Xyz};
use {
Component, FloatComponent, FromColor, GetHue, IntoColor, Limited, Mix, Pixel, Saturate, Shade,
Component, FloatComponent, FromColor, GetHue, Hue, IntoColor, Limited, Mix, Pixel,
RelativeContrast, Saturate, Shade,
};

/// CIE L\*C\*h° with an alpha component. See the [`Lcha` implementation in
Expand Down Expand Up @@ -517,6 +518,21 @@ where
}
}

impl<Wp, T> RelativeContrast for Lch<Wp, T>
where
Wp: WhitePoint,
T: FloatComponent,
{
type Scalar = T;

fn get_contrast_ratio(&self, other: &Self) -> T {
let luma1 = self.into_luma();
let luma2 = other.into_luma();

contrast_ratio(luma1.luma, luma2.luma)
}
}

#[cfg(test)]
mod test {
use white_point::D65;
Expand Down
2 changes: 2 additions & 0 deletions palette/src/lib.rs
Expand Up @@ -189,6 +189,7 @@ pub use convert::{ConvertFrom, ConvertInto, FromColor, IntoColor, OutOfBounds};
pub use encoding::pixel::Pixel;
pub use hues::{LabHue, RgbHue};
pub use matrix::Mat3;
pub use relative_contrast::{contrast_ratio, RelativeContrast};

//Helper macro for checking ranges and clamping.
#[cfg(test)]
Expand Down Expand Up @@ -371,6 +372,7 @@ mod convert;
pub mod encoding;
mod equality;
mod matrix;
mod relative_contrast;
pub mod white_point;

pub mod float;
Expand Down
19 changes: 17 additions & 2 deletions palette/src/luma/luma.rs
Expand Up @@ -5,16 +5,16 @@ use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
use approx::{AbsDiffEq, RelativeEq, UlpsEq};

use blend::PreAlpha;
use clamp;
use encoding::linear::LinearFn;
use encoding::pixel::RawPixel;
use encoding::{Linear, Srgb, TransferFn};
use luma::LumaStandard;
use white_point::WhitePoint;
use {clamp, contrast_ratio};
use {Alpha, Xyz, Yxy};
use {
Blend, Component, ComponentWise, FloatComponent, FromColor, FromComponent, IntoColor, Limited,
Mix, Pixel, Shade,
Mix, Pixel, RelativeContrast, Shade,
};

/// Luminance with an alpha component. See the [`Lumaa` implementation
Expand Down Expand Up @@ -725,6 +725,21 @@ where
}
}

impl<S, T> RelativeContrast for Luma<S, T>
where
T: FloatComponent,
S: LumaStandard,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An alternative could be to force this to be linear. But I'm not sure that's the way to go. Maybe it's better to go the opposite way this time and implement this trait for anything that can be converted to linear Luma. I think a blanket implementation would be possible.

{
type Scalar = T;

fn get_contrast_ratio(&self, other: &Self) -> T {
let luma1 = self.into_luma();
let luma2 = other.into_luma();

contrast_ratio(luma1.luma, luma2.luma)
}
}

#[cfg(test)]
mod test {
use encoding::Srgb;
Expand Down
147 changes: 147 additions & 0 deletions palette/src/relative_contrast.rs
@@ -0,0 +1,147 @@
use component::Component;
use core::ops::{Add, Div};
use {from_f64, FromF64};

/// A trait for calculating relative contrast between two colors.
///
/// W3C's Web Content Accessibility Guidelines (WCAG) 2.1 suggest a method
/// to calculate accessible contrast ratios of text and background colors for
/// those with low vision or color deficiencies, and for contrast of colors used
/// in user interface graphics objects.
///
/// These criteria are recommendations, not hard and fast rules. Most
/// importantly, look at the colors in action and make sure they're clear and
/// comfortable to read. A pair of colors may pass contrast guidelines but still
/// be uncomfortable to look at. Favor readability over only satisfying the
/// contrast ratio metric. It is recommended to verify the contrast ratio
/// in the output format of the colors and not to assume the contrast ratio
/// remains exactly the same across color formats. The following example checks
/// the contrast ratio of two colors in RGB format.
///
/// ```rust
/// #[macro_use]
/// extern crate approx;
///
/// use std::str::FromStr;
/// use palette::{Srgb, RelativeContrast};
///
/// fn main() {
/// // the rustdoc "DARK" theme background and text colors
/// let my_background_rgb: Srgb<f32> = Srgb::from_str("#353535").unwrap().into_format();
/// let my_foreground_rgb = Srgb::from_str("#ddd").unwrap().into_format();
///
/// assert!(my_background_rgb.has_enhanced_contrast_text(&my_foreground_rgb));
/// }
/// ```
///
/// The possible range of contrast ratios is from 1:1 to 21:1. There is a
/// Success Criterion for Contrast (Minimum) and a Success Criterion for
/// Contrast (Enhanced), SC 1.4.3 and SC 1.4.6 respectively, which are concerned
/// with text and images of text. SC 1.4.11 is a Success Criterion for "non-text
/// contrast" such as user interface components and other graphics. The relative
/// contrast is calculated by `(L1 + 0.05) / (L2 + 0.05)`, where `L1` is the
/// luminance of the brighter color and `L2` is the luminance of the darker
/// color both in sRGB linear space. A higher contrast ratio is generally
/// desireable.
///
/// For more details, visit the following links:
///
/// [Success Criterion 1.4.3 Contrast (Minimum) (Level AA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum)
///
/// [Success Criterion 1.4.6 Contrast (Enhanced) (Level AAA)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-enhanced)
///
/// [Success Criterion 1.4.11 Non-text Contrast (Level AA)](https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html)

pub trait RelativeContrast {
/// The type of the contrast ratio.
type Scalar: FromF64 + PartialOrd;

/// Calculate contrast ratio between two colors.
fn get_contrast_ratio(&self, other: &Self) -> Self::Scalar;
/// Verify the contrast between two colors satisfies SC 1.4.3. Contrast
/// is at least 4.5:1 (Level AA).
fn has_min_contrast_text(&self, other: &Self) -> bool {
self.get_contrast_ratio(other) >= from_f64(4.5)
}
/// Verify the contrast between two colors satisfies SC 1.4.3 for large
/// text. Contrast is at least 3:1 (Level AA).
fn has_min_contrast_large_text(&self, other: &Self) -> bool {
self.get_contrast_ratio(other) >= from_f64(3.0)
}
/// Verify the contrast between two colors satisfies SC 1.4.6. Contrast
/// is at least 7:1 (Level AAA).
fn has_enhanced_contrast_text(&self, other: &Self) -> bool {
self.get_contrast_ratio(other) >= from_f64(7.0)
}
/// Verify the contrast between two colors satisfies SC 1.4.6 for large
/// text. Contrast is at least 4.5:1 (Level AAA).
fn has_enhanced_contrast_large_text(&self, other: &Self) -> bool {
self.has_min_contrast_text(other)
}
/// Verify the contrast between two colors satisfies SC 1.4.11 for graphical
/// objects. Contrast is at least 3:1 (Level AA).
fn has_min_contrast_graphics(&self, other: &Self) -> bool {
self.has_min_contrast_large_text(other)
}
}

/// Calculate a ratio between two `luma` values.
pub fn contrast_ratio<T>(luma1: T, luma2: T) -> T
where
T: Add<Output = T>,
T: Div<Output = T>,
T: FromF64,
T: Component,
{
if luma1 > luma2 {
(luma1 + from_f64(0.05)) / (luma2 + from_f64(0.05))
} else {
(luma2 + from_f64(0.05)) / (luma1 + from_f64(0.05))
}
}

#[cfg(test)]
mod test {
use core::str::FromStr;
use RelativeContrast;
use Srgb;

#[test]
fn relative_contrast() {
let white = Srgb::new(1.0, 1.0, 1.0);
let black = Srgb::new(0.0, 0.0, 0.0);

assert_relative_eq!(white.get_contrast_ratio(&white), 1.0);
assert_relative_eq!(white.get_contrast_ratio(&black), 21.0);
assert_relative_eq!(
white.get_contrast_ratio(&black),
black.get_contrast_ratio(&white)
);

let c1 = Srgb::from_str("#600").unwrap().into_format();

assert_relative_eq!(c1.get_contrast_ratio(&white), 13.41, epsilon = 0.01);
assert_relative_eq!(c1.get_contrast_ratio(&black), 1.56, epsilon = 0.01);

assert!(c1.has_min_contrast_text(&white));
assert!(c1.has_min_contrast_large_text(&white));
assert!(c1.has_enhanced_contrast_text(&white));
assert!(c1.has_enhanced_contrast_large_text(&white));
assert!(c1.has_min_contrast_graphics(&white));
assert!(c1.has_min_contrast_text(&black) == false);
assert!(c1.has_min_contrast_large_text(&black) == false);
assert!(c1.has_enhanced_contrast_text(&black) == false);
assert!(c1.has_enhanced_contrast_large_text(&black) == false);
assert!(c1.has_min_contrast_graphics(&black) == false);

let c1 = Srgb::from_str("#066").unwrap().into_format();

assert_relative_eq!(c1.get_contrast_ratio(&white), 6.79, epsilon = 0.01);
assert_relative_eq!(c1.get_contrast_ratio(&black), 3.09, epsilon = 0.01);

let c1 = Srgb::from_str("#9f9").unwrap().into_format();

assert_relative_eq!(c1.get_contrast_ratio(&white), 1.22, epsilon = 0.01);
assert_relative_eq!(c1.get_contrast_ratio(&black), 17.11, epsilon = 0.01);
}
}