LibWeb: Implement <feComposite> SVG filter

This commit is contained in:
Tim Ledbetter 2025-08-06 13:27:50 +01:00 committed by Tim Ledbetter
parent bbfc3a0f5e
commit a00e7cb20b
Notes: github-actions[bot] 2025-09-30 21:34:29 +00:00
15 changed files with 303 additions and 0 deletions

View file

@ -9,6 +9,7 @@
#include <LibGfx/SkiaUtils.h>
#include <core/SkBlendMode.h>
#include <core/SkColorFilter.h>
#include <core/SkScalar.h>
#include <effects/SkColorMatrix.h>
#include <effects/SkImageFilters.h>
@ -41,6 +42,16 @@ FilterImpl const& Filter::impl() const
return *m_impl;
}
Filter Filter::arithmetic(Optional<Filter const&> background, Optional<Filter const&> foreground, float k1, float k2, float k3, float k4)
{
sk_sp<SkImageFilter> background_skia = background.has_value() ? background->m_impl->filter : nullptr;
sk_sp<SkImageFilter> foreground_skia = foreground.has_value() ? foreground->m_impl->filter : nullptr;
auto filter = SkImageFilters::Arithmetic(
SkFloatToScalar(k1), SkFloatToScalar(k2), SkFloatToScalar(k3), SkFloatToScalar(k4), false, move(background_skia), move(foreground_skia));
return Filter(Impl::create(filter));
}
Filter Filter::compose(Filter const& outer, Filter const& inner)
{
auto inner_skia = inner.m_impl->filter;

View file

@ -34,6 +34,7 @@ public:
~Filter();
static Filter arithmetic(Optional<Filter const&> background, Optional<Filter const&> foreground, float k1, float k2, float k3, float k4);
static Filter compose(Filter const& outer, Filter const& inner);
static Filter blend(Optional<Filter const&> background, Optional<Filter const&> foreground, CompositingAndBlendingOperator mode);
static Filter flood(Gfx::Color color, float opacity);

View file

@ -903,6 +903,7 @@ set(SOURCES
SVG/SVGElement.cpp
SVG/SVGEllipseElement.cpp
SVG/SVGFEBlendElement.cpp
SVG/SVGFECompositeElement.cpp
SVG/SVGFEFloodElement.cpp
SVG/SVGFEGaussianBlurElement.cpp
SVG/SVGFEImageElement.cpp

View file

@ -92,6 +92,7 @@
#include <LibWeb/SVG/SVGDescElement.h>
#include <LibWeb/SVG/SVGEllipseElement.h>
#include <LibWeb/SVG/SVGFEBlendElement.h>
#include <LibWeb/SVG/SVGFECompositeElement.h>
#include <LibWeb/SVG/SVGFEFloodElement.h>
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
#include <LibWeb/SVG/SVGFEImageElement.h>
@ -471,6 +472,8 @@ static GC::Ref<SVG::SVGElement> create_svg_element(JS::Realm& realm, Document& d
return realm.create<SVG::SVGEllipseElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feBlend)
return realm.create<SVG::SVGFEBlendElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feComposite)
return realm.create<SVG::SVGFECompositeElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feFlood)
return realm.create<SVG::SVGFEFloodElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feGaussianBlur)

View file

@ -1087,6 +1087,7 @@ class SVGDescElement;
class SVGElement;
class SVGEllipseElement;
class SVGFEBlendElement;
class SVGFECompositeElement;
class SVGFEFloodElement;
class SVGFEGaussianBlurElement;
class SVGFEImageElement;

View file

@ -39,6 +39,10 @@ namespace Web::SVG::AttributeNames {
__ENUMERATE_SVG_ATTRIBUTE(in2, "in2") \
__ENUMERATE_SVG_ATTRIBUTE(kernelMatrix, "kernelMatrix") \
__ENUMERATE_SVG_ATTRIBUTE(kernelUnitLength, "kernelUnitLength") \
__ENUMERATE_SVG_ATTRIBUTE(k1, "k1") \
__ENUMERATE_SVG_ATTRIBUTE(k2, "k2") \
__ENUMERATE_SVG_ATTRIBUTE(k3, "k3") \
__ENUMERATE_SVG_ATTRIBUTE(k4, "k4") \
__ENUMERATE_SVG_ATTRIBUTE(keyPoints, "keyPoints") \
__ENUMERATE_SVG_ATTRIBUTE(keySplines, "keySplines") \
__ENUMERATE_SVG_ATTRIBUTE(keyTimes, "keyTimes") \
@ -53,6 +57,7 @@ namespace Web::SVG::AttributeNames {
__ENUMERATE_SVG_ATTRIBUTE(numOctaves, "numOctaves") \
__ENUMERATE_SVG_ATTRIBUTE(offset, "offset") \
__ENUMERATE_SVG_ATTRIBUTE(opacity, "opacity") \
__ENUMERATE_SVG_ATTRIBUTE(operator_, "operator") \
__ENUMERATE_SVG_ATTRIBUTE(pathLength, "pathLength") \
__ENUMERATE_SVG_ATTRIBUTE(patternContentUnits, "patternContentUnits") \
__ENUMERATE_SVG_ATTRIBUTE(patternTransform, "patternTransform") \

View file

@ -0,0 +1,133 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/SVGFECompositeElementPrototype.h>
#include <LibWeb/SVG/SVGAnimatedEnumeration.h>
#include <LibWeb/SVG/SVGFECompositeElement.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGFECompositeElement);
SVGFECompositeElement::SVGFECompositeElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: SVGElement(document, qualified_name)
{
}
void SVGFECompositeElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGFECompositeElement);
Base::initialize(realm);
}
void SVGFECompositeElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
SVGFilterPrimitiveStandardAttributes::visit_edges(visitor);
visitor.visit(m_in1);
visitor.visit(m_in2);
visitor.visit(m_k1);
visitor.visit(m_k2);
visitor.visit(m_k3);
visitor.visit(m_k4);
}
static SVGFECompositeElement::CompositingOperator string_to_compositing_operator(StringView string)
{
if (string == "over"sv)
return SVGFECompositeElement::CompositingOperator::Over;
if (string == "in"sv)
return SVGFECompositeElement::CompositingOperator::In;
if (string == "out"sv)
return SVGFECompositeElement::CompositingOperator::Out;
if (string == "atop"sv)
return SVGFECompositeElement::CompositingOperator::Atop;
if (string == "xor"sv)
return SVGFECompositeElement::CompositingOperator::Xor;
if (string == "arithmetic"sv)
return SVGFECompositeElement::CompositingOperator::Arithmetic;
return SVGFECompositeElement::CompositingOperator::Unknown;
}
void SVGFECompositeElement::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::operator_) {
auto parse_compositing_operator = [](Optional<String> const& value) -> Optional<CompositingOperator> {
if (!value.has_value())
return {};
return string_to_compositing_operator(*value);
};
m_operator = parse_compositing_operator(new_value);
}
}
GC::Ref<SVGAnimatedString> SVGFECompositeElement::in1()
{
if (!m_in1)
m_in1 = SVGAnimatedString::create(realm(), *this, AttributeNames::in);
return *m_in1;
}
GC::Ref<SVGAnimatedString> SVGFECompositeElement::in2()
{
if (!m_in2)
m_in2 = SVGAnimatedString::create(realm(), *this, AttributeNames::in2);
return *m_in2;
}
// https://drafts.fxtf.org/filter-effects/#element-attrdef-fecomposite-k1
GC::Ref<SVGAnimatedNumber> SVGFECompositeElement::k1()
{
if (!m_k1)
m_k1 = SVGAnimatedNumber::create(realm(), *this, AttributeNames::k1, 0.f);
return *m_k1;
}
// https://drafts.fxtf.org/filter-effects/#element-attrdef-fecomposite-k2
GC::Ref<SVGAnimatedNumber> SVGFECompositeElement::k2()
{
if (!m_k2)
m_k2 = SVGAnimatedNumber::create(realm(), *this, AttributeNames::k2, 0.f);
return *m_k2;
}
// https://drafts.fxtf.org/filter-effects/#element-attrdef-fecomposite-k3
GC::Ref<SVGAnimatedNumber> SVGFECompositeElement::k3()
{
if (!m_k3)
m_k3 = SVGAnimatedNumber::create(realm(), *this, AttributeNames::k3, 0.f);
return *m_k3;
}
// https://drafts.fxtf.org/filter-effects/#element-attrdef-fecomposite-k4
GC::Ref<SVGAnimatedNumber> SVGFECompositeElement::k4()
{
if (!m_k4)
m_k4 = SVGAnimatedNumber::create(realm(), *this, AttributeNames::k4, 0.f);
return *m_k4;
}
SVGFECompositeElement::CompositingOperator SVGFECompositeElement::operator_() const
{
return m_operator.value_or(CompositingOperator::Over);
}
// https://drafts.fxtf.org/filter-effects/#element-attrdef-fecomposite-operator
GC::Ref<SVGAnimatedEnumeration> SVGFECompositeElement::operator_for_bindings() const
{
return SVGAnimatedEnumeration::create(realm(), to_underlying(m_operator.value_or(CompositingOperator::Over)));
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "SVGAnimatedNumber.h"
#include <LibWeb/SVG/SVGAnimatedLength.h>
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.h>
namespace Web::SVG {
class SVGFECompositeElement final
: public SVGElement
, public SVGFilterPrimitiveStandardAttributes<SVGFECompositeElement> {
WEB_PLATFORM_OBJECT(SVGFECompositeElement, SVGElement);
GC_DECLARE_ALLOCATOR(SVGFECompositeElement);
public:
virtual ~SVGFECompositeElement() override = default;
GC::Ref<SVGAnimatedString> in1();
GC::Ref<SVGAnimatedString> in2();
GC::Ref<SVGAnimatedNumber> k1();
GC::Ref<SVGAnimatedNumber> k2();
GC::Ref<SVGAnimatedNumber> k3();
GC::Ref<SVGAnimatedNumber> k4();
enum class CompositingOperator : u8 {
Unknown = 0,
Over = 1,
In = 2,
Out = 3,
Atop = 4,
Xor = 5,
Arithmetic = 6,
Lighter = 7,
};
SVGFECompositeElement::CompositingOperator operator_() const;
GC::Ref<SVGAnimatedEnumeration> operator_for_bindings() const;
private:
SVGFECompositeElement(DOM::Document&, DOM::QualifiedName);
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;
GC::Ptr<SVGAnimatedNumber> m_k1;
GC::Ptr<SVGAnimatedNumber> m_k2;
GC::Ptr<SVGAnimatedNumber> m_k3;
GC::Ptr<SVGAnimatedNumber> m_k4;
Optional<CompositingOperator> m_operator;
};
}

View file

@ -0,0 +1,26 @@
#import <SVG/SVGAnimatedEnumeration.idl>
#import <SVG/SVGAnimatedString.idl>
#import <SVG/SVGFilterPrimitiveStandardAttributes.idl>
// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFECompositeElement
[Exposed=Window]
interface SVGFECompositeElement : SVGElement {
// Composite Operators
const unsigned short SVG_FECOMPOSITE_OPERATOR_UNKNOWN = 0;
const unsigned short SVG_FECOMPOSITE_OPERATOR_OVER = 1;
const unsigned short SVG_FECOMPOSITE_OPERATOR_IN = 2;
const unsigned short SVG_FECOMPOSITE_OPERATOR_OUT = 3;
const unsigned short SVG_FECOMPOSITE_OPERATOR_ATOP = 4;
const unsigned short SVG_FECOMPOSITE_OPERATOR_XOR = 5;
const unsigned short SVG_FECOMPOSITE_OPERATOR_ARITHMETIC = 6;
readonly attribute SVGAnimatedString in1;
readonly attribute SVGAnimatedString in2;
[ImplementedAs=operator_for_bindings] readonly attribute SVGAnimatedEnumeration operator;
readonly attribute SVGAnimatedNumber k2;
readonly attribute SVGAnimatedNumber k3;
readonly attribute SVGAnimatedNumber k1;
readonly attribute SVGAnimatedNumber k4;
};
SVGFECompositeElement includes SVGFilterPrimitiveStandardAttributes;

View file

@ -12,6 +12,7 @@
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/SVG/SVGFEBlendElement.h>
#include <LibWeb/SVG/SVGFECompositeElement.h>
#include <LibWeb/SVG/SVGFEFloodElement.h>
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
#include <LibWeb/SVG/SVGFEImageElement.h>
@ -125,6 +126,42 @@ Optional<Gfx::Filter> SVGFilterElement::gfx_filter(Layout::NodeWithStyle const&
root_filter = Gfx::Filter::blend(background, foreground, blend_mode);
update_result_map(*blend_primitive);
} else if (auto* composite_primitive = as_if<SVGFECompositeElement>(node)) {
auto foreground = resolve_input_filter(composite_primitive->in1()->base_val());
auto background = resolve_input_filter(composite_primitive->in2()->base_val());
auto operator_ = composite_primitive->operator_();
if (operator_ == SVGFECompositeElement::CompositingOperator::Arithmetic) {
auto k1 = composite_primitive->k1()->base_val();
auto k2 = composite_primitive->k2()->base_val();
auto k3 = composite_primitive->k3()->base_val();
auto k4 = composite_primitive->k4()->base_val();
root_filter = Gfx::Filter::arithmetic(background, foreground, k1, k2, k3, k4);
} else {
auto to_compositing_and_blending_operator = [](SVGFECompositeElement::CompositingOperator operator_) {
switch (operator_) {
case SVGFECompositeElement::CompositingOperator::Over:
return Gfx::CompositingAndBlendingOperator::SourceOver;
case SVGFECompositeElement::CompositingOperator::In:
return Gfx::CompositingAndBlendingOperator::SourceIn;
case SVGFECompositeElement::CompositingOperator::Out:
return Gfx::CompositingAndBlendingOperator::DestinationOut;
case SVGFECompositeElement::CompositingOperator::Atop:
return Gfx::CompositingAndBlendingOperator::SourceATop;
case SVGFECompositeElement::CompositingOperator::Xor:
return Gfx::CompositingAndBlendingOperator::Xor;
case SVGFECompositeElement::CompositingOperator::Lighter:
return Gfx::CompositingAndBlendingOperator::Lighter;
default:
break;
}
return Gfx::CompositingAndBlendingOperator::SourceOver;
};
root_filter = Gfx::Filter::blend(background, foreground, to_compositing_and_blending_operator(operator_));
}
update_result_map(*composite_primitive);
} else if (auto* blur_primitive = as_if<SVGFEGaussianBlurElement>(node)) {
auto input = resolve_input_filter(blur_primitive->in1()->base_val());

View file

@ -18,6 +18,7 @@ namespace Web::SVG::TagNames {
__ENUMERATE_SVG_TAG(desc) \
__ENUMERATE_SVG_TAG(ellipse) \
__ENUMERATE_SVG_TAG(feBlend) \
__ENUMERATE_SVG_TAG(feComposite) \
__ENUMERATE_SVG_TAG(feFlood) \
__ENUMERATE_SVG_TAG(feGaussianBlur) \
__ENUMERATE_SVG_TAG(feImage) \

View file

@ -388,6 +388,7 @@ libweb_js_bindings(SVG/SVGImageElement)
libweb_js_bindings(SVG/SVGCircleElement)
libweb_js_bindings(SVG/SVGEllipseElement)
libweb_js_bindings(SVG/SVGFEBlendElement)
libweb_js_bindings(SVG/SVGFECompositeElement)
libweb_js_bindings(SVG/SVGFEFloodElement)
libweb_js_bindings(SVG/SVGFEGaussianBlurElement)
libweb_js_bindings(SVG/SVGFEImageElement)

View file

@ -0,0 +1,4 @@
<!DOCTYPE html>
<svg width="110" height="110" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="100" height="100" fill="green" />
</svg>

View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<link rel="match" href="../../expected/svg/composite-filter-ref.html" />
<svg width="110" height="110" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="compositeFilter">
<feFlood flood-color="green" result="greenFill" />
<feComposite in="greenFill" in2="SourceGraphic" operator="in" />
</filter>
</defs>
<rect x="10" y="10" width="100" height="100" fill="red" filter="url(#compositeFilter)" />
</svg>

View file

@ -376,6 +376,7 @@ SVGDescElement
SVGElement
SVGEllipseElement
SVGFEBlendElement
SVGFECompositeElement
SVGFEFloodElement
SVGFEGaussianBlurElement
SVGFEImageElement