diff --git a/Libraries/LibGfx/Filter.cpp b/Libraries/LibGfx/Filter.cpp index c0cee62622c..07a366e2bca 100644 --- a/Libraries/LibGfx/Filter.cpp +++ b/Libraries/LibGfx/Filter.cpp @@ -277,6 +277,18 @@ Filter Filter::merge(Vector> const& inputs) return Filter(Impl::create(SkImageFilters::Merge(skia_filters.data(), skia_filters.size()))); } +Filter Filter::erode(float radius_x, float radius_y, Optional const& input) +{ + sk_sp 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 const& input) +{ + sk_sp 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 input) { sk_sp input_skia = input.has_value() ? input->m_impl->filter : nullptr; diff --git a/Libraries/LibGfx/Filter.h b/Libraries/LibGfx/Filter.h index 0bca39c7eb3..12a28ddbe56 100644 --- a/Libraries/LibGfx/Filter.h +++ b/Libraries/LibGfx/Filter.h @@ -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> const&); static Filter offset(float dx, float dy, Optional input = {}); + static Filter erode(float radius_x, float radius_y, Optional const& input = {}); + static Filter dilate(float radius_x, float radius_y, Optional const& input = {}); FilterImpl const& impl() const; diff --git a/Libraries/LibGfx/MorphologyOperator.h b/Libraries/LibGfx/MorphologyOperator.h new file mode 100644 index 00000000000..9b074dac6b2 --- /dev/null +++ b/Libraries/LibGfx/MorphologyOperator.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2025, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +namespace Gfx { + +enum class MorphologyOperator { + Unknown, + Erode, + Dilate, +}; + +} diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 54eef248d3f..74df23d1c0a 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -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 diff --git a/Libraries/LibWeb/DOM/ElementFactory.cpp b/Libraries/LibWeb/DOM/ElementFactory.cpp index 768411cb98c..655465de6a3 100644 --- a/Libraries/LibWeb/DOM/ElementFactory.cpp +++ b/Libraries/LibWeb/DOM/ElementFactory.cpp @@ -106,6 +106,7 @@ #include #include #include +#include #include #include #include @@ -504,6 +505,8 @@ static GC::Ref create_svg_element(JS::Realm& realm, Document& d return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::feMergeNode) return realm.create(document, move(qualified_name)); + if (local_name == SVG::TagNames::feMorphology) + return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::feOffset) return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::filter) diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index 7d3f2215bf5..25e218ca759 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -1116,6 +1116,7 @@ class SVGFEFuncGElement; class SVGFEFuncRElement; class SVGFEGaussianBlurElement; class SVGFEImageElement; +class SVGFEMorphologyElement; class SVGFilterElement; class SVGFitToViewBox; class SVGForeignObjectElement; diff --git a/Libraries/LibWeb/SVG/AttributeNames.h b/Libraries/LibWeb/SVG/AttributeNames.h index 69b070bac5d..97799b4f9d1 100644 --- a/Libraries/LibWeb/SVG/AttributeNames.h +++ b/Libraries/LibWeb/SVG/AttributeNames.h @@ -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") \ diff --git a/Libraries/LibWeb/SVG/SVGFEMorphologyElement.cpp b/Libraries/LibWeb/SVG/SVGFEMorphologyElement.cpp new file mode 100644 index 00000000000..24386c0bc18 --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGFEMorphologyElement.cpp @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2025, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include + +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 const& old_value, Optional const& new_value, Optional 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 SVGFEMorphologyElement::in1() +{ + if (!m_in1) + m_in1 = SVGAnimatedString::create(realm(), *this, DOM::QualifiedName { AttributeNames::in, OptionalNone {}, OptionalNone {} }); + + return *m_in1; +} + +GC::Ref SVGFEMorphologyElement::operator_for_bindings() +{ + return SVGAnimatedEnumeration::create(realm(), to_underlying(m_morphology_operator)); +} + +GC::Ref 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 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; +} + +} diff --git a/Libraries/LibWeb/SVG/SVGFEMorphologyElement.h b/Libraries/LibWeb/SVG/SVGFEMorphologyElement.h new file mode 100644 index 00000000000..a6440d46015 --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGFEMorphologyElement.h @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2025, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace Web::SVG { + +// https://www.w3.org/TR/filter-effects-1/#svgfemorphologyelement +class SVGFEMorphologyElement final + : public SVGElement + , public SVGFilterPrimitiveStandardAttributes { + WEB_PLATFORM_OBJECT(SVGFEMorphologyElement, SVGElement); + GC_DECLARE_ALLOCATOR(SVGFEMorphologyElement); + +public: + virtual ~SVGFEMorphologyElement() override = default; + + GC::Ref in1(); + + GC::Ref operator_for_bindings(); + Gfx::MorphologyOperator morphology_operator() { return m_morphology_operator; } + + GC::Ref radius_x(); + GC::Ref 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 const& old_value, Optional const& new_value, Optional const& namespace_) override; + + GC::Ptr m_in1; + Gfx::MorphologyOperator m_morphology_operator { Gfx::MorphologyOperator::Erode }; + GC::Ptr m_radius_x; + GC::Ptr m_radius_y; +}; + +} diff --git a/Libraries/LibWeb/SVG/SVGFEMorphologyElement.idl b/Libraries/LibWeb/SVG/SVGFEMorphologyElement.idl new file mode 100644 index 00000000000..7f2f27cbcdd --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGFEMorphologyElement.idl @@ -0,0 +1,22 @@ +#import +#import +#import +#import +#import + +// 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; diff --git a/Libraries/LibWeb/SVG/SVGFilterElement.cpp b/Libraries/LibWeb/SVG/SVGFilterElement.cpp index 339a49ed0cd..f738ae63153 100644 --- a/Libraries/LibWeb/SVG/SVGFilterElement.cpp +++ b/Libraries/LibWeb/SVG/SVGFilterElement.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include @@ -284,6 +285,24 @@ Optional 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(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(node)) { auto input = resolve_input_filter(offset_primitive->in1()->base_val()); diff --git a/Libraries/LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.cpp b/Libraries/LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.cpp index e07518553f4..33337d43523 100644 --- a/Libraries/LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.cpp +++ b/Libraries/LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include namespace Web::SVG { @@ -60,6 +61,7 @@ template class SVGFilterPrimitiveStandardAttributes; template class SVGFilterPrimitiveStandardAttributes; template class SVGFilterPrimitiveStandardAttributes; template class SVGFilterPrimitiveStandardAttributes; +template class SVGFilterPrimitiveStandardAttributes; template class SVGFilterPrimitiveStandardAttributes; } diff --git a/Libraries/LibWeb/SVG/TagNames.h b/Libraries/LibWeb/SVG/TagNames.h index a87d0674bd2..54c1d140f59 100644 --- a/Libraries/LibWeb/SVG/TagNames.h +++ b/Libraries/LibWeb/SVG/TagNames.h @@ -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) \ diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 2a092b669b9..594e8508112 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -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) diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/reference/green-100x100.html b/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/reference/green-100x100.html new file mode 100644 index 00000000000..f718ea6abfb --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/reference/green-100x100.html @@ -0,0 +1,2 @@ + +
diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/morphology-mirrored.html b/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/morphology-mirrored.html new file mode 100644 index 00000000000..6a2f5ccd77a --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/morphology-mirrored.html @@ -0,0 +1,15 @@ + +feMorphology filter on mirrored content + + + + + + + + + + diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index 71c91c51f04..3a505814c05 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -390,6 +390,7 @@ SVGFEGaussianBlurElement SVGFEImageElement SVGFEMergeElement SVGFEMergeNodeElement +SVGFEMorphologyElement SVGFEOffsetElement SVGFilterElement SVGForeignObjectElement