[csswg-drafts] [css-colors-4][css-colors-5] rgb-to-hwb algorithm disagrees indirectly with relative-color-out-of-gamut and color-mix-out-of-gamut expectations (#10695)

squelart has just created a new issue for https://github.com/w3c/csswg-drafts:

== [css-colors-4][css-colors-5] rgb-to-hwb algorithm disagrees indirectly with relative-color-out-of-gamut and color-mix-out-of-gamut expectations ==
_Bias warning: I've made the title as neutral as possible, but I personally think the specs are at fault and the tests are correct (though additional tests would be welcome).
Also I'm not an expert in colorspaces and CSS and specs, so my bias may be wrong; also I may use incorrect terminology below._

tl;dr: The [rgb-to-hwb algorithm](https://drafts.csswg.org/css-color-4/#rgb-to-hwb) uses [rgb-to-hsl](https://drafts.csswg.org/css-color-4/#rgb-to-hsl) as-is, which produces a rotated hue for some out-of-gamut colors with negative saturation, while some [relative-color tests](https://github.com/web-platform-tests/wpt/blob/master/css/css-color/parsing/relative-color-out-of-gamut.html) like `hwb(from lab(100 104.3 -50.9) h w b)` (if they internally convert via rgb) expect non-rotated hue.
I will argue that the function definition in https://drafts.csswg.org/css-color-4/#rgb-to-hwb needs to be tweaked.

First, about the "indirect" tests: Safari fails tests like `hwb(from lab(100 104.3 -50.9) h w b)` because internally it converts from lab to rgb first, before using the rgb-to-hwb algorithm to compute the final hwb color.
However, I think this internal implementation converting via rgb is not at issue here, because we could add another simple test case that goes directly from rgb to hwb, e.g.:
```
  fuzzy_test_computed_color(
    `hwb(from color(srgb 1.59343 0.58802 1.40564) h w b / alpha)`,
    `color(srgb 1.59343 0.58802 1.40564)`);
```
This also fails in Safari with an actual output `color(srgb 0.587758 1.593503 0.775713)`.

Next, about the test  expectations.
Focusing on this relative-color-out-of-gamut.html test case:
```
  fuzzy_test_computed_color(
    `hwb(from lab(100 104.3 -50.9) h w b)`,
    `color(srgb 1.59343 0.58802 1.40564)`);
```
I believe the expectation from lab to srgb is correct, because I can reproduce it in a number of ways:
- Using the macOS ColorSync Utility. ![colorsync-lab-to-rgb](https://github.com/user-attachments/assets/350656fe-7982-47e8-8d37-6fe43f1077f4)
- On https://www.mathworks.com/help/images/ref/lab2rgb.html
- On https://www.colorspaceconverter.com/converter/lab-to-rgb , https://colordesigner.io/convert/labtorgb , https://www.colormine.org/convert/lab-to-rgb#google_vignette , https://products.aspose.app/svg/color-converter/lab-to-rgb#google_vignette (all clipped, but tending towards the expected extra-red half-green extra-blue color.)

Assuming the test expectations are correct, I'll now focus on the rgb-to-hwb conversion:
https://drafts.csswg.org/css-color-4/#rgb-to-hwb defines the `rgbToHwb` function, and its first step is to call `rgbToHsl`.
https://drafts.csswg.org/css-color-4/#rgb-to-hsl defines `rgbToHsl`. Just above it there's this note:
> Special care is taken to deal with intermediate negative values of saturation, which can be produced by colors far outside the sRGB gamut.

And in the `rgbToHsl` function definition this is reflected in this bit of code:
```
    // Very out of gamut colors can produce negative saturation
    // If so, just rotate the hue by 180 and use a positive saturation
    // see https://github.com/w3c/csswg-drafts/issues/9222
    if (sat < 0) {
        hue += 180;
        sat = Math.abs(sat);
    }
```
Notice how the hue is rotated 180 degrees, and the saturation is negated. And it probably makes sense to normalize HSL colors that way.

However, back in `rgbToHwb`, only the resulting hue component is kept, the saturation and lightness are simply ignored.
I believe that this is where things go wrong, because `rgbToHsl` could have flipped the hue and negated the saturation, but we only use that flipped hue and have lost the corresponding information stored in the saturation.
And this explains why we get something like rgb(0.6, 1.6, 0.8), which seems to have an opposite hue to the expected rgb(1.6, 0.6, 1.4).

My proposed solution would be to compute the hue in a similar way, except that we wouldn't need to compute the saturation (nor lightness), and the hue would never be rotated/flipped.

To illustrate this, I've written this codepen: https://codepen.io/squelart/pen/MWMoeoB
It uses the function definitions straight from https://drafts.csswg.org/css-color-4 , and tests rgb -> hwb -> rgb.
We can notice how our hero test color doesn't survive the round trip. I've added a few test cases with a decreasing red component, it looks like there's a cut-off between 1.4 and 1.5.

In the codepen I've also added a modified rgb-to-hwb function that implements my suggested rgb-to-hue sub-function (which derives from rgb-to-hsl). And this one shows a working round trip.
(And I'm working on a similar fix in WebKit: https://github.com/WebKit/WebKit/pull/31636 , but I'll wait for this discussion here to be resolved first.)

In conclusion:
- I believe the function definition in https://drafts.csswg.org/css-color-4/#rgb-to-hwb needs to be tweaked to avoid flipping the hue.
- More out-of-gamut test cases (related to css-colors-5) could be useful, to cover more colorspace pairs, and more colors.

To be complete: There's the possibility that my argument is incorrect, and in fact the css-color-4 conversion functions should be taken as authoritative and correct, in which case the test expectations should be updated.

Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/10695 using your GitHub account


-- 
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config

Received on Tuesday, 6 August 2024 01:18:57 UTC