Title: [289396] trunk
Revision
289396
Author
wei...@apple.com
Date
2022-02-08 10:57:30 -0800 (Tue, 08 Feb 2022)

Log Message

Conversion to a color space with a smaller gamut should perform gamut mapping
https://bugs.webkit.org/show_bug.cgi?id=236200

Reviewed by Darin Adler.

LayoutTests/imported/w3c:

Update tests to include more examples that force gamut mapping and update the results
of a few existing ones to account for the new algorithm.

* web-platform-tests/css/css-color/parsing/color-mix-computed-expected.txt:
* web-platform-tests/css/css-color/parsing/color-mix-computed.html:
* web-platform-tests/css/css-color/parsing/color-mix-valid-expected.txt:
* web-platform-tests/css/css-color/parsing/color-mix-valid.html:
* web-platform-tests/css/css-color/parsing/relative-color-computed-expected.txt:
* web-platform-tests/css/css-color/parsing/relative-color-computed.html:
* web-platform-tests/css/css-color/parsing/relative-color-valid-expected.txt:
* web-platform-tests/css/css-color/parsing/relative-color-valid.html:

Source/WebCore:

CSS Color now defines that gamut mapping should happen when converting to an RGB color space
with bounded gamut that is smaller than the origin color's color space. Specifically, it
specifies the use of a new "CSS gamut mapping algorithm" https://drafts.csswg.org/css-color/#css-gamut-mapping
which implements a relative colorimetric intent mapping with colors inside the destination
gamut unchanged.

The previous behavior we implemented was to clip out of gamut colors to the gamut (e.g.
color(srgb 1.4 -0.2 .5) would become color(srgb 1 0 .5)) which can lead to very odd results.

To keep things simple, the gamut mapping has been incorporated directly into the main color
conversion pipeline, replacing the call to makeFromComponentsClampingExceptAlpha in toBounded()
with one implements the new algorithm. To accomadate this, the pipeline was modified so that
the matrix conversions now only happen to/from extended-linear and XYZ color types, rather than
the old behavior which allowed shortcuting from bounded-linear, which was ok because the clip
would result in the same value. Since conversion from bounded-linear to extended-linear is free
(just a type change, no conversion), the only additional cost here is the gamut mapping.

The implementation of the CSS gamut mapping algorithm itself is a naive iterative bisection
implementation based on the psuedo-code from the spec. As an optimization in the future, we should
consider implementing an analytic solution, which will be more complicated, but likely faster.

Currently, the only place where the effect of this new gamut mapping behavior will be visible is
via functions that explicitly convert to a bounded color space, specifically the CSS color-mix()
function and relative color syntax. Ultimately, we should also be using this to map CSS colors
to the output color space of the screen / HTML canvas, but finding the right place to do that
is not a part of this initial change.

* platform/graphics/ColorConversion.cpp:
(WebCore::ColorConversion<Lab<float>, XYZA<float, WhitePoint::D50>>::convert):
(WebCore::ColorConversion<OKLab<float>, XYZA<float, WhitePoint::D65>>::convert):
Add call to makeFromComponentsClampingExceptAlpha at the end of conversion to Lab/OKLab
as the conversion functions can produce lightness values just ever so less than 0.
In practice, only the lightness value is bounded, so it is the only thing that actually
gets clamped thanks to the constexpr nature of clampedComponent().

* platform/graphics/ColorConversion.h:
(WebCore::computeDeltaEOK):
Add implementation of the deltaEOK algorithm.

(WebCore::ClipGamutMapping::mapToBoundedGamut):
Add implementation of the degenerate gamut mapping algorithm.

(WebCore::CSSGamutMapping::mapToBoundedGamut):
Add implementation of the CSS gamut mapping algorithm.

(WebCore::ColorConversion::convert):
Add a new step to the main pipeline that explicitly handles bounds conversions to/from
extended-linear so that the matrix conversions can rely on always working in extended space.

(WebCore::ColorConversion::toBounded):
Replace call to clamp with call out to gamut mapping algorithm.

(WebCore::ColorConversion::handleMatrixConversion):
Replace makeFromComponentsClampingExceptAlpha with makeFromComponents now that all conversions
are in the extended space and clamping is no-op.

* platform/graphics/ColorTypes.h:
(WebCore::inGamut):
(WebCore::colorIfInGamut):
(WebCore::clipToGamut):
Add some helpers to check/convert/clip to the bounded gamut of RGB types.

Tools:

* TestWebKitAPI/Tests/WebCore/ColorTests.cpp:
(TestWebKitAPI::TEST):
Update color conversion tests to account for gamut mapping and add a new test
that bypasses gamut mapping by converting to extended sRGB.

LayoutTests:

* platform/gtk/TestExpectations:
Add more tests that are only failing on the GTK bots due to a small floating point issues that needs further investigation.

Modified Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (289395 => 289396)


--- trunk/LayoutTests/ChangeLog	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/ChangeLog	2022-02-08 18:57:30 UTC (rev 289396)
@@ -1,3 +1,13 @@
+2022-02-08  Sam Weinig  <wei...@apple.com>
+
+        Conversion to a color space with a smaller gamut should perform gamut mapping
+        https://bugs.webkit.org/show_bug.cgi?id=236200
+
+        Reviewed by Darin Adler.
+
+        * platform/gtk/TestExpectations:
+        Add more tests that are only failing on the GTK bots due to a small floating point issues that needs further investigation.
+
 2022-02-08  Youenn Fablet  <you...@apple.com>
 
         LibWebRTCCodecs SharedVideoFrameWriters can deadlock in case of GPUProcess crash

Modified: trunk/LayoutTests/imported/w3c/ChangeLog (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/ChangeLog	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/ChangeLog	2022-02-08 18:57:30 UTC (rev 289396)
@@ -1,3 +1,22 @@
+2022-02-08  Sam Weinig  <wei...@apple.com>
+
+        Conversion to a color space with a smaller gamut should perform gamut mapping
+        https://bugs.webkit.org/show_bug.cgi?id=236200
+
+        Reviewed by Darin Adler.
+
+        Update tests to include more examples that force gamut mapping and update the results
+        of a few existing ones to account for the new algorithm.
+
+        * web-platform-tests/css/css-color/parsing/color-mix-computed-expected.txt:
+        * web-platform-tests/css/css-color/parsing/color-mix-computed.html:
+        * web-platform-tests/css/css-color/parsing/color-mix-valid-expected.txt:
+        * web-platform-tests/css/css-color/parsing/color-mix-valid.html:
+        * web-platform-tests/css/css-color/parsing/relative-color-computed-expected.txt:
+        * web-platform-tests/css/css-color/parsing/relative-color-computed.html:
+        * web-platform-tests/css/css-color/parsing/relative-color-valid-expected.txt:
+        * web-platform-tests/css/css-color/parsing/relative-color-valid.html:
+
 2022-02-07  Chris Dumez  <cdu...@apple.com>
 
         Add support for sharing Shared Workers (including across WebProcesses)

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-computed-expected.txt (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-computed-expected.txt	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-computed-expected.txt	2022-02-08 18:57:30 UTC (rev 289396)
@@ -62,6 +62,15 @@
 PASS Property color value 'color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40%))'
 PASS Property color value 'color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / 0.5))'
 PASS Property color value 'color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / none))'
+PASS Property color value 'color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hsl, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hsl, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hsl, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hsl, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hsl, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hsl, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hsl, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hsl, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)'
 PASS Property color value 'color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%))'
 PASS Property color value 'color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%))'
 PASS Property color value 'color-mix(in hwb, 25% hwb(120deg 10% 20%), hwb(30deg 30% 40%))'
@@ -125,6 +134,15 @@
 PASS Property color value 'color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40%))'
 PASS Property color value 'color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / 0.5))'
 PASS Property color value 'color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / none))'
+PASS Property color value 'color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hwb, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hwb, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hwb, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hwb, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hwb, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hwb, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hwb, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)'
+PASS Property color value 'color-mix(in hwb, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)'
 PASS Property color value 'color-mix(in lch, lch(10% 20 30deg), lch(50% 60 70deg))'
 PASS Property color value 'color-mix(in lch, lch(10% 20 30deg) 25%, lch(50% 60 70deg))'
 PASS Property color value 'color-mix(in lch, 25% lch(10% 20 30deg), lch(50% 60 70deg))'

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-computed.html (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-computed.html	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-computed.html	2022-02-08 18:57:30 UTC (rev 289396)
@@ -97,7 +97,17 @@
     test_computed_value(`color`, `color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / 0.5))`, canonicalize(`hsl(60deg 40% 40% / 0.5)`));
     test_computed_value(`color`, `color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / none))`, canonicalize(`hsl(60deg 40% 40% / none)`));
 
+    test_computed_value(`color`, `color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_computed_value(`color`, `color-mix(in hsl, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `color-mix(in hsl, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `color-mix(in hsl, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `color-mix(in hsl, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `color-mix(in hsl, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_computed_value(`color`, `color-mix(in hsl, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_computed_value(`color`, `color-mix(in hsl, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_computed_value(`color`, `color-mix(in hsl, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
 
+
     test_computed_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `rgb(147, 179, 52)`);
     test_computed_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%))`, `rgb(166, 153, 64)`);
     test_computed_value(`color`, `color-mix(in hwb, 25% hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `rgb(166, 153, 64)`);
@@ -170,6 +180,16 @@
     test_computed_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / 0.5))`, canonicalize(`hwb(75deg 20% 30% / 0.5)`));
     test_computed_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / none))`, canonicalize(`hwb(75deg 20% 30% / none)`));
 
+    test_computed_value(`color`, `color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_computed_value(`color`, `color-mix(in hwb, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `color-mix(in hwb, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `color-mix(in hwb, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `color-mix(in hwb, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `color-mix(in hwb, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_computed_value(`color`, `color-mix(in hwb, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_computed_value(`color`, `color-mix(in hwb, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_computed_value(`color`, `color-mix(in hwb, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
+
     for (const colorSpace of [ "lch", "oklch" ]) {
         test_computed_value(`color`, `color-mix(in ${colorSpace}, ${colorSpace}(10% 20 30deg), ${colorSpace}(50% 60 70deg))`, `${colorSpace}(30% 40 50)`);
         test_computed_value(`color`, `color-mix(in ${colorSpace}, ${colorSpace}(10% 20 30deg) 25%, ${colorSpace}(50% 60 70deg))`, `${colorSpace}(40% 50 60)`);

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-valid-expected.txt (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-valid-expected.txt	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-valid-expected.txt	2022-02-08 18:57:30 UTC (rev 289396)
@@ -62,6 +62,15 @@
 PASS e.style['color'] = "color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40%))" should set the property value
 PASS e.style['color'] = "color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / 0.5))" should set the property value
 PASS e.style['color'] = "color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / none))" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hsl, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)" should set the property value
 PASS e.style['color'] = "color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%))" should set the property value
 PASS e.style['color'] = "color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%))" should set the property value
 PASS e.style['color'] = "color-mix(in hwb, 25% hwb(120deg 10% 20%), hwb(30deg 30% 40%))" should set the property value
@@ -125,6 +134,15 @@
 PASS e.style['color'] = "color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40%))" should set the property value
 PASS e.style['color'] = "color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / 0.5))" should set the property value
 PASS e.style['color'] = "color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / none))" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)" should set the property value
+PASS e.style['color'] = "color-mix(in hwb, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)" should set the property value
 PASS e.style['color'] = "color-mix(in lch, lch(10% 20 30deg), lch(50% 60 70deg))" should set the property value
 PASS e.style['color'] = "color-mix(in lch, lch(10% 20 30deg) 25%, lch(50% 60 70deg))" should set the property value
 PASS e.style['color'] = "color-mix(in lch, 25% lch(10% 20 30deg), lch(50% 60 70deg))" should set the property value

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-valid.html (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-valid.html	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-valid.html	2022-02-08 18:57:30 UTC (rev 289396)
@@ -97,7 +97,17 @@
     test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / 0.5))`, canonicalize(`hsl(60deg 40% 40% / 0.5)`));
     test_valid_value(`color`, `color-mix(in hsl, hsl(120deg 40% 40% / none), hsl(0deg 40% 40% / none))`, canonicalize(`hsl(60deg 40% 40% / none)`));
 
+    test_valid_value(`color`, `color-mix(in hsl, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_valid_value(`color`, `color-mix(in hsl, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `color-mix(in hsl, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `color-mix(in hsl, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `color-mix(in hsl, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `color-mix(in hsl, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_valid_value(`color`, `color-mix(in hsl, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_valid_value(`color`, `color-mix(in hsl, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_valid_value(`color`, `color-mix(in hsl, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
 
+
     test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `rgb(147, 179, 52)`);
     test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20%) 25%, hwb(30deg 30% 40%))`, `rgb(166, 153, 64)`);
     test_valid_value(`color`, `color-mix(in hwb, 25% hwb(120deg 10% 20%), hwb(30deg 30% 40%))`, `rgb(166, 153, 64)`);
@@ -170,6 +180,16 @@
     test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / 0.5))`, canonicalize(`hwb(75deg 20% 30% / 0.5)`));
     test_valid_value(`color`, `color-mix(in hwb, hwb(120deg 10% 20% / none), hwb(30deg 30% 40% / none))`, canonicalize(`hwb(75deg 20% 30% / none)`));
 
+    test_valid_value(`color`, `color-mix(in hwb, color(display-p3 0 1 0) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_valid_value(`color`, `color-mix(in hwb, lab(100% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `color-mix(in hwb, lab(0% 104.3 -50.9) 100%, rgb(0, 0, 0) 0%)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `color-mix(in hwb, lch(100% 116 334) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `color-mix(in hwb, lch(0% 116 334) 100%, rgb(0, 0, 0) 0%)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `color-mix(in hwb, oklab(100% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_valid_value(`color`, `color-mix(in hwb, oklab(0% 0.365 -0.16) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_valid_value(`color`, `color-mix(in hwb, oklch(100% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_valid_value(`color`, `color-mix(in hwb, oklch(0% 0.399 336.3) 100%, rgb(0, 0, 0) 0%)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
+
     for (const colorSpace of [ "lch", "oklch" ]) {
         test_valid_value(`color`, `color-mix(in ${colorSpace}, ${colorSpace}(10% 20 30deg), ${colorSpace}(50% 60 70deg))`, `${colorSpace}(30% 40 50)`);
         test_valid_value(`color`, `color-mix(in ${colorSpace}, ${colorSpace}(10% 20 30deg) 25%, ${colorSpace}(50% 60 70deg))`, `${colorSpace}(40% 50 60)`);

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-computed-expected.txt (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-computed-expected.txt	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-computed-expected.txt	2022-02-08 18:57:30 UTC (rev 289396)
@@ -5,6 +5,14 @@
 PASS Property color value 'rgb(from hsl(120deg 20% 50% / .5) r g b / alpha)'
 PASS Property color value 'rgb(from rgb(from rebeccapurple r g b) r g b)'
 PASS Property color value 'rgb(from color(display-p3 0 1 0) r g b / alpha)'
+PASS Property color value 'rgb(from lab(100% 104.3 -50.9) r g b)'
+PASS Property color value 'rgb(from lab(0% 104.3 -50.9) r g b)'
+PASS Property color value 'rgb(from lch(100% 116 334) r g b)'
+PASS Property color value 'rgb(from lch(0% 116 334) r g b)'
+PASS Property color value 'rgb(from oklab(100% 0.365 -0.16) r g b)'
+PASS Property color value 'rgb(from oklab(0% 0.365 -0.16) r g b)'
+PASS Property color value 'rgb(from oklch(100% 0.399 336.3) r g b)'
+PASS Property color value 'rgb(from oklch(0% 0.399 336.3) r g b)'
 PASS Property color value 'rgb(from rebeccapurple 0 0 0)'
 PASS Property color value 'rgb(from rebeccapurple 0 0 0 / 0)'
 PASS Property color value 'rgb(from rebeccapurple 0 g b / alpha)'
@@ -74,6 +82,14 @@
 PASS Property color value 'hsl(from hsl(120deg 20% 50% / .5) h s l / alpha)'
 PASS Property color value 'hsl(from hsl(from rebeccapurple h s l) h s l)'
 PASS Property color value 'hsl(from color(display-p3 0 1 0) h s l / alpha)'
+PASS Property color value 'hsl(from lab(100% 104.3 -50.9) h s l)'
+PASS Property color value 'hsl(from lab(0% 104.3 -50.9) h s l)'
+PASS Property color value 'hsl(from lch(100% 116 334) h s l)'
+PASS Property color value 'hsl(from lch(0% 116 334) h s l)'
+PASS Property color value 'hsl(from oklab(100% 0.365 -0.16) h s l)'
+PASS Property color value 'hsl(from oklab(0% 0.365 -0.16) h s l)'
+PASS Property color value 'hsl(from oklch(100% 0.399 336.3) h s l)'
+PASS Property color value 'hsl(from oklch(0% 0.399 336.3) h s l)'
 PASS Property color value 'hsl(from rebeccapurple 0 0% 0%)'
 PASS Property color value 'hsl(from rebeccapurple 0deg 0% 0%)'
 PASS Property color value 'hsl(from rebeccapurple 0 0% 0% / 0)'
@@ -128,6 +144,14 @@
 PASS Property color value 'hwb(from hsl(120deg 20% 50% / .5) h w b / alpha)'
 PASS Property color value 'hwb(from hwb(from rebeccapurple h w b) h w b)'
 PASS Property color value 'hwb(from color(display-p3 0 1 0) h w b / alpha)'
+PASS Property color value 'hwb(from lab(100% 104.3 -50.9) h w b)'
+PASS Property color value 'hwb(from lab(0% 104.3 -50.9) h w b)'
+PASS Property color value 'hwb(from lch(100% 116 334) h w b)'
+PASS Property color value 'hwb(from lch(0% 116 334) h w b)'
+PASS Property color value 'hwb(from oklab(100% 0.365 -0.16) h w b)'
+PASS Property color value 'hwb(from oklab(0% 0.365 -0.16) h w b)'
+PASS Property color value 'hwb(from oklch(100% 0.399 336.3) h w b)'
+PASS Property color value 'hwb(from oklch(0% 0.399 336.3) h w b)'
 PASS Property color value 'hwb(from rebeccapurple 0 0% 0%)'
 PASS Property color value 'hwb(from rebeccapurple 0deg 0% 0%)'
 PASS Property color value 'hwb(from rebeccapurple 0 0% 0% / 0)'

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-computed.html (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-computed.html	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-computed.html	2022-02-08 18:57:30 UTC (rev 289396)
@@ -33,8 +33,16 @@
     // Test nesting relative colors.
     test_computed_value(`color`, `rgb(from rgb(from rebeccapurple r g b) r g b)`, `rgb(102, 51, 153)`);
 
-    // Testing non-sRGB origin colors to see gamut clipping.
-    test_computed_value(`color`, `rgb(from color(display-p3 0 1 0) r g b / alpha)`, `rgb(0, 255, 0)`);
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test_computed_value(`color`, `rgb(from color(display-p3 0 1 0) r g b / alpha)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_computed_value(`color`, `rgb(from lab(100% 104.3 -50.9) r g b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `rgb(from lab(0% 104.3 -50.9) r g b)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `rgb(from lch(100% 116 334) r g b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `rgb(from lch(0% 116 334) r g b)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `rgb(from oklab(100% 0.365 -0.16) r g b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_computed_value(`color`, `rgb(from oklab(0% 0.365 -0.16) r g b)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_computed_value(`color`, `rgb(from oklch(100% 0.399 336.3) r g b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_computed_value(`color`, `rgb(from oklch(0% 0.399 336.3) r g b)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
 
     // Testing replacement with 0.
     test_computed_value(`color`, `rgb(from rebeccapurple 0 0 0)`, `rgb(0, 0, 0)`);
@@ -131,8 +139,16 @@
     // Test nesting relative colors.
     test_computed_value(`color`, `hsl(from hsl(from rebeccapurple h s l) h s l)`, `rgb(102, 51, 153)`);
 
-    // Testing non-sRGB origin colors to see gamut clipping.
-    test_computed_value(`color`, `hsl(from color(display-p3 0 1 0) h s l / alpha)`, `rgb(0, 255, 0)`);
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test_computed_value(`color`, `hsl(from color(display-p3 0 1 0) h s l / alpha)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_computed_value(`color`, `hsl(from lab(100% 104.3 -50.9) h s l)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `hsl(from lab(0% 104.3 -50.9) h s l)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `hsl(from lch(100% 116 334) h s l)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `hsl(from lch(0% 116 334) h s l)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `hsl(from oklab(100% 0.365 -0.16) h s l)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_computed_value(`color`, `hsl(from oklab(0% 0.365 -0.16) h s l)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_computed_value(`color`, `hsl(from oklch(100% 0.399 336.3) h s l)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_computed_value(`color`, `hsl(from oklch(0% 0.399 336.3) h s l)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
 
     // Testing replacement with 0.
     test_computed_value(`color`, `hsl(from rebeccapurple 0 0% 0%)`, `rgb(0, 0, 0)`);
@@ -204,8 +220,16 @@
     // Test nesting relative colors.
     test_computed_value(`color`, `hwb(from hwb(from rebeccapurple h w b) h w b)`, `rgb(102, 51, 153)`);
 
-    // Testing non-sRGB origin colors to see gamut clipping.
-    test_computed_value(`color`, `hwb(from color(display-p3 0 1 0) h w b / alpha)`, `rgb(0, 255, 0)`);
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test_computed_value(`color`, `hwb(from color(display-p3 0 1 0) h w b / alpha)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_computed_value(`color`, `hwb(from lab(100% 104.3 -50.9) h w b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `hwb(from lab(0% 104.3 -50.9) h w b)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `hwb(from lch(100% 116 334) h w b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_computed_value(`color`, `hwb(from lch(0% 116 334) h w b)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_computed_value(`color`, `hwb(from oklab(100% 0.365 -0.16) h w b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_computed_value(`color`, `hwb(from oklab(0% 0.365 -0.16) h w b)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_computed_value(`color`, `hwb(from oklch(100% 0.399 336.3) h w b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_computed_value(`color`, `hwb(from oklch(0% 0.399 336.3) h w b)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
 
     // Testing replacement with 0.
     test_computed_value(`color`, `hwb(from rebeccapurple 0 0% 0%)`, `rgb(255, 0, 0)`);
@@ -342,7 +366,7 @@
         // Test nesting relative colors.
         test_computed_value(`color`, `${colorSpace}(from ${colorSpace}(from ${colorSpace}(70% 45 30) l c h) l c h)`, `${colorSpace}(70% 45 30)`);
 
-        // Testing non-sRGB origin colors to see gamut clipping.
+        // Testing non-sRGB origin colors (no gamut mapping will happen since the destination is not a bounded RGB color space).
         test_computed_value(`color`, `${colorSpace}(from color(display-p3 0 0 0) l c h / alpha)`, `${colorSpace}(0% 0 0)`);
         test_computed_value(`color`, `${colorSpace}(from ${rectangularForm}(70% 45 30) l c h / alpha)`, `${colorSpace}(70% 54.08327 33.690067)`);
 

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-valid-expected.txt (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-valid-expected.txt	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-valid-expected.txt	2022-02-08 18:57:30 UTC (rev 289396)
@@ -5,6 +5,14 @@
 PASS e.style['color'] = "rgb(from hsl(120deg 20% 50% / .5) r g b / alpha)" should set the property value
 PASS e.style['color'] = "rgb(from rgb(from rebeccapurple r g b) r g b)" should set the property value
 PASS e.style['color'] = "rgb(from color(display-p3 0 1 0) r g b / alpha)" should set the property value
+PASS e.style['color'] = "rgb(from lab(100% 104.3 -50.9) r g b)" should set the property value
+PASS e.style['color'] = "rgb(from lab(0% 104.3 -50.9) r g b)" should set the property value
+PASS e.style['color'] = "rgb(from lch(100% 116 334) r g b)" should set the property value
+PASS e.style['color'] = "rgb(from lch(0% 116 334) r g b)" should set the property value
+PASS e.style['color'] = "rgb(from oklab(100% 0.365 -0.16) r g b)" should set the property value
+PASS e.style['color'] = "rgb(from oklab(0% 0.365 -0.16) r g b)" should set the property value
+PASS e.style['color'] = "rgb(from oklch(100% 0.399 336.3) r g b)" should set the property value
+PASS e.style['color'] = "rgb(from oklch(0% 0.399 336.3) r g b)" should set the property value
 PASS e.style['color'] = "rgb(from rebeccapurple 0 0 0)" should set the property value
 PASS e.style['color'] = "rgb(from rebeccapurple 0 0 0 / 0)" should set the property value
 PASS e.style['color'] = "rgb(from rebeccapurple 0 g b / alpha)" should set the property value
@@ -74,6 +82,14 @@
 PASS e.style['color'] = "hsl(from hsl(120deg 20% 50% / .5) h s l / alpha)" should set the property value
 PASS e.style['color'] = "hsl(from hsl(from rebeccapurple h s l) h s l)" should set the property value
 PASS e.style['color'] = "hsl(from color(display-p3 0 1 0) h s l / alpha)" should set the property value
+PASS e.style['color'] = "hsl(from lab(100% 104.3 -50.9) h s l)" should set the property value
+PASS e.style['color'] = "hsl(from lab(0% 104.3 -50.9) h s l)" should set the property value
+PASS e.style['color'] = "hsl(from lch(100% 116 334) h s l)" should set the property value
+PASS e.style['color'] = "hsl(from lch(0% 116 334) h s l)" should set the property value
+PASS e.style['color'] = "hsl(from oklab(100% 0.365 -0.16) h s l)" should set the property value
+PASS e.style['color'] = "hsl(from oklab(0% 0.365 -0.16) h s l)" should set the property value
+PASS e.style['color'] = "hsl(from oklch(100% 0.399 336.3) h s l)" should set the property value
+PASS e.style['color'] = "hsl(from oklch(0% 0.399 336.3) h s l)" should set the property value
 PASS e.style['color'] = "hsl(from rebeccapurple 0 0% 0%)" should set the property value
 PASS e.style['color'] = "hsl(from rebeccapurple 0deg 0% 0%)" should set the property value
 PASS e.style['color'] = "hsl(from rebeccapurple 0 0% 0% / 0)" should set the property value
@@ -128,6 +144,14 @@
 PASS e.style['color'] = "hwb(from hsl(120deg 20% 50% / .5) h w b / alpha)" should set the property value
 PASS e.style['color'] = "hwb(from hwb(from rebeccapurple h w b) h w b)" should set the property value
 PASS e.style['color'] = "hwb(from color(display-p3 0 1 0) h w b / alpha)" should set the property value
+PASS e.style['color'] = "hwb(from lab(100% 104.3 -50.9) h w b)" should set the property value
+PASS e.style['color'] = "hwb(from lab(0% 104.3 -50.9) h w b)" should set the property value
+PASS e.style['color'] = "hwb(from lch(100% 116 334) h w b)" should set the property value
+PASS e.style['color'] = "hwb(from lch(0% 116 334) h w b)" should set the property value
+PASS e.style['color'] = "hwb(from oklab(100% 0.365 -0.16) h w b)" should set the property value
+PASS e.style['color'] = "hwb(from oklab(0% 0.365 -0.16) h w b)" should set the property value
+PASS e.style['color'] = "hwb(from oklch(100% 0.399 336.3) h w b)" should set the property value
+PASS e.style['color'] = "hwb(from oklch(0% 0.399 336.3) h w b)" should set the property value
 PASS e.style['color'] = "hwb(from rebeccapurple 0 0% 0%)" should set the property value
 PASS e.style['color'] = "hwb(from rebeccapurple 0deg 0% 0%)" should set the property value
 PASS e.style['color'] = "hwb(from rebeccapurple 0 0% 0% / 0)" should set the property value

Modified: trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-valid.html (289395 => 289396)


--- trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-valid.html	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-valid.html	2022-02-08 18:57:30 UTC (rev 289396)
@@ -32,8 +32,16 @@
     // Test nesting relative colors.
     test_valid_value(`color`, `rgb(from rgb(from rebeccapurple r g b) r g b)`, `rgb(102, 51, 153)`);
 
-    // Testing non-sRGB origin colors to see gamut clipping.
-    test_valid_value(`color`, `rgb(from color(display-p3 0 1 0) r g b / alpha)`, `rgb(0, 255, 0)`);
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test_valid_value(`color`, `rgb(from color(display-p3 0 1 0) r g b / alpha)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_valid_value(`color`, `rgb(from lab(100% 104.3 -50.9) r g b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `rgb(from lab(0% 104.3 -50.9) r g b)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `rgb(from lch(100% 116 334) r g b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `rgb(from lch(0% 116 334) r g b)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `rgb(from oklab(100% 0.365 -0.16) r g b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_valid_value(`color`, `rgb(from oklab(0% 0.365 -0.16) r g b)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_valid_value(`color`, `rgb(from oklch(100% 0.399 336.3) r g b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_valid_value(`color`, `rgb(from oklch(0% 0.399 336.3) r g b)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
 
     // Testing replacement with 0.
     test_valid_value(`color`, `rgb(from rebeccapurple 0 0 0)`, `rgb(0, 0, 0)`);
@@ -130,8 +138,16 @@
     // Test nesting relative colors.
     test_valid_value(`color`, `hsl(from hsl(from rebeccapurple h s l) h s l)`, `rgb(102, 51, 153)`);
 
-    // Testing non-sRGB origin colors to see gamut clipping.
-    test_valid_value(`color`, `hsl(from color(display-p3 0 1 0) h s l / alpha)`, `rgb(0, 255, 0)`);
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test_valid_value(`color`, `hsl(from color(display-p3 0 1 0) h s l / alpha)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_valid_value(`color`, `hsl(from lab(100% 104.3 -50.9) h s l)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `hsl(from lab(0% 104.3 -50.9) h s l)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `hsl(from lch(100% 116 334) h s l)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `hsl(from lch(0% 116 334) h s l)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `hsl(from oklab(100% 0.365 -0.16) h s l)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_valid_value(`color`, `hsl(from oklab(0% 0.365 -0.16) h s l)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_valid_value(`color`, `hsl(from oklch(100% 0.399 336.3) h s l)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_valid_value(`color`, `hsl(from oklch(0% 0.399 336.3) h s l)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
 
     // Testing replacement with 0.
     test_valid_value(`color`, `hsl(from rebeccapurple 0 0% 0%)`, `rgb(0, 0, 0)`);
@@ -203,8 +219,16 @@
     // Test nesting relative colors.
     test_valid_value(`color`, `hwb(from hwb(from rebeccapurple h w b) h w b)`, `rgb(102, 51, 153)`);
 
-    // Testing non-sRGB origin colors to see gamut clipping.
-    test_valid_value(`color`, `hwb(from color(display-p3 0 1 0) h w b / alpha)`, `rgb(0, 255, 0)`);
+    // Testing non-sRGB origin colors to see gamut mapping.
+    test_valid_value(`color`, `hwb(from color(display-p3 0 1 0) h w b / alpha)`, `rgb(0, 249, 66)`); // Naive clip based mapping would give rgb(0, 255, 0).
+    test_valid_value(`color`, `hwb(from lab(100% 104.3 -50.9) h w b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `hwb(from lab(0% 104.3 -50.9) h w b)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `hwb(from lch(100% 116 334) h w b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 150, 255).
+    test_valid_value(`color`, `hwb(from lch(0% 116 334) h w b)`, `rgb(42, 0, 34)`); // Naive clip based mapping would give rgb(90, 0, 76). NOTE: 0% lightness in Lab/LCH does not automatically correspond with sRGB black,
+    test_valid_value(`color`, `hwb(from oklab(100% 0.365 -0.16) h w b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 92, 255).
+    test_valid_value(`color`, `hwb(from oklab(0% 0.365 -0.16) h w b)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(19, 0, 24).
+    test_valid_value(`color`, `hwb(from oklch(100% 0.399 336.3) h w b)`, `rgb(255, 255, 255)`); // Naive clip based mapping would give rgb(255, 91, 255).
+    test_valid_value(`color`, `hwb(from oklch(0% 0.399 336.3) h w b)`, `rgb(0, 0, 0)`); // Naive clip based mapping would give rgb(20, 0, 24).
 
     // Testing replacement with 0.
     test_valid_value(`color`, `hwb(from rebeccapurple 0 0% 0%)`, `rgb(255, 0, 0)`);
@@ -341,7 +365,7 @@
         // Test nesting relative colors.
         test_valid_value(`color`, `${colorSpace}(from ${colorSpace}(from ${colorSpace}(70% 45 30) l c h) l c h)`, `${colorSpace}(70% 45 30)`);
 
-        // Testing non-sRGB origin colors to see gamut clipping.
+        // Testing non-sRGB origin colors (no gamut mapping will happen since the destination is not a bounded RGB color space).
         test_valid_value(`color`, `${colorSpace}(from color(display-p3 0 0 0) l c h / alpha)`, `${colorSpace}(0% 0 0)`);
         test_valid_value(`color`, `${colorSpace}(from ${rectangularForm}(70% 45 30) l c h / alpha)`, `${colorSpace}(70% 54.08327 33.690067)`);
 

Modified: trunk/LayoutTests/platform/gtk/TestExpectations (289395 => 289396)


--- trunk/LayoutTests/platform/gtk/TestExpectations	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/LayoutTests/platform/gtk/TestExpectations	2022-02-08 18:57:30 UTC (rev 289396)
@@ -1838,8 +1838,11 @@
 
 webkit.org/b/230277 imported/w3c/web-platform-tests/css/css-transforms/3dtransform-and-filter-no-perspective-001.html [ ImageOnlyFailure ]
 
+webkit.org/b/234726 imported/w3c/web-platform-tests/css/css-color/color-mix-non-srgb-001.html [ Failure ]
 webkit.org/b/234726 imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-computed.html  [ Failure ]
 webkit.org/b/234726 imported/w3c/web-platform-tests/css/css-color/parsing/relative-color-valid.html [ Failure ]
+webkit.org/b/234726 imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-computed.html [ Failure ]
+webkit.org/b/234726 imported/w3c/web-platform-tests/css/css-color/parsing/color-mix-valid.html [ Failure ]
 
 webkit.org/b/234782 imported/w3c/web-platform-tests/css/css-writing-modes/text-shadow-orientation-upright-001.html [ ImageOnlyFailure ]
 

Modified: trunk/Source/WebCore/ChangeLog (289395 => 289396)


--- trunk/Source/WebCore/ChangeLog	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/Source/WebCore/ChangeLog	2022-02-08 18:57:30 UTC (rev 289396)
@@ -1,3 +1,72 @@
+2022-02-08  Sam Weinig  <wei...@apple.com>
+
+        Conversion to a color space with a smaller gamut should perform gamut mapping
+        https://bugs.webkit.org/show_bug.cgi?id=236200
+
+        Reviewed by Darin Adler.
+
+        CSS Color now defines that gamut mapping should happen when converting to an RGB color space
+        with bounded gamut that is smaller than the origin color's color space. Specifically, it
+        specifies the use of a new "CSS gamut mapping algorithm" https://drafts.csswg.org/css-color/#css-gamut-mapping
+        which implements a relative colorimetric intent mapping with colors inside the destination
+        gamut unchanged.
+
+        The previous behavior we implemented was to clip out of gamut colors to the gamut (e.g. 
+        color(srgb 1.4 -0.2 .5) would become color(srgb 1 0 .5)) which can lead to very odd results.
+
+        To keep things simple, the gamut mapping has been incorporated directly into the main color
+        conversion pipeline, replacing the call to makeFromComponentsClampingExceptAlpha in toBounded() 
+        with one implements the new algorithm. To accomadate this, the pipeline was modified so that
+        the matrix conversions now only happen to/from extended-linear and XYZ color types, rather than
+        the old behavior which allowed shortcuting from bounded-linear, which was ok because the clip
+        would result in the same value. Since conversion from bounded-linear to extended-linear is free
+        (just a type change, no conversion), the only additional cost here is the gamut mapping.
+
+        The implementation of the CSS gamut mapping algorithm itself is a naive iterative bisection
+        implementation based on the psuedo-code from the spec. As an optimization in the future, we should
+        consider implementing an analytic solution, which will be more complicated, but likely faster.
+
+        Currently, the only place where the effect of this new gamut mapping behavior will be visible is
+        via functions that explicitly convert to a bounded color space, specifically the CSS color-mix()
+        function and relative color syntax. Ultimately, we should also be using this to map CSS colors
+        to the output color space of the screen / HTML canvas, but finding the right place to do that
+        is not a part of this initial change.
+
+        * platform/graphics/ColorConversion.cpp:
+        (WebCore::ColorConversion<Lab<float>, XYZA<float, WhitePoint::D50>>::convert):
+        (WebCore::ColorConversion<OKLab<float>, XYZA<float, WhitePoint::D65>>::convert):
+        Add call to makeFromComponentsClampingExceptAlpha at the end of conversion to Lab/OKLab
+        as the conversion functions can produce lightness values just ever so less than 0.
+        In practice, only the lightness value is bounded, so it is the only thing that actually
+        gets clamped thanks to the constexpr nature of clampedComponent().
+
+        * platform/graphics/ColorConversion.h:
+        (WebCore::computeDeltaEOK):
+        Add implementation of the deltaEOK algorithm.
+
+        (WebCore::ClipGamutMapping::mapToBoundedGamut):
+        Add implementation of the degenerate gamut mapping algorithm.
+
+        (WebCore::CSSGamutMapping::mapToBoundedGamut):
+        Add implementation of the CSS gamut mapping algorithm.
+
+        (WebCore::ColorConversion::convert):
+        Add a new step to the main pipeline that explicitly handles bounds conversions to/from 
+        extended-linear so that the matrix conversions can rely on always working in extended space.
+
+        (WebCore::ColorConversion::toBounded):
+        Replace call to clamp with call out to gamut mapping algorithm.
+
+        (WebCore::ColorConversion::handleMatrixConversion):
+        Replace makeFromComponentsClampingExceptAlpha with makeFromComponents now that all conversions
+        are in the extended space and clamping is no-op.
+
+        * platform/graphics/ColorTypes.h:
+        (WebCore::inGamut):
+        (WebCore::colorIfInGamut):
+        (WebCore::clipToGamut):
+        Add some helpers to check/convert/clip to the bounded gamut of RGB types.
+
 2022-02-07  Yusuke Suzuki  <ysuz...@apple.com>
 
         [WebCore] JSValueInWrappedObject is not correct for concurrent GC

Modified: trunk/Source/WebCore/platform/graphics/ColorConversion.cpp (289395 => 289396)


--- trunk/Source/WebCore/platform/graphics/ColorConversion.cpp	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/Source/WebCore/platform/graphics/ColorConversion.cpp	2022-02-08 18:57:30 UTC (rev 289396)
@@ -44,7 +44,7 @@
     float hue = rad2deg(atan2(b, a));
     float chroma = std::hypot(a, b);
 
-    return { lightness, chroma, hue >= 0 ? hue : hue + 360, alpha };
+    return { lightness, chroma, hue >= 0.0f ? hue : hue + 360.0f, alpha };
 }
 
 template<typename LabLike, typename LCHLike>
@@ -272,7 +272,7 @@
     float a = 500.0f * (f0 - f1);
     float b = 200.0f * (f1 - f2);
 
-    return { lightness, a, b, alpha };
+    return makeFromComponentsClampingExceptAlpha<Lab<float>>(lightness, a, b, alpha);
 }
 
 // MARK: LCH conversions.
@@ -358,7 +358,7 @@
     auto [lightness, a, b] = NonLinearLMSToOKLab.transformedColorComponents(nonLinearLMS);
 
     // 4. Transform lightness from unit lightness to percentage lightness.
-    return { lightness * 100.0f, a, b, alpha };
+    return makeFromComponentsClampingExceptAlpha<OKLab<float>>(lightness * 100.0f, a, b, alpha);
 }
 
 // MARK: OKLCH conversions.

Modified: trunk/Source/WebCore/platform/graphics/ColorConversion.h (289395 => 289396)


--- trunk/Source/WebCore/platform/graphics/ColorConversion.h	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/Source/WebCore/platform/graphics/ColorConversion.h	2022-02-08 18:57:30 UTC (rev 289396)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017-2021 Apple Inc. All rights reserved.
+ * Copyright (C) 2017-2022 Apple Inc. All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions
@@ -26,6 +26,7 @@
 #pragma once
 
 #include "ColorTypes.h"
+#include <wtf/MathExtras.h>
 
 namespace WebCore {
 
@@ -139,7 +140,90 @@
     }
 };
 
+// MARK: DeltaE color difference algorithms.
 
+template<typename ColorType1, typename ColorType2> inline constexpr float computeDeltaEOK(ColorType1 color1, ColorType2 color2)
+{
+    // https://drafts.csswg.org/css-color/#color-difference-OK
+
+    auto [L1, a1, b1, alpha1] = convertColor<OKLab<float>>(color1).resolved();
+    auto [L2, a2, b2, alpha2] = convertColor<OKLab<float>>(color2).resolved();
+
+    auto deltaL = (L1 / 100.0f) - (L2 / 100.0f);
+    auto deltaA = a1 - a2;
+    auto deltaB = b1 - b2;
+
+    return std::hypot(deltaL, deltaA, deltaB);
+}
+
+// MARK: Gamut mapping algorithms.
+
+struct ClipGamutMapping {
+    template<typename ColorType> static auto mapToBoundedGamut(const ColorType& color) -> typename ColorType::BoundedCounterpart
+    {
+        return clipToGamut<typename ColorType::BoundedCounterpart>(color);
+    }
+};
+
+struct CSSGamutMapping {
+    // This implements the CSS gamut mapping algorithm (https://drafts.csswg.org/css-color/#css-gamut-mapping) for RGB
+    // colors that are out of gamut for a particular RGB color space. It implements a relative colorimetric intent mapping
+    // for colors that are outside the destionation gamut and leaves colors inside the destination gamut unchanged.
+
+    // A simple optimization over the psuedocode in the specification has been made to avoid unnecessary work in the
+    // main bisection loop by checking the gamut using the extended linear color space of the RGB family regardless of
+    // of whether the final type is gamma encoded or not. This avoids unnecessary gamma encoding for non final loops.
+
+    // FIXME: This is a naive iterative solution that works for any bounded RGB color space. This can be optimized by
+    // using an analytical solution that computes the exact intersection.
+
+    static constexpr float JND = 0.02f;
+
+    template<typename ColorType> static auto mapToBoundedGamut(const ColorType& color) -> typename ColorType::BoundedCounterpart
+    {
+        using BoundedColorType = typename ColorType::BoundedCounterpart;
+        using ExtendedLinearColorType = ExtendedLinearEncoded<typename BoundedColorType::ComponentType, typename BoundedColorType::Descriptor>;
+        using BoundedLinearColorType = BoundedLinearEncoded<typename BoundedColorType::ComponentType, typename BoundedColorType::Descriptor>;
+
+        auto resolvedColor = color.resolved();
+
+        if (auto result = colorIfInGamut<BoundedColorType>(resolvedColor))
+            return *result;
+
+        auto colorInOKLCHColorSpace = convertColor<OKLCHA<float>>(resolvedColor).resolved();
+
+        if (WTF::areEssentiallyEqual(colorInOKLCHColorSpace.lightness, 100.0f) || colorInOKLCHColorSpace.lightness > 100.0f)
+            return { 1.0, 1.0, 1.0, resolvedColor.alpha };
+        else if (WTF::areEssentiallyEqual(colorInOKLCHColorSpace.lightness, 0.0f))
+            return { 0.0f, 0.0f, 0.0f, resolvedColor.alpha };
+
+        float min = 0.0f;
+        float max = colorInOKLCHColorSpace.chroma;
+
+        while (true) {
+            auto chroma = (min + max) / 2.0f;
+
+            auto current = colorInOKLCHColorSpace;
+            current.chroma = chroma;
+
+            auto currentInExtendedLinearColorSpace = convertColor<ExtendedLinearColorType>(current).resolved();
+
+            if (inGamut<BoundedLinearColorType>(currentInExtendedLinearColorSpace)) {
+                min = chroma;
+                continue;
+            }
+
+            auto currentClippedToBoundedLinearColorSpace = clipToGamut<BoundedLinearColorType>(currentInExtendedLinearColorSpace);
+
+            auto deltaE = computeDeltaEOK(currentClippedToBoundedLinearColorSpace, current);
+            if (deltaE < JND)
+                return convertColor<BoundedColorType>(currentClippedToBoundedLinearColorSpace);
+
+            max = chroma;
+        }
+    }
+};
+
 // Main conversion.
 
 //                ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
@@ -152,12 +236,16 @@
 //       │                         │                  │                  │                               │                               │                               │                         │
 //       │        │                │                  │                  │                               │                               │                               │                │        │
 //       │          ProPhotoRGB───────────────────┐   │   SRGB──────────────────────────┐ DisplayP3─────────────────────┐ A98RGB────────────────────────┐ Rec2020───────────────────────┐          │
-//       │        │ │┌────────┐ ┌────────────────┐│   │   │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│ │        │
-//       │          ││ Linear │ │ LinearExtended ││   │   ││ Linear │ │ LinearExtended ││ ││ Linear │ │ LinearExtended ││ ││ Linear │ │ LinearExtended ││ ││ Linear │ │ LinearExtended ││          │
-//       │        │ │└────────┘ └────────────────┘│   │   │└────────┘ └────────────────┘│ │└────────┘ └────────────────┘│ │└────────┘ └────────────────┘│ │└────────┘ └────────────────┘│ │        │
-//       │         ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─│─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─         │
+//       │        │ │           ┌────────────────┐│   │   │           ┌────────────────┐│ │           ┌────────────────┐│ │           ┌────────────────┐│ │           ┌────────────────┐│ │        │
+//       │          │     ┌─────▶︎ LinearExtended ││   │   │     ┌─────▶︎ LinearExtended ││ │     ┌─────▶︎ LinearExtended ││ │     ┌─────▶︎ LinearExtended ││ │     ┌─────▶︎ LinearExtended ││          │
+//       │        │ │     │     └────────▲───────┘│   │   │     │     └────────▲───────┘│ │     │     └────────▲───────┘│ │     │     └────────▲───────┘│ │     │     └────────▲───────┘│ │        │
+//       │         ─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─ ─│─ ─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─         │
+//       │          │┌────────┐          │        │   │   │┌────────┐          │        │ │┌────────┐          │        │ │┌────────┐          │        │ │┌────────┐          │        │          │
+//       │          ││ Linear │          │        │   │   ││ Linear │          │        │ ││ Linear │          │        │ ││ Linear │          │        │ ││ Linear │          │        │          │
+//       │          │└────▲───┘          │        │   │   │└────▲───┘          │        │ │└────▲───┘          │        │ │└────▲───┘          │        │ │└────▲───┘          │        │          │
+//       │          │     │              │        │   │   │     │              │        │ │     │              │        │ │     │              │        │ │     │              │        │          │
 // ┌───────────┐    │┌────────┐ ┌────────────────┐│   │   │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│    ┌───────────┐
-// │    Lab    │    ││ Gamma  │ │ GammaExtended  ││   │   ││ Gamma  │ │ GammaExtended  ││ ││ Gamma  │ │ GammaExtended  ││ ││ Gamma  │ │ GammaExtended  ││ ││ Gamma  │ │ GammaExtended  ││    │   OKLab   │
+// │    Lab    │    ││ Gamma  │─│ GammaExtended  ││   │   ││ Gamma  │─│ GammaExtended  ││ ││ Gamma  │─│ GammaExtended  ││ ││ Gamma  │─│ GammaExtended  ││ ││ Gamma  │─│ GammaExtended  ││    │   OKLab   │
 // └─────▲─────┘    │└────────┘ └────────────────┘│   │   │└────▲───┘ └────────────────┘│ │└────────┘ └────────────────┘│ │└────────┘ └────────────────┘│ │└────────┘ └────────────────┘│    └─────▲─────┘
 //       │          └─────────────────────────────┘   │   └─────┼───────────────────────┘ └─────────────────────────────┘ └─────────────────────────────┘ └─────────────────────────────┘          │
 //       │                                            │      ┌──┴──────────┐                                                                                                                       │
@@ -195,7 +283,13 @@
         else if constexpr (IsRGBGammaEncodedType<Output>)
             return convertColor<Output>(convertColor<typename Output::LinearCounterpart>(color));
 
-        // 5. At this point, Input and Output are each either Linear-RGB types (of different familes) or XYZA
+        // 5. Handle any bounds conversions for the Input and Output.
+        else if constexpr (IsRGBBoundedType<Input>)
+            return convertColor<Output>(convertColor<typename Input::ExtendedCounterpart>(color));
+        else if constexpr (IsRGBBoundedType<Output>)
+            return convertColor<Output>(convertColor<typename Output::ExtendedCounterpart>(color));
+
+        // 6. At this point, Input and Output are each either ExtendedLinear-RGB types (of different familes) or XYZA
         //    and therefore all additional conversion can happen via matrix transformation.
         else
             return handleMatrixConversion(color);
@@ -243,7 +337,7 @@
 
     template<typename ColorType> static inline constexpr auto toBounded(const ColorType& color) -> typename ColorType::BoundedCounterpart
     {
-        return makeFromComponentsClampingExceptAlpha<typename ColorType::BoundedCounterpart>(asColorComponents(color.resolved()));
+        return CSSGamutMapping::mapToBoundedGamut(color);
     }
 
     static inline constexpr Output handleRGBFamilyConversion(const Input& color)
@@ -251,12 +345,17 @@
         static_assert(IsSameRGBTypeFamily<Output, Input>);
 
         //  RGB Family────────────────────┐
+        //  │           ┌────────────────┐│
+        //  │     ┌─────▶︎ LinearExtended ││
+        //  │     │     └────────▲───────┘│
+        //  │     │              │        │
+        //  │┌────────┐          │        │
+        //  ││ Linear │          │        │
+        //  │└────▲───┘          │        │
+        //  │     │              │        │
         //  │┌────────┐ ┌────────────────┐│
-        //  ││ Linear │ │ LinearExtended ││
+        //  ││ Gamma  │─│ GammaExtended  ││
         //  │└────────┘ └────────────────┘│
-        //  │┌────────┐ ┌────────────────┐│
-        //  ││ Gamma  │ │ GammaExtended  ││
-        //  │└────────┘ └────────────────┘│
         //  └─────────────────────────────┘
 
         // This handles conversions between any two of these within the same family, so SRGBLinear -> SRGB, but not
@@ -285,8 +384,8 @@
 
     static inline constexpr Output handleMatrixConversion(const Input& color)
     {
-        static_assert(IsRGBLinearEncodedType<Input> || IsXYZA<Input>);
-        static_assert(IsRGBLinearEncodedType<Output> || IsXYZA<Output>);
+        static_assert((IsRGBLinearEncodedType<Input> && IsRGBExtendedType<Input>) || IsXYZA<Input>);
+        static_assert((IsRGBLinearEncodedType<Output> && IsRGBExtendedType<Output>) || IsXYZA<Output>);
 
         //  ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
         //   Matrix Conversions    ┌───────────┐│┌───────────┐
@@ -298,12 +397,12 @@
         //                   │                  │                  │                               │                               │                               │
         //  │                │                  │                  │                               │                               │                               │                │
         //    ProPhotoRGB───────────────────┐   │   SRGB──────────────────────────┐ DisplayP3─────────────────────┐ A98RGB────────────────────────┐ Rec2020───────────────────────┐
-        //  │ │┌────────┐ ┌────────────────┐│   │   │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│ │┌────────┐ ┌────────────────┐│ │
-        //    ││ Linear │ │ LinearExtended ││   │   ││ Linear │ │ LinearExtended ││ ││ Linear │ │ LinearExtended ││ ││ Linear │ │ LinearExtended ││ ││ Linear │ │ LinearExtended ││
-        //  │ │└────────┘ └────────────────┘│   │   │└────────┘ └────────────────┘│ │└────────┘ └────────────────┘│ │└────────┘ └────────────────┘│ │└────────┘ └────────────────┘│ │
-        //   ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─│─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─
+        //  │ │           ┌────────────────┐│   │   │           ┌────────────────┐│ │           ┌────────────────┐│ │           ┌────────────────┐│ │           ┌────────────────┐│ │
+        //    │     ┌─────▶︎ LinearExtended ││   │   │     ┌─────▶︎ LinearExtended ││ │     ┌─────▶︎ LinearExtended ││ │     ┌─────▶︎ LinearExtended ││ │     ┌─────▶︎ LinearExtended ││
+        //  │ │     │     └────────▲───────┘│   │   │     │     └────────▲───────┘│ │     │     └────────▲───────┘│ │     │     └────────▲───────┘│ │     │     └────────▲───────┘│ │
+        //   ─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─ ─│─ ─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─│─ ─ ─│─ ─ ─ ─ ─ ─ ─ ┼ ─ ─ ─ ─│─
 
-        // This handles conversions between linear color types that can be converted using pre-defined
+        // This handles conversions between extended linear color types that can be converted using pre-defined
         // 3x3 matrices.
         
         // FIXME: Pre-compute (using constexpr) the concatenation of the matrices prior to applying them
@@ -312,7 +411,7 @@
         // have sufficient testing coverage to notice any adverse effects.
 
         auto applyMatrices = [](const Input& color, auto... matrices) {
-            return makeFromComponentsClampingExceptAlpha<Output>(applyMatricesToColorComponents(asColorComponents(color.resolved()), matrices...));
+            return makeFromComponents<Output>(applyMatricesToColorComponents(asColorComponents(color.resolved()), matrices...));
         };
 
         if constexpr (Input::whitePoint == Output::whitePoint) {

Modified: trunk/Source/WebCore/platform/graphics/ColorTypes.h (289395 => 289396)


--- trunk/Source/WebCore/platform/graphics/ColorTypes.h	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/Source/WebCore/platform/graphics/ColorTypes.h	2022-02-08 18:57:30 UTC (rev 289396)
@@ -29,6 +29,7 @@
 #include "ColorMatrix.h"
 #include "ColorModels.h"
 #include "ColorTransferFunctions.h"
+#include <optional>
 
 namespace WebCore {
 
@@ -311,6 +312,47 @@
 template<typename ColorType1, typename ColorType2> inline constexpr bool IsSameRGBTypeFamilyValue<ColorType1, ColorType2, true> = std::is_same_v<typename ColorType1::Descriptor, typename ColorType2::Descriptor>;
 template<typename ColorType1, typename ColorType2> inline constexpr bool IsSameRGBTypeFamily = IsSameRGBTypeFamilyValue<ColorType1, ColorType2, IsRGBType<ColorType1> && IsRGBType<ColorType2>>;
 
+template<typename BoundedColorType> constexpr bool inGamut(typename BoundedColorType::ComponentType component)
+{
+    static_assert(IsRGBBoundedType<BoundedColorType>);
+
+    return component >= 0.0f && component <= 1.0f;
+}
+
+template<typename BoundedColorType> constexpr bool inGamut(ColorComponents<typename BoundedColorType::ComponentType, 4> components)
+{
+    static_assert(IsRGBBoundedType<BoundedColorType>);
+
+    return inGamut<BoundedColorType>(components[0]) && inGamut<BoundedColorType>(components[1]) && inGamut<BoundedColorType>(components[2]);
+}
+
+template<typename BoundedColorType, typename ColorType> constexpr bool inGamut(ColorType color)
+{
+    static_assert(IsRGBBoundedType<BoundedColorType>);
+    static_assert(std::is_same_v<BoundedColorType, typename ColorType::BoundedCounterpart>);
+
+    return inGamut<BoundedColorType>(asColorComponents(color.resolved()));
+}
+
+template<typename BoundedColorType, typename ColorType> constexpr std::optional<BoundedColorType> colorIfInGamut(ColorType color)
+{
+    static_assert(IsRGBBoundedType<BoundedColorType>);
+    static_assert(std::is_same_v<BoundedColorType, typename ColorType::BoundedCounterpart>);
+
+    auto components = asColorComponents(color.resolved());
+    if (!inGamut<BoundedColorType>(components))
+        return std::nullopt;
+    return makeFromComponents<BoundedColorType>(components);
+}
+
+template<typename BoundedColorType, typename ColorType> constexpr BoundedColorType clipToGamut(ColorType color)
+{
+    static_assert(IsRGBBoundedType<BoundedColorType>);
+    static_assert(std::is_same_v<BoundedColorType, typename ColorType::BoundedCounterpart>);
+
+    return makeFromComponentsClampingExceptAlpha<BoundedColorType>(asColorComponents(color.resolved()));
+}
+
 struct SRGBADescriptor {
     template<typename T, TransferFunctionMode Mode> using TransferFunction = SRGBTransferFunction<T, Mode>;
     static constexpr auto whitePoint = WhitePoint::D65;

Modified: trunk/Tools/ChangeLog (289395 => 289396)


--- trunk/Tools/ChangeLog	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/Tools/ChangeLog	2022-02-08 18:57:30 UTC (rev 289396)
@@ -1,3 +1,15 @@
+2022-02-08  Sam Weinig  <wei...@apple.com>
+
+        Conversion to a color space with a smaller gamut should perform gamut mapping
+        https://bugs.webkit.org/show_bug.cgi?id=236200
+
+        Reviewed by Darin Adler.
+
+        * TestWebKitAPI/Tests/WebCore/ColorTests.cpp:
+        (TestWebKitAPI::TEST):
+        Update color conversion tests to account for gamut mapping and add a new test
+        that bypasses gamut mapping by converting to extended sRGB.
+
 2022-02-01  Jonathan Bedard  <jbed...@apple.com>
 
         [git-webkit] Autostash when pulling

Modified: trunk/Tools/TestWebKitAPI/Tests/WebCore/ColorTests.cpp (289395 => 289396)


--- trunk/Tools/TestWebKitAPI/Tests/WebCore/ColorTests.cpp	2022-02-08 18:55:54 UTC (rev 289395)
+++ trunk/Tools/TestWebKitAPI/Tests/WebCore/ColorTests.cpp	2022-02-08 18:57:30 UTC (rev 289396)
@@ -372,6 +372,16 @@
     Color p3Color { DisplayP3<float> { 1.0, 0.5, 0.25, 0.75 } };
     auto sRGBAColor = p3Color.toColorTypeLossy<SRGBA<float>>().resolved();
     EXPECT_FLOAT_EQ(sRGBAColor.red, 1.0f);
+    EXPECT_FLOAT_EQ(sRGBAColor.green, 0.50120097f);
+    EXPECT_FLOAT_EQ(sRGBAColor.blue, 0.26161569f);
+    EXPECT_FLOAT_EQ(sRGBAColor.alpha, 0.75f);
+}
+
+TEST(Color, P3ConversionToExtendedSRGB)
+{
+    Color p3Color { DisplayP3<float> { 1.0, 0.5, 0.25, 0.75 } };
+    auto sRGBAColor = p3Color.toColorTypeLossy<ExtendedSRGBA<float>>().resolved();
+    EXPECT_FLOAT_EQ(sRGBAColor.red, 1.0740442f);
     EXPECT_FLOAT_EQ(sRGBAColor.green, 0.46253282f);
     EXPECT_FLOAT_EQ(sRGBAColor.blue, 0.14912748f);
     EXPECT_FLOAT_EQ(sRGBAColor.alpha, 0.75f);
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to