From 3406b69614e27dcb6036a6d2e5ca62833070c0a1 Mon Sep 17 00:00:00 2001 From: Lucas CHOLLET Date: Sat, 26 Oct 2024 17:16:13 -0400 Subject: [PATCH] LibGfx+LibWeb/CSS: Add support for the `lab()` color function The support in LibWeb is quite easy as the previous commits introduced helpers to support lab-like colors. Now for the methods in Color: - The formulas in `from_lab()` are derived from the CIEXYZ to CIELAB formulas the "Colorimetry" paper published by the CIE. - The conversion in `from_xyz50()` can be decomposed in multiple steps XYZ D50 -> XYZ D65 -> Linear sRGB -> sRGB. The two first conversion are done with a singular matrix operation. This matrix was generated with a Python script [1]. This commit makes us pass all the `css/css-color/lab-00*.html` WPT tests (0 to 7 at the time of writing). [1] Python script used to generate the XYZ D50 -> Linear sRGB conversion: ```python import numpy as np # http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html # First let's convert from D50 to D65 using the Bradford method. m_a = np.array([ [0.8951000, 0.2664000, -0.1614000], [-0.7502000, 1.7135000, 0.0367000], [0.0389000, -0.0685000, 1.0296000] ]) # D50 chromaticities_source = np.array([0.96422, 1, 0.82521]) # D65 chromaticities_destination = np.array([0.9505, 1, 1.0890]) cone_response_source = m_a @ chromaticities_source cone_response_destination = m_a @ chromaticities_destination cone_response_ratio = cone_response_destination / cone_response_source m = np.linalg.inv(m_a) @ np.diagflat(cone_response_ratio) @ m_a D50_to_D65 = m # https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB # Then, the matrix to convert to linear sRGB. xyz_65_to_srgb = np.array([ [3.2406, - 1.5372, - 0.4986], [-0.9689, + 1.8758, 0.0415], [0.0557, - 0.2040, 1.0570] ]) # Finally, let's retrieve the final transformation. xyz_50_to_srgb = xyz_65_to_srgb @ D50_to_D65 print(xyz_50_to_srgb) ``` --- Userland/Libraries/LibGfx/Color.cpp | 50 +++++++++++++++++++ Userland/Libraries/LibGfx/Color.h | 3 ++ .../Libraries/LibWeb/CSS/Parser/Parser.cpp | 22 ++++++++ Userland/Libraries/LibWeb/CSS/Parser/Parser.h | 1 + .../LibWeb/CSS/StyleValues/CSSColorValue.h | 1 + .../LibWeb/CSS/StyleValues/CSSLabLike.cpp | 17 +++++++ .../LibWeb/CSS/StyleValues/CSSLabLike.h | 22 ++++++++ 7 files changed, 116 insertions(+) diff --git a/Userland/Libraries/LibGfx/Color.cpp b/Userland/Libraries/LibGfx/Color.cpp index b63904c0d92..b2653162d35 100644 --- a/Userland/Libraries/LibGfx/Color.cpp +++ b/Userland/Libraries/LibGfx/Color.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2018-2020, Andreas Kling * Copyright (c) 2019-2023, Shannon Booth + * Copyright (c) 2024, Lucas Chollet * * SPDX-License-Identifier: BSD-2-Clause */ @@ -395,6 +396,55 @@ Vector Color::tints(u32 steps, float max) const return tints; } +Color Color::from_xyz50(float x, float y, float z, float alpha) +{ + // See commit description for these values + float red = 3.13397926 * x - 1.61689519 * y - 0.49070587 * z; + float green = -0.97840009 * x + 1.91589112 * y + 0.03339256 * z; + float blue = 0.07200357 * x - 0.22897505 * y + 1.40517398 * z; + + auto linear_to_srgb = [](float c) { + return c >= 0.0031308f ? 1.055f * pow(c, 0.4166666f) - 0.055f : 12.92f * c; + }; + + red = linear_to_srgb(red) * 255.f; + green = linear_to_srgb(green) * 255.f; + blue = linear_to_srgb(blue) * 255.f; + + return Color( + clamp(lroundf(red), 0, 255), + clamp(lroundf(green), 0, 255), + clamp(lroundf(blue), 0, 255), + clamp(lroundf(alpha * 255.f), 0, 255)); +} + +Color Color::from_lab(float L, float a, float b, float alpha) +{ + // Third edition of "Colorimetry" by the CIE + // 8.2.1 CIE 1976 (L*a*b*) colour space; CIELAB colour space + float y = (L + 16) / 116; + float x = y + a / 500; + float z = y - b / 200; + + auto f_inv = [](float t) -> float { + constexpr auto delta = 24. / 116; + if (t > delta) + return t * t * t; + return (108. / 841) * (t - 116. / 16); + }; + + // D50 + constexpr float x_n = 0.96422; + constexpr float y_n = 1; + constexpr float z_n = 0.82521; + + x = x_n * f_inv(x); + y = y_n * f_inv(y); + z = z_n * f_inv(z); + + return from_xyz50(x, y, z, alpha); +} + } template<> diff --git a/Userland/Libraries/LibGfx/Color.h b/Userland/Libraries/LibGfx/Color.h index f00a48b71b9..f02aa8c2d4e 100644 --- a/Userland/Libraries/LibGfx/Color.h +++ b/Userland/Libraries/LibGfx/Color.h @@ -182,6 +182,9 @@ public: return Color(r_u8, g_u8, b_u8, a_u8); } + static Color from_lab(float L, float a, float b, float alpha = 1.0f); + static Color from_xyz50(float x, float y, float z, float alpha = 1.0f); + // https://bottosson.github.io/posts/oklab/ static constexpr Color from_oklab(float L, float a, float b, float alpha = 1.0f) { diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp index 91909abc57d..c8d104df621 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp @@ -3296,6 +3296,26 @@ Optional, 4>> Parser::parse_lab_like_color_value(Tok return Array { move(l), move(a), move(b), move(alpha) }; } +// https://www.w3.org/TR/css-color-4/#funcdef-lab +RefPtr Parser::parse_lab_color_value(TokenStream& outer_tokens) +{ + // lab() = lab( [ | | none] + // [ | | none] + // [ | | none] + // [ / [ | none] ]? ) + + auto maybe_color_values = parse_lab_like_color_value(outer_tokens, "lab"sv); + if (!maybe_color_values.has_value()) + return {}; + + auto& color_values = *maybe_color_values; + + return CSSLab::create(color_values[0].release_nonnull(), + color_values[1].release_nonnull(), + color_values[2].release_nonnull(), + color_values[3].release_nonnull()); +} + // https://www.w3.org/TR/css-color-4/#funcdef-oklab RefPtr Parser::parse_oklab_color_value(TokenStream& outer_tokens) { @@ -3387,6 +3407,8 @@ RefPtr Parser::parse_color_value(TokenStream& tok return hsl; if (auto hwb = parse_hwb_color_value(tokens)) return hwb; + if (auto lab = parse_lab_color_value(tokens)) + return lab; if (auto oklab = parse_oklab_color_value(tokens)) return oklab; if (auto oklch = parse_oklch_color_value(tokens)) diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h index ad4b8cabac6..2cb4dd4b4d7 100644 --- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.h +++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.h @@ -251,6 +251,7 @@ private: RefPtr parse_hsl_color_value(TokenStream&); RefPtr parse_hwb_color_value(TokenStream&); Optional, 4>> parse_lab_like_color_value(TokenStream&, StringView); + RefPtr parse_lab_color_value(TokenStream&); RefPtr parse_oklab_color_value(TokenStream&); RefPtr parse_oklch_color_value(TokenStream&); RefPtr parse_color_value(TokenStream&); diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h index 677bcd71df7..32ab45a1881 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSColorValue.h @@ -26,6 +26,7 @@ public: RGB, HSL, HWB, + Lab, OKLab, OKLCH, }; diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSLabLike.cpp b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSLabLike.cpp index ccdcbf2e767..f5210e0eece 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSLabLike.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSLabLike.cpp @@ -41,4 +41,21 @@ String CSSOKLab::to_string() const return serialize_a_srgb_value(to_color({})); } +Color CSSLab::to_color(Optional) const +{ + auto const l_val = clamp(resolve_with_reference_value(m_properties.l, 100).value_or(0), 0, 100); + auto const a_val = resolve_with_reference_value(m_properties.a, 125).value_or(0); + auto const b_val = resolve_with_reference_value(m_properties.b, 125).value_or(0); + auto const alpha_val = resolve_alpha(m_properties.alpha).value_or(1); + + return Color::from_lab(l_val, a_val, b_val, alpha_val); +} + +// https://www.w3.org/TR/css-color-4/#serializing-lab-lch +String CSSLab::to_string() const +{ + // FIXME: Do this properly, taking unresolved calculated values into account. + return serialize_a_srgb_value(to_color({})); +} + } diff --git a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSLabLike.h b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSLabLike.h index 783a03cfa9d..e82070e483f 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleValues/CSSLabLike.h +++ b/Userland/Libraries/LibWeb/CSS/StyleValues/CSSLabLike.h @@ -60,4 +60,26 @@ private: } }; +// https://drafts.css-houdini.org/css-typed-om-1/#csslab +class CSSLab final : public CSSLabLike { +public: + static ValueComparingNonnullRefPtr create(ValueComparingNonnullRefPtr l, ValueComparingNonnullRefPtr a, ValueComparingNonnullRefPtr b, ValueComparingRefPtr alpha = {}) + { + // alpha defaults to 1 + if (!alpha) + return adopt_ref(*new (nothrow) CSSLab(move(l), move(a), move(b), NumberStyleValue::create(1))); + + return adopt_ref(*new (nothrow) CSSLab(move(l), move(a), move(b), alpha.release_nonnull())); + } + + virtual Color to_color(Optional) const override; + virtual String to_string() const override; + +private: + CSSLab(ValueComparingNonnullRefPtr l, ValueComparingNonnullRefPtr a, ValueComparingNonnullRefPtr b, ValueComparingNonnullRefPtr alpha) + : CSSLabLike(ColorType::Lab, move(l), move(a), move(b), move(alpha)) + { + } +}; + }