LibWeb+LibGfx: Implement SVGFEMorphologyElement

This filter primitive is used to erode or dilate an image.
This commit is contained in:
Tim Ledbetter
2025-11-15 12:30:15 +00:00
committed by Andreas Kling
parent 837d5fb7ea
commit 36c6079dbc
Notes: github-actions[bot] 2025-11-15 15:09:46 +00:00
17 changed files with 235 additions and 0 deletions

View File

@@ -277,6 +277,18 @@ Filter Filter::merge(Vector<Optional<Filter>> const& inputs)
return Filter(Impl::create(SkImageFilters::Merge(skia_filters.data(), skia_filters.size())));
}
Filter Filter::erode(float radius_x, float radius_y, Optional<Filter> const& input)
{
sk_sp<SkImageFilter> input_skia = input.has_value() ? input->m_impl->filter : nullptr;
return Filter(Impl::create(SkImageFilters::Erode(radius_x, radius_y, input_skia)));
}
Filter Filter::dilate(float radius_x, float radius_y, Optional<Filter> const& input)
{
sk_sp<SkImageFilter> input_skia = input.has_value() ? input->m_impl->filter : nullptr;
return Filter(Impl::create(SkImageFilters::Dilate(radius_x, radius_y, input_skia)));
}
Filter Filter::offset(float dx, float dy, Optional<Filter const&> input)
{
sk_sp<SkImageFilter> input_skia = input.has_value() ? input->m_impl->filter : nullptr;

View File

@@ -48,6 +48,8 @@ public:
static Filter image(Gfx::ImmutableBitmap const& bitmap, Gfx::IntRect const& src_rect, Gfx::IntRect const& dest_rect, Gfx::ScalingMode scaling_mode);
static Filter merge(Vector<Optional<Filter>> const&);
static Filter offset(float dx, float dy, Optional<Filter const&> input = {});
static Filter erode(float radius_x, float radius_y, Optional<Filter> const& input = {});
static Filter dilate(float radius_x, float radius_y, Optional<Filter> const& input = {});
FilterImpl const& impl() const;

View File

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

View File

@@ -927,6 +927,7 @@ set(SOURCES
SVG/SVGFEImageElement.cpp
SVG/SVGFEMergeElement.cpp
SVG/SVGFEMergeNodeElement.cpp
SVG/SVGFEMorphologyElement.cpp
SVG/SVGFEOffsetElement.cpp
SVG/SVGFilterElement.cpp
SVG/SVGFilterPrimitiveStandardAttributes.cpp

View File

@@ -106,6 +106,7 @@
#include <LibWeb/SVG/SVGFEImageElement.h>
#include <LibWeb/SVG/SVGFEMergeElement.h>
#include <LibWeb/SVG/SVGFEMergeNodeElement.h>
#include <LibWeb/SVG/SVGFEMorphologyElement.h>
#include <LibWeb/SVG/SVGFEOffsetElement.h>
#include <LibWeb/SVG/SVGFilterElement.h>
#include <LibWeb/SVG/SVGForeignObjectElement.h>
@@ -504,6 +505,8 @@ static GC::Ref<SVG::SVGElement> create_svg_element(JS::Realm& realm, Document& d
return realm.create<SVG::SVGFEMergeElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feMergeNode)
return realm.create<SVG::SVGFEMergeNodeElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feMorphology)
return realm.create<SVG::SVGFEMorphologyElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feOffset)
return realm.create<SVG::SVGFEOffsetElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::filter)

View File

@@ -1116,6 +1116,7 @@ class SVGFEFuncGElement;
class SVGFEFuncRElement;
class SVGFEGaussianBlurElement;
class SVGFEImageElement;
class SVGFEMorphologyElement;
class SVGFilterElement;
class SVGFitToViewBox;
class SVGForeignObjectElement;

View File

@@ -76,6 +76,7 @@ namespace Web::SVG::AttributeNames {
__ENUMERATE_SVG_ATTRIBUTE(preserveAspectRatio, "preserveAspectRatio") \
__ENUMERATE_SVG_ATTRIBUTE(primitiveUnits, "primitiveUnits") \
__ENUMERATE_SVG_ATTRIBUTE(r, "r") \
__ENUMERATE_SVG_ATTRIBUTE(radius, "radius") \
__ENUMERATE_SVG_ATTRIBUTE(refX, "refX") \
__ENUMERATE_SVG_ATTRIBUTE(refY, "refY") \
__ENUMERATE_SVG_ATTRIBUTE(repeatCount, "repeatCount") \

View File

@@ -0,0 +1,85 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/SVGFEMorphologyElementPrototype.h>
#include <LibWeb/SVG/AttributeNames.h>
#include <LibWeb/SVG/SVGFEMorphologyElement.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGFEMorphologyElement);
SVGFEMorphologyElement::SVGFEMorphologyElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: SVGElement(document, qualified_name)
{
}
void SVGFEMorphologyElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGFEMorphologyElement);
Base::initialize(realm);
}
void SVGFEMorphologyElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
SVGFilterPrimitiveStandardAttributes::visit_edges(visitor);
visitor.visit(m_in1);
visitor.visit(m_radius_x);
visitor.visit(m_radius_y);
}
void SVGFEMorphologyElement::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_) {
if (!new_value.has_value()) {
m_morphology_operator = Gfx::MorphologyOperator::Dilate;
return;
}
if (new_value->equals_ignoring_ascii_case("erode"sv)) {
m_morphology_operator = Gfx::MorphologyOperator::Erode;
} else if (new_value->equals_ignoring_ascii_case("dilate"sv)) {
m_morphology_operator = Gfx::MorphologyOperator::Dilate;
} else {
m_morphology_operator = Gfx::MorphologyOperator::Dilate;
}
}
}
GC::Ref<SVGAnimatedString> SVGFEMorphologyElement::in1()
{
if (!m_in1)
m_in1 = SVGAnimatedString::create(realm(), *this, DOM::QualifiedName { AttributeNames::in, OptionalNone {}, OptionalNone {} });
return *m_in1;
}
GC::Ref<SVGAnimatedEnumeration> SVGFEMorphologyElement::operator_for_bindings()
{
return SVGAnimatedEnumeration::create(realm(), to_underlying(m_morphology_operator));
}
GC::Ref<SVGAnimatedNumber> SVGFEMorphologyElement::radius_x()
{
if (!m_radius_x)
m_radius_x = SVGAnimatedNumber::create(realm(), *this, DOM::QualifiedName { SVG::AttributeNames::radius, OptionalNone {}, OptionalNone {} }, 0.0,
SVGAnimatedNumber::SupportsSecondValue::Yes, SVGAnimatedNumber::ValueRepresented::First);
return *m_radius_x;
}
GC::Ref<SVGAnimatedNumber> SVGFEMorphologyElement::radius_y()
{
if (!m_radius_y)
m_radius_y = SVGAnimatedNumber::create(realm(), *this, DOM::QualifiedName { SVG::AttributeNames::radius, OptionalNone {}, OptionalNone {} }, 0.0,
SVGAnimatedNumber::SupportsSecondValue::Yes, SVGAnimatedNumber::ValueRepresented::Second);
return *m_radius_y;
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibGfx/MorphologyOperator.h>
#include <LibWeb/SVG/SVGAnimatedEnumeration.h>
#include <LibWeb/SVG/SVGAnimatedNumber.h>
#include <LibWeb/SVG/SVGAnimatedString.h>
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.h>
namespace Web::SVG {
// https://www.w3.org/TR/filter-effects-1/#svgfemorphologyelement
class SVGFEMorphologyElement final
: public SVGElement
, public SVGFilterPrimitiveStandardAttributes<SVGFEMorphologyElement> {
WEB_PLATFORM_OBJECT(SVGFEMorphologyElement, SVGElement);
GC_DECLARE_ALLOCATOR(SVGFEMorphologyElement);
public:
virtual ~SVGFEMorphologyElement() override = default;
GC::Ref<SVGAnimatedString> in1();
GC::Ref<SVGAnimatedEnumeration> operator_for_bindings();
Gfx::MorphologyOperator morphology_operator() { return m_morphology_operator; }
GC::Ref<SVGAnimatedNumber> radius_x();
GC::Ref<SVGAnimatedNumber> radius_y();
private:
SVGFEMorphologyElement(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;
Gfx::MorphologyOperator m_morphology_operator { Gfx::MorphologyOperator::Erode };
GC::Ptr<SVGAnimatedNumber> m_radius_x;
GC::Ptr<SVGAnimatedNumber> m_radius_y;
};
}

View File

@@ -0,0 +1,22 @@
#import <SVG/SVGAnimatedEnumeration.idl>
#import <SVG/SVGAnimatedNumber.idl>
#import <SVG/SVGAnimatedString.idl>
#import <SVG/SVGElement.idl>
#import <SVG/SVGFilterPrimitiveStandardAttributes.idl>
// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEMorphologyElement
[Exposed=Window]
interface SVGFEMorphologyElement : SVGElement {
// Morphology Operators
const unsigned short SVG_MORPHOLOGY_OPERATOR_UNKNOWN = 0;
const unsigned short SVG_MORPHOLOGY_OPERATOR_ERODE = 1;
const unsigned short SVG_MORPHOLOGY_OPERATOR_DILATE = 2;
readonly attribute SVGAnimatedString in1;
[ImplementedAs=operator_for_bindings] readonly attribute SVGAnimatedEnumeration operator;
readonly attribute SVGAnimatedNumber radiusX;
readonly attribute SVGAnimatedNumber radiusY;
};
SVGFEMorphologyElement includes SVGFilterPrimitiveStandardAttributes;

View File

@@ -27,6 +27,7 @@
#include <LibWeb/SVG/SVGFEImageElement.h>
#include <LibWeb/SVG/SVGFEMergeElement.h>
#include <LibWeb/SVG/SVGFEMergeNodeElement.h>
#include <LibWeb/SVG/SVGFEMorphologyElement.h>
#include <LibWeb/SVG/SVGFEOffsetElement.h>
#include <LibWeb/SVG/SVGFilterElement.h>
@@ -284,6 +285,24 @@ Optional<Gfx::Filter> SVGFilterElement::gfx_filter(Layout::NodeWithStyle const&
root_filter = Gfx::Filter::merge(merge_inputs);
update_result_map(*merge_primitive);
} else if (auto* morphology_primitive = as_if<SVGFEMorphologyElement>(node)) {
auto input = resolve_input_filter(morphology_primitive->in1()->base_val());
auto radius_x = morphology_primitive->radius_x()->base_val();
auto radius_y = morphology_primitive->radius_y()->base_val();
auto morphology_operator = morphology_primitive->morphology_operator();
switch (morphology_operator) {
case Gfx::MorphologyOperator::Erode:
root_filter = Gfx::Filter::erode(radius_x, radius_y, input);
break;
case Gfx::MorphologyOperator::Dilate:
root_filter = Gfx::Filter::dilate(radius_x, radius_y, input);
break;
case Gfx::MorphologyOperator::Unknown:
VERIFY_NOT_REACHED();
}
update_result_map(*morphology_primitive);
} else if (auto* offset_primitive = as_if<SVGFEOffsetElement>(node)) {
auto input = resolve_input_filter(offset_primitive->in1()->base_val());

View File

@@ -15,6 +15,7 @@
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
#include <LibWeb/SVG/SVGFEImageElement.h>
#include <LibWeb/SVG/SVGFEMergeElement.h>
#include <LibWeb/SVG/SVGFEMorphologyElement.h>
#include <LibWeb/SVG/SVGFEOffsetElement.h>
namespace Web::SVG {
@@ -60,6 +61,7 @@ template class SVGFilterPrimitiveStandardAttributes<SVGFEFloodElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFEGaussianBlurElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFEImageElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFEMergeElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFEMorphologyElement>;
template class SVGFilterPrimitiveStandardAttributes<SVGFEOffsetElement>;
}

View File

@@ -30,6 +30,7 @@ namespace Web::SVG::TagNames {
__ENUMERATE_SVG_TAG(feImage) \
__ENUMERATE_SVG_TAG(feMerge) \
__ENUMERATE_SVG_TAG(feMergeNode) \
__ENUMERATE_SVG_TAG(feMorphology) \
__ENUMERATE_SVG_TAG(feOffset) \
__ENUMERATE_SVG_TAG(filter) \
__ENUMERATE_SVG_TAG(foreignObject) \

View File

@@ -402,6 +402,7 @@ libweb_js_bindings(SVG/SVGFEGaussianBlurElement)
libweb_js_bindings(SVG/SVGFEImageElement)
libweb_js_bindings(SVG/SVGFEMergeElement)
libweb_js_bindings(SVG/SVGFEMergeNodeElement)
libweb_js_bindings(SVG/SVGFEMorphologyElement)
libweb_js_bindings(SVG/SVGFEOffsetElement)
libweb_js_bindings(SVG/SVGFilterElement)
libweb_js_bindings(SVG/SVGForeignObjectElement)

View File

@@ -0,0 +1,2 @@
<!DOCTYPE html>
<div style="width: 100px; height: 100px; background-color: green"></div>

View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<title>feMorphology filter on mirrored content</title>
<link rel="help" href="https://drafts.fxtf.org/filter-effects-1/#feMorphologyElement">
<link rel="match" href="../../../../expected/wpt-import/css/filter-effects/reference/green-100x100.html">
<svg>
<filter id="dilate" filterUnits="userSpaceOnUse"
color-interpolation-filters="sRGB">
<feMorphology operator="dilate" radius="10"/>
</filter>
<rect width="100" height="100" fill="red"/>
<rect x="10" y="10" width="80" height="30" fill="green" filter="url(#dilate)"
transform="translate(0, 50) scale(1, -1)"/>
<rect x="10" y="60" width="80" height="30" fill="green" filter="url(#dilate)"
transform="translate(100, 0) scale(-1, 1)"/>
</svg>

View File

@@ -390,6 +390,7 @@ SVGFEGaussianBlurElement
SVGFEImageElement
SVGFEMergeElement
SVGFEMergeNodeElement
SVGFEMorphologyElement
SVGFEOffsetElement
SVGFilterElement
SVGForeignObjectElement