LibGfx: Implement WCAG's contrast ratio in Color

We're going to implement the `contrast-color()` CSS function in the next
commit, and the spec advises to use the contrast ratio definition as
described in WCAG 2. So let's replace our `Color::contrast_ratio()`
implementation by that.

Co-authored-by: InvalidUsernameException
 <InvalidUsernameException@users.noreply.github.com>
This commit is contained in:
Jelle Raaijmakers 2026-06-09 14:23:07 +02:00 committed by Jelle Raaijmakers
parent d42b0f72ae
commit 49cdf582ff
6 changed files with 77 additions and 18 deletions

View file

@ -67,6 +67,52 @@ Array<char, 4> format_to_8bit_compatible(u8 value)
}
// https://w3c.github.io/wcag/guidelines/22/#dfn-relative-luminance
double Color::relative_luminance() const
{
// the relative brightness of any point in a colorspace, normalized to 0 for darkest black and 1 for lightest
// white.
// Note 1:
// For the sRGB colorspace, the relative luminance of a color is defined as L = 0.2126 * R + 0.7152 * G +
// 0.0722 * B where R, G and B are defined as:
// * if RsRGB <= 0.04045 then R = RsRGB/12.92 else R = ((RsRGB+0.055)/1.055) ^ 2.4
// * if GsRGB <= 0.04045 then G = GsRGB/12.92 else G = ((GsRGB+0.055)/1.055) ^ 2.4
// * if BsRGB <= 0.04045 then B = BsRGB/12.92 else B = ((BsRGB+0.055)/1.055) ^ 2.4
// and RsRGB, GsRGB, and BsRGB are defined as:
// * RsRGB = R8bit/255
// * GsRGB = G8bit/255
// * BsRGB = B8bit/255
// The "^" character is the exponentiation operator. (Formula taken from [SRGB].)
auto linearized_srgb_component = [](u8 component) {
auto srgb_component = component / 255.0;
if (srgb_component <= 0.04045)
return srgb_component / 12.92;
return AK::pow((srgb_component + 0.055) / 1.055, 2.4);
};
return 0.2126 * linearized_srgb_component(red())
+ 0.7152 * linearized_srgb_component(green())
+ 0.0722 * linearized_srgb_component(blue());
}
// https://w3c.github.io/wcag/guidelines/22/#dfn-contrast-ratio
double Color::contrast_ratio(Color const other) const
{
// (L1 + 0.05) / (L2 + 0.05), where
// * L1 is the relative luminance of the lighter of the colors, and
// * L2 is the relative luminance of the darker of the colors.
auto l1 = relative_luminance();
auto l2 = other.relative_luminance();
auto darkest = min(l1, l2);
auto brightest = max(l1, l2);
return (brightest + 0.05) / (darkest + 0.05);
}
Color Color::suggested_foreground_color() const
{
return contrast_ratio(Black) >= contrast_ratio(White) ? Black : White;
}
// https://www.w3.org/TR/css-color-4/#serializing-sRGB-values
void Color::serialize_a_srgb_value(StringBuilder& builder) const
{

View file

@ -324,19 +324,8 @@ public:
return delta_alpha * delta_alpha / (2.0f * 255 * 255) + rgb_distance * alpha() * other.alpha() / (255 * 255);
}
constexpr u8 luminosity() const
{
return round_to<u8>(red() * 0.2126f + green() * 0.7152f + blue() * 0.0722f);
}
constexpr float contrast_ratio(Color other)
{
auto l1 = luminosity();
auto l2 = other.luminosity();
auto darkest = min(l1, l2) / 255.;
auto brightest = max(l1, l2) / 255.;
return (brightest + 0.05) / (darkest + 0.05);
}
double relative_luminance() const;
double contrast_ratio(Color other) const;
constexpr Color sepia(float amount = 1.0f) const
{
@ -457,10 +446,7 @@ public:
return Color(out_r, out_g, out_b);
}
constexpr Color suggested_foreground_color() const
{
return luminosity() < 128 ? Color::White : Color::Black;
}
Color suggested_foreground_color() const;
private:
constexpr explicit Color(BGRA8888 argb)

View file

@ -7,6 +7,34 @@
#include <LibGfx/Color.h>
#include <LibTest/TestCase.h>
TEST_CASE(relative_luminance)
{
auto const orange = Color(247, 143, 0);
EXPECT_APPROXIMATE(Color(Color::NamedColor::Black).relative_luminance(), 0.0);
EXPECT_APPROXIMATE(Color(Color::NamedColor::White).relative_luminance(), 1.0);
EXPECT_APPROXIMATE_WITH_ERROR(orange.relative_luminance(), 0.394190782, 0.0000005);
}
TEST_CASE(contrast_ratio)
{
auto const orange = Color(247, 143, 0);
EXPECT_APPROXIMATE(Color(Color::NamedColor::Black).contrast_ratio(Color::NamedColor::White), 21.0);
EXPECT_APPROXIMATE_WITH_ERROR(
orange.contrast_ratio(Color::NamedColor::Black), 8.883815642, 0.0000005);
EXPECT_APPROXIMATE_WITH_ERROR(
orange.contrast_ratio(Color::NamedColor::White), 2.363849144, 0.0000005);
EXPECT(orange.contrast_ratio(Color::NamedColor::Black) > orange.contrast_ratio(Color::NamedColor::White));
}
TEST_CASE(suggested_foreground_color)
{
EXPECT_EQ(Color(Color::NamedColor::White), Color(Color::NamedColor::Black).suggested_foreground_color());
EXPECT_EQ(Color(Color::NamedColor::Black), Color(Color::NamedColor::White).suggested_foreground_color());
EXPECT_EQ(Color(Color::NamedColor::Black), Color(247, 143, 0).suggested_foreground_color());
}
TEST_CASE(from_bgrx)
{
EXPECT_EQ(Color(0x00, 0x00, 0xff), Color::from_bgrx(0x000000ff));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -1,5 +1,4 @@
<!DOCTYPE html>
<meta name="fuzzy" content="maxDifference=0-3;totalPixels=0-4660">
<style>
body>div>div {
padding: 1em;