LibWeb: Implement the color-interpolation property for SVG gradients

This changes the operating color space for gradient `<linearGradient>`
and `<radialGradient>` elements.
This commit is contained in:
Tim Ledbetter 2025-08-15 23:53:15 +01:00 committed by Jelle Raaijmakers
commit ad06ac0d58
Notes: github-actions[bot] 2025-08-17 08:52:13 +00:00
29 changed files with 192 additions and 13 deletions

View file

@ -0,0 +1,16 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
namespace Gfx {
enum class InterpolationColorSpace {
LinearRGB,
SRGB,
};
}

View file

@ -232,6 +232,12 @@ Color ComputedProperties::color_or_fallback(PropertyID id, ColorResolutionContex
return value.to_color(color_resolution_context).value();
}
ColorInterpolation ComputedProperties::color_interpolation() const
{
auto const& value = property(PropertyID::ColorInterpolation);
return keyword_to_color_interpolation(value.to_keyword()).value_or(CSS::ColorInterpolation::Auto);
}
// https://drafts.csswg.org/css-color-adjust-1/#determine-the-used-color-scheme
PreferredColorScheme ComputedProperties::color_scheme(PreferredColorScheme preferred_scheme, Optional<Vector<String> const&> document_supported_schemes) const
{

View file

@ -78,6 +78,7 @@ public:
Optional<LengthPercentage> length_percentage(PropertyID) const;
LengthBox length_box(PropertyID left_id, PropertyID top_id, PropertyID right_id, PropertyID bottom_id, Length const& default_value) const;
Color color_or_fallback(PropertyID, ColorResolutionContext, Color fallback) const;
ColorInterpolation color_interpolation() const;
PreferredColorScheme color_scheme(PreferredColorScheme, Optional<Vector<String> const&> document_supported_schemes) const;
TextAnchor text_anchor() const;
TextAlign text_align() const;

View file

@ -104,6 +104,7 @@ public:
static Color caret_color() { return Color::Black; }
static CSS::Clear clear() { return CSS::Clear::None; }
static CSS::Clip clip() { return CSS::Clip::make_auto(); }
static CSS::ColorInterpolation color_interpolation() { return CSS::ColorInterpolation::Auto; }
static CSS::PreferredColorScheme color_scheme() { return CSS::PreferredColorScheme::Auto; }
static CSS::ContentVisibility content_visibility() { return CSS::ContentVisibility::Visible; }
static CursorData cursor() { return { CSS::CursorPredefined::Auto }; }
@ -428,6 +429,7 @@ public:
Color caret_color() const { return m_inherited.caret_color; }
CSS::Clear clear() const { return m_noninherited.clear; }
CSS::Clip clip() const { return m_noninherited.clip; }
CSS::ColorInterpolation color_interpolation() const { return m_inherited.color_interpolation; }
CSS::PreferredColorScheme color_scheme() const { return m_inherited.color_scheme; }
CSS::ContentVisibility content_visibility() const { return m_inherited.content_visibility; }
Vector<CursorData> const& cursor() const { return m_inherited.cursor; }
@ -638,6 +640,7 @@ protected:
CSS::Length border_spacing_vertical { InitialValues::border_spacing() };
CSS::CaptionSide caption_side { InitialValues::caption_side() };
Color color { InitialValues::color() };
CSS::ColorInterpolation color_interpolation { InitialValues::color_interpolation() };
CSS::PreferredColorScheme color_scheme { InitialValues::color_scheme() };
Optional<Color> accent_color {};
Color webkit_text_fill_color { InitialValues::color() };
@ -837,6 +840,7 @@ public:
void set_border_spacing_vertical(CSS::Length border_spacing_vertical) { m_inherited.border_spacing_vertical = border_spacing_vertical; }
void set_caption_side(CSS::CaptionSide caption_side) { m_inherited.caption_side = caption_side; }
void set_color(Color color) { m_inherited.color = color; }
void set_color_interpolation(CSS::ColorInterpolation color_interpolation) { m_inherited.color_interpolation = color_interpolation; }
void set_color_scheme(CSS::PreferredColorScheme color_scheme) { m_inherited.color_scheme = color_scheme; }
void set_clip(CSS::Clip const& clip) { m_noninherited.clip = clip; }
void set_content(ContentData const& content) { m_noninherited.content = content; }

View file

@ -132,6 +132,11 @@
"inline-start",
"inline-end"
],
"color-interpolation": [
"auto",
"linearrgb",
"srgb"
],
"column-span": [
"none",
"all"

View file

@ -301,6 +301,7 @@
"lighter",
"line-through",
"linear",
"linearrgb",
"lining-nums",
"linktext",
"list-item",

View file

@ -1229,6 +1229,15 @@
"hashless-hex-color"
]
},
"color-interpolation": {
"affects-layout": false,
"animation-type": "discrete",
"inherited": true,
"initial": "srgb",
"valid-types": [
"color-interpolation"
]
},
"color-scheme": {
"affects-layout": false,
"animation-type": "discrete",

View file

@ -1017,6 +1017,7 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
computed_values.set_contain(computed_style.contain());
computed_values.set_caret_color(computed_style.caret_color(*this));
computed_values.set_color_interpolation(computed_style.color_interpolation());
propagate_style_to_anonymous_wrappers();

View file

@ -633,6 +633,9 @@ static SkPaint paint_style_to_skia_paint(Painting::SVGGradientPaintStyle const&
shader = SkGradientShader::MakeTwoPointConical(start_center, start_radius, end_center, end_radius, colors.data(), positions.data(), color_stops.size(), tile_mode, 0, &matrix);
}
paint.setShader(shader);
if (auto* gradient_paint_style = as_if<SVGGradientPaintStyle>(paint_style); gradient_paint_style->color_space() == Gfx::InterpolationColorSpace::LinearRGB) {
paint.setColorFilter(SkColorFilters::LinearToSRGBGamma());
}
return paint;
}

View file

@ -7,6 +7,7 @@
#pragma once
#include <AK/AtomicRefCounted.h>
#include <LibGfx/InterpolationColorSpace.h>
#include <LibGfx/PaintStyle.h>
namespace Web::Painting {
@ -46,6 +47,9 @@ public:
ReadonlySpan<ColorStop> color_stops() const { return m_color_stops; }
Optional<float> repeat_length() const { return m_repeat_length; }
Gfx::InterpolationColorSpace color_space() const { return m_color_space; }
void set_color_space(Gfx::InterpolationColorSpace color_space) { m_color_space = color_space; }
virtual ~SVGGradientPaintStyle() { }
protected:
@ -54,6 +58,7 @@ protected:
Optional<Gfx::AffineTransform> m_gradient_transform {};
SpreadMethod m_spread_method { SpreadMethod::Pad };
Gfx::InterpolationColorSpace m_color_space { Gfx::InterpolationColorSpace::SRGB };
};
class SVGLinearGradientPaintStyle final : public SVGGradientPaintStyle {

View file

@ -54,6 +54,7 @@ static ReadonlySpan<NamedPropertyID> attribute_style_properties()
NamedPropertyID(CSS::PropertyID::ClipPath),
NamedPropertyID(CSS::PropertyID::ClipRule),
NamedPropertyID(CSS::PropertyID::Color),
NamedPropertyID(CSS::PropertyID::ColorInterpolation),
NamedPropertyID(CSS::PropertyID::Cursor),
NamedPropertyID(CSS::PropertyID::Cx, { SVG::TagNames::circle, SVG::TagNames::ellipse }),
NamedPropertyID(CSS::PropertyID::Cy, { SVG::TagNames::circle, SVG::TagNames::ellipse }),

View file

@ -66,6 +66,19 @@ SpreadMethod SVGGradientElement::spread_method_impl(HashTable<SVGGradientElement
return SpreadMethod::Pad;
}
Gfx::InterpolationColorSpace SVGGradientElement::color_space() const
{
switch (computed_properties()->color_interpolation()) {
case CSS::ColorInterpolation::Linearrgb:
return Gfx::InterpolationColorSpace::LinearRGB;
case CSS::ColorInterpolation::Auto:
case CSS::ColorInterpolation::Srgb:
return Gfx::InterpolationColorSpace::SRGB;
}
VERIFY_NOT_REACHED();
}
Optional<Gfx::AffineTransform> SVGGradientElement::gradient_transform() const
{
HashTable<SVGGradientElement const*> seen_gradients;

View file

@ -52,6 +52,8 @@ public:
SpreadMethod spread_method() const;
Gfx::InterpolationColorSpace color_space() const;
Optional<Gfx::AffineTransform> gradient_transform() const;
protected:

View file

@ -156,6 +156,7 @@ Optional<Painting::PaintStyle> SVGLinearGradientElement::to_gfx_paint_style(SVGP
m_paint_style->set_gradient_transform(gradient_paint_transform(paint_context));
m_paint_style->set_spread_method(to_painting_spread_method(spread_method()));
m_paint_style->set_color_space(color_space());
return *m_paint_style;
}

View file

@ -212,6 +212,7 @@ Optional<Painting::PaintStyle> SVGRadialGradientElement::to_gfx_paint_style(SVGP
}
m_paint_style->set_gradient_transform(gradient_paint_transform(paint_context));
m_paint_style->set_spread_method(to_painting_spread_method(spread_method()));
m_paint_style->set_color_space(color_space());
return *m_paint_style;
}

View file

@ -0,0 +1,24 @@
<svg width="100%" height="100%"
xmlns="http://www.w3.org/2000/svg"
xmlns:html="http://www.w3.org/1999/xhtml">
<style>
div {
border: none;
margin: 0;
padding: 0;
};
</style>
<defs>
<linearGradient id="gradientLinearRGB" gradientUnits="objectBoundingBox" color-interpolation="linearRGB">
<stop offset="0" stop-color="white"/>
<stop offset=".33" stop-color="blue"/>
<stop offset=".66" stop-color="red"/>
<stop offset="1" stop-color="yellow"/>
</linearGradient>
</defs>
<foreignObject x="20" y="20" width="200" height="200">
<html:div style="height:100%;width:100%;background: linear-gradient(90deg in srgb-linear, white 0%, blue 33%, red 66%, yellow 100%);"/>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View file

@ -0,0 +1,22 @@
<svg width="100%" height="100%"
xmlns="http://www.w3.org/2000/svg"
xmlns:html="http://www.w3.org/1999/xhtml">
<g id="testmeta">
<title>Gradient with color-interpolation: linearRGB</title>
<html:link rel="help"
href="https://www.w3.org/TR/SVG2/pservers.html#LinearGradients"/>
<html:link rel="match" href="../../../../../expected/wpt-import/svg/pservers/reftests/reference/gradient-color-interpolation-ref.svg" />
<html:meta name="fuzzy" content="maxDifference=0-20;totalPixels=0-29400" />
</g>
<defs>
<linearGradient id="gradientLinearRGB" gradientUnits="objectBoundingBox" color-interpolation="linearRGB">
<stop offset="0" stop-color="white"/>
<stop offset=".33" stop-color="blue"/>
<stop offset=".66" stop-color="red"/>
<stop offset="1" stop-color="yellow"/>
</linearGradient>
</defs>
<rect x="20" y="20" width="200" height="200" style="fill:url(#gradientLinearRGB)" />
</svg>

After

Width:  |  Height:  |  Size: 953 B

View file

@ -8,6 +8,7 @@ All properties associated with getComputedStyle(document.body):
"caret-color",
"clip-rule",
"color",
"color-interpolation",
"color-scheme",
"cursor",
"direction",

View file

@ -340,6 +340,8 @@ All supported properties and their default values exposed from CSSStylePropertie
'clipRule': 'nonzero'
'clip-rule': 'nonzero'
'color': 'rgb(0, 0, 0)'
'colorInterpolation': 'srgb'
'color-interpolation': 'srgb'
'colorScheme': 'normal'
'color-scheme': 'normal'
'columnCount': 'auto'

View file

@ -6,6 +6,7 @@ caption-side: top
caret-color: rgb(0, 0, 0)
clip-rule: nonzero
color: rgb(0, 0, 0)
color-interpolation: srgb
color-scheme: normal
cursor: auto
direction: ltr
@ -89,7 +90,7 @@ background-position-x: 0%
background-position-y: 0%
background-repeat: repeat
background-size: auto
block-size: 1365px
block-size: 1380px
border-block-end-color: rgb(0, 0, 0)
border-block-end-style: none
border-block-end-width: 0px
@ -165,7 +166,7 @@ grid-row-start: auto
grid-template-areas: none
grid-template-columns: none
grid-template-rows: none
height: 2505px
height: 2520px
inline-size: 784px
inset-block-end: auto
inset-block-start: auto

View file

@ -1,8 +1,8 @@
Harness status: OK
Found 254 tests
Found 255 tests
247 Pass
248 Pass
7 Fail
Pass accent-color
Pass border-collapse
@ -11,6 +11,7 @@ Pass caption-side
Pass caret-color
Pass clip-rule
Pass color
Pass color-interpolation
Pass color-scheme
Pass cursor
Pass direction

View file

@ -2,15 +2,15 @@ Harness status: OK
Found 20 tests
15 Pass
5 Fail
14 Pass
6 Fail
Pass The serialization of border: 1px; border-top: 1px; should be canonical.
Pass The serialization of border: 1px solid red; should be canonical.
Pass The serialization of border: 1px red; should be canonical.
Pass The serialization of border: red; should be canonical.
Fail The serialization of border-top: 1px; border-right: 1px; border-bottom: 1px; border-left: 1px; border-image: none; should be canonical.
Fail The serialization of border-top: 1px; border-right: 1px; border-bottom: 1px; border-left: 1px; should be canonical.
Pass The serialization of border-top: 1px; border-right: 2px; border-bottom: 3px; border-left: 4px; should be canonical.
Fail The serialization of border-top: 1px; border-right: 2px; border-bottom: 3px; border-left: 4px; should be canonical.
Fail The serialization of border: 1px; border-top: 2px; should be canonical.
Fail The serialization of border: 1px; border-top: 1px !important; should be canonical.
Fail The serialization of border: 1px; border-top-color: red; should be canonical.

View file

@ -0,0 +1,7 @@
Harness status: OK
Found 2 tests
2 Pass
Pass e.style['color-interpolation'] = "none" should not set the property value
Pass e.style['color-interpolation'] = "auto srgb" should not set the property value

View file

@ -0,0 +1,8 @@
Harness status: OK
Found 3 tests
3 Pass
Pass e.style['color-interpolation'] = "auto" should set the property value
Pass e.style['color-interpolation'] = "srgb" should set the property value
Pass e.style['color-interpolation'] = "linearrgb" should set the property value

View file

@ -1,12 +1,13 @@
Harness status: OK
Found 45 tests
Found 46 tests
42 Pass
43 Pass
3 Fail
Pass clip-path presentation attribute supported on an irrelevant element
Pass clip-rule presentation attribute supported on an irrelevant element
Pass color presentation attribute supported on an irrelevant element
Pass color-interpolation presentation attribute supported on an irrelevant element
Pass cursor presentation attribute supported on an irrelevant element
Pass direction presentation attribute supported on an irrelevant element
Pass display presentation attribute supported on an irrelevant element

View file

@ -1,12 +1,13 @@
Harness status: OK
Found 55 tests
Found 56 tests
51 Pass
52 Pass
4 Fail
Pass clip-path presentation attribute supported on a relevant element
Pass clip-rule presentation attribute supported on a relevant element
Pass color presentation attribute supported on a relevant element
Pass color-interpolation presentation attribute supported on a relevant element
Pass cursor presentation attribute supported on a relevant element
Pass cx presentation attribute supported on a relevant element
Pass cy presentation attribute supported on a relevant element

View file

@ -1,12 +1,13 @@
Harness status: OK
Found 45 tests
Found 46 tests
42 Pass
43 Pass
3 Fail
Pass clip-path presentation attribute supported on an unknown SVG element
Pass clip-rule presentation attribute supported on an unknown SVG element
Pass color presentation attribute supported on an unknown SVG element
Pass color-interpolation presentation attribute supported on an unknown SVG element
Pass cursor presentation attribute supported on an unknown SVG element
Pass direction presentation attribute supported on an unknown SVG element
Pass display presentation attribute supported on an unknown SVG element

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:h="http://www.w3.org/1999/xhtml"
width="800px" height="800px">
<title>SVG Painting: parsing color-interpolation with invalid values</title>
<metadata>
<h:link rel="help" href="https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty"/>
<h:meta name="assert" content="color-interpolation supports only the grammar 'auto | sRGB | linearRGB'."/>
</metadata>
<g id="target"></g>
<h:script src="../../../resources/testharness.js"/>
<h:script src="../../../resources/testharnessreport.js"/>
<h:script src="../../../css/support/parsing-testcommon.js"/>
<script><![CDATA[
test_invalid_value("color-interpolation", "none");
test_invalid_value("color-interpolation", "auto srgb");
]]></script>
</svg>

After

Width:  |  Height:  |  Size: 827 B

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:h="http://www.w3.org/1999/xhtml"
width="800px" height="800px">
<title>SVG Painting: parsing color-interpolation with valid values</title>
<metadata>
<h:link rel="help" href="https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty"/>
<h:meta name="assert" content="color-interpolation supports the full grammar 'auto | sRGB | linearRGB'."/>
</metadata>
<g id="target"></g>
<h:script src="../../../resources/testharness.js"/>
<h:script src="../../../resources/testharnessreport.js"/>
<h:script src="../../../css/support/parsing-testcommon.js"/>
<script><![CDATA[
test_valid_value("color-interpolation", "auto");
test_valid_value("color-interpolation", "srgb");
test_valid_value("color-interpolation", "linearrgb");
]]></script>
</svg>

After

Width:  |  Height:  |  Size: 870 B