LibWeb/SVG: Apply SVGFeBlendElement blend mode

Previously, the blend mode was always assumed to be `normal`.
This commit is contained in:
Tim Ledbetter 2025-08-06 11:35:14 +01:00 committed by Jelle Raaijmakers
commit 1dd3608960
Notes: github-actions[bot] 2025-08-06 13:22:18 +00:00
8 changed files with 176 additions and 7 deletions

View file

@ -7,6 +7,7 @@
#include <LibWeb/Bindings/SVGFEBlendElementPrototype.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Painting/Blending.h>
#include <LibWeb/SVG/SVGAnimatedEnumeration.h>
#include <LibWeb/SVG/SVGFEBlendElement.h>
@ -33,6 +34,24 @@ void SVGFEBlendElement::visit_edges(Cell::Visitor& visitor)
visitor.visit(m_in2);
}
void SVGFEBlendElement::attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& new_value, Optional<FlyString> const& namespace_)
{
Base::attribute_changed(name, old_value, new_value, namespace_);
if (name == SVG::AttributeNames::mode) {
auto parse_mix_blend_mode = [](Optional<String> const& value) -> Optional<CSS::MixBlendMode> {
if (!value.has_value())
return {};
auto keyword = CSS::keyword_from_string(*value);
if (!keyword.has_value())
return {};
return CSS::keyword_to_mix_blend_mode(*keyword);
};
m_mode = parse_mix_blend_mode(new_value);
}
}
GC::Ref<SVGAnimatedString> SVGFEBlendElement::in1()
{
if (!m_in1)
@ -49,10 +68,14 @@ GC::Ref<SVGAnimatedString> SVGFEBlendElement::in2()
return *m_in2;
}
GC::Ref<SVGAnimatedEnumeration> SVGFEBlendElement::mode() const
Gfx::CompositingAndBlendingOperator SVGFEBlendElement::mode() const
{
// FIXME: Resolve the actual value from AttributeName::mode.
return SVGAnimatedEnumeration::create(realm(), 1);
return Painting::mix_blend_mode_to_compositing_and_blending_operator(m_mode.value_or(CSS::MixBlendMode::Normal));
}
GC::Ref<SVGAnimatedEnumeration> SVGFEBlendElement::mode_for_bindings() const
{
return SVGAnimatedEnumeration::create(realm(), to_underlying(mode()));
}
}

View file

@ -24,7 +24,9 @@ public:
GC::Ref<SVGAnimatedString> in1();
GC::Ref<SVGAnimatedString> in2();
GC::Ref<SVGAnimatedEnumeration> mode() const;
Gfx::CompositingAndBlendingOperator mode() const;
GC::Ref<SVGAnimatedEnumeration> mode_for_bindings() const;
private:
SVGFEBlendElement(DOM::Document&, DOM::QualifiedName);
@ -32,8 +34,12 @@ private:
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& new_value, Optional<FlyString> const& namespace_) override;
GC::Ptr<SVGAnimatedString> m_in1;
GC::Ptr<SVGAnimatedString> m_in2;
Optional<CSS::MixBlendMode> m_mode;
};
}

View file

@ -27,7 +27,7 @@ interface SVGFEBlendElement : SVGElement {
readonly attribute SVGAnimatedString in1;
readonly attribute SVGAnimatedString in2;
readonly attribute SVGAnimatedEnumeration mode;
[ImplementedAs=mode_for_bindings] readonly attribute SVGAnimatedEnumeration mode;
};
SVGFEBlendElement includes SVGFilterPrimitiveStandardAttributes;

View file

@ -118,8 +118,7 @@ Optional<Gfx::Filter> SVGFilterElement::gfx_filter()
auto foreground = resolve_input_filter(blend_primitive->in1()->base_val());
auto background = resolve_input_filter(blend_primitive->in2()->base_val());
// FIXME: Actually resolve the blend mode
auto blend_mode = Gfx::CompositingAndBlendingOperator::Normal;
auto blend_mode = blend_primitive.mode();
root_filter = Gfx::Filter::blend(background, foreground, blend_mode);
update_result_map(*blend_primitive);

View file

@ -0,0 +1,5 @@
<!DOCTYPE html>
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="100" height="100" fill="green"/>
<circle cx="50" cy="50" r="50" fill="black" />
</svg>

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<link rel="match" href="../../expected/svg/blend-filter-ref.html" />
<meta name="fuzzy" content="0-1;0-35">
<svg width="100" height="100" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="blendFilter">
<feFlood flood-color="green" />
<feBlend in2="SourceGraphic" mode="darken" result="blended" />
</filter>
</defs>
<rect x="0" y="0" width="100" height="100" fill="blue" />
<circle cx="50" cy="50" r="50" fill="red" filter="url(#blendFilter)" />
</svg>

View file

@ -0,0 +1,41 @@
Harness status: OK
Found 35 tests
18 Pass
17 Fail
Pass SVGFEBlendElement.prototype.mode, getter, initial value
Pass SVGFEBlendElement.prototype.mode, getter, invalid value
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "normal"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "multiply"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "screen"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "darken"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "lighten"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "overlay"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "color-dodge"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "color-burn"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "hard-light"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "soft-light"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "difference"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "exclusion"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "hue"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "saturation"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "color"
Pass SVGFEBlendElement.prototype.mode, getter, numeric value for "luminosity"
Fail SVGFEBlendElement.prototype.mode, setter, invalid value
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "normal"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "multiply"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "screen"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "darken"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "lighten"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "overlay"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "color-dodge"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "color-burn"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "hard-light"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "soft-light"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "difference"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "exclusion"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "hue"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "saturation"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "color"
Fail SVGFEBlendElement.prototype.mode, setter, numeric value for "luminosity"

View file

@ -0,0 +1,82 @@
<!DOCTYPE HTML>
<title>SVGFEBlendElement.prototype.mode</title>
<link rel="help" href="https://drafts.fxtf.org/filter-effects/#InterfaceSVGFEBlendElement">
<link rel="help" href="https://svgwg.org/svg2-draft/types.html#InterfaceSVGAnimatedEnumeration">
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script>
// This test checks the use of SVGAnimatedEnumeration for 'mode' on SVGFEBlendElement.
const svgNs = "http://www.w3.org/2000/svg";
test(function() {
var element = document.createElementNS(svgNs, "feBlend");
assert_equals(element.mode.baseVal, SVGFEBlendElement.SVG_FEBLEND_MODE_NORMAL);
assert_false(element.hasAttribute("mode"));
}, document.title + ", getter, initial value");
test(function() {
var element = document.createElementNS(svgNs, "feBlend");
element.setAttribute("mode", "not-a-valid-value");
assert_equals(element.mode.baseVal, SVGFEBlendElement.SVG_FEBLEND_MODE_NORMAL);
assert_true(element.hasAttribute("mode"));
assert_equals(element.getAttribute("mode"), "not-a-valid-value");
}, document.title + ", getter, invalid value");
const enumerationMap = [
[ "normal", "NORMAL" ],
[ "multiply", "MULTIPLY" ],
[ "screen", "SCREEN" ],
[ "darken", "DARKEN" ],
[ "lighten", "LIGHTEN" ],
[ "overlay", "OVERLAY" ],
[ "color-dodge", "COLOR_DODGE" ],
[ "color-burn", "COLOR_BURN" ],
[ "hard-light", "HARD_LIGHT" ],
[ "soft-light", "SOFT_LIGHT" ],
[ "difference", "DIFFERENCE" ],
[ "exclusion", "EXCLUSION" ],
[ "hue", "HUE" ],
[ "saturation", "SATURATION" ],
[ "color", "COLOR" ],
[ "luminosity", "LUMINOSITY" ],
];
for (let [enumeration, value_suffix] of enumerationMap) {
test(function() {
let value = SVGFEBlendElement["SVG_FEBLEND_MODE_" + value_suffix];
var element = document.createElementNS(svgNs, "feBlend");
element.setAttribute("mode", enumeration);
assert_equals(element.mode.baseVal, value);
}, document.title + ", getter, numeric value for \"" + enumeration + "\"");
}
test(function() {
var element = document.createElementNS(svgNs, "feBlend");
element.setAttribute("mode", "lighten");
assert_equals(element.mode.baseVal, SVGFEBlendElement.SVG_FEBLEND_MODE_LIGHTEN);
assert_equals(element.getAttribute('mode'), "lighten");
assert_throws_js(TypeError, function() { element.mode.baseVal = 17; });
assert_equals(element.mode.baseVal, SVGFEBlendElement.SVG_FEBLEND_MODE_LIGHTEN);
assert_equals(element.getAttribute('mode'), "lighten");
assert_throws_js(TypeError, function() { element.mode.baseVal = -1; });
assert_equals(element.mode.baseVal, SVGFEBlendElement.SVG_FEBLEND_MODE_LIGHTEN);
assert_equals(element.getAttribute('mode'), "lighten");
assert_throws_js(TypeError, function() { element.mode.baseVal = 0; });
assert_equals(element.mode.baseVal, SVGFEBlendElement.SVG_FEBLEND_MODE_LIGHTEN);
assert_equals(element.getAttribute('mode'), "lighten");
}, document.title + ", setter, invalid value");
for (let [enumeration, value_suffix] of enumerationMap) {
test(function() {
let value = SVGFEBlendElement["SVG_FEBLEND_MODE_" + value_suffix];
var element = document.createElementNS(svgNs, "feBlend");
element.mode.baseVal = value;
assert_equals(element.mode.baseVal, value);
assert_equals(element.getAttribute('mode'), enumeration);
}, document.title + ", setter, numeric value for \"" + enumeration + "\"");
}
</script>