diff --git a/Libraries/LibGfx/Filter.cpp b/Libraries/LibGfx/Filter.cpp index e3b4ca2f909..864e9537e0a 100644 --- a/Libraries/LibGfx/Filter.cpp +++ b/Libraries/LibGfx/Filter.cpp @@ -224,6 +224,15 @@ Filter Filter::hue_rotate(float angle_degrees, Optional input) return Filter(Impl::create(SkImageFilters::ColorFilter(color_filter, input_skia))); } +Filter Filter::image(Gfx::ImmutableBitmap const& bitmap, Gfx::IntRect const& src_rect, Gfx::IntRect const& dest_rect, Gfx::ScalingMode scaling_mode) +{ + auto skia_src_rect = to_skia_rect(src_rect); + auto skia_dest_rect = to_skia_rect(dest_rect); + auto sampling_options = to_skia_sampling_options(scaling_mode); + + return Filter(Impl::create(SkImageFilters::Image(sk_ref_sp(bitmap.sk_image()), skia_src_rect, skia_dest_rect, sampling_options))); +} + Filter Filter::merge(Vector> const& inputs) { Vector> skia_filters; diff --git a/Libraries/LibGfx/Filter.h b/Libraries/LibGfx/Filter.h index 3908fcd6841..62aac3c019f 100644 --- a/Libraries/LibGfx/Filter.h +++ b/Libraries/LibGfx/Filter.h @@ -9,6 +9,9 @@ #include #include #include +#include +#include +#include namespace Gfx { @@ -40,6 +43,7 @@ public: static Filter color_matrix(float matrix[20], Optional input = {}); static Filter saturate(float value, Optional input = {}); static Filter hue_rotate(float angle_degrees, Optional input = {}); + 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 = {}); diff --git a/Libraries/LibWeb/CMakeLists.txt b/Libraries/LibWeb/CMakeLists.txt index 382c83c4f68..b181cc8bd8e 100644 --- a/Libraries/LibWeb/CMakeLists.txt +++ b/Libraries/LibWeb/CMakeLists.txt @@ -874,6 +874,7 @@ set(SOURCES SVG/SVGFEBlendElement.cpp SVG/SVGFEFloodElement.cpp SVG/SVGFEGaussianBlurElement.cpp + SVG/SVGFEImageElement.cpp SVG/SVGFEMergeElement.cpp SVG/SVGFEMergeNodeElement.cpp SVG/SVGFEOffsetElement.cpp diff --git a/Libraries/LibWeb/DOM/ElementFactory.cpp b/Libraries/LibWeb/DOM/ElementFactory.cpp index a819b837177..affcf6bdf1b 100644 --- a/Libraries/LibWeb/DOM/ElementFactory.cpp +++ b/Libraries/LibWeb/DOM/ElementFactory.cpp @@ -94,6 +94,7 @@ #include #include #include +#include #include #include #include @@ -474,6 +475,8 @@ static GC::Ref create_svg_element(JS::Realm& realm, Document& d return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::feGaussianBlur) return realm.create(document, move(qualified_name)); + if (local_name == SVG::TagNames::feImage) + return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::feMerge) return realm.create(document, move(qualified_name)); if (local_name == SVG::TagNames::feMergeNode) diff --git a/Libraries/LibWeb/DOM/Node.h b/Libraries/LibWeb/DOM/Node.h index cab51d4b8dd..86b2043dd45 100644 --- a/Libraries/LibWeb/DOM/Node.h +++ b/Libraries/LibWeb/DOM/Node.h @@ -106,6 +106,7 @@ enum class StyleInvalidationReason { X(LayoutTreeUpdate) \ X(NavigableSetViewportSize) \ X(SVGImageElementFetchTheDocument) \ + X(SVGImageFilterFetch) \ X(StyleChange) enum class SetNeedsLayoutReason { diff --git a/Libraries/LibWeb/Forward.h b/Libraries/LibWeb/Forward.h index d83eb2ba4ad..040d7768f06 100644 --- a/Libraries/LibWeb/Forward.h +++ b/Libraries/LibWeb/Forward.h @@ -1051,6 +1051,7 @@ class SVGEllipseElement; class SVGFEBlendElement; class SVGFEFloodElement; class SVGFEGaussianBlurElement; +class SVGFEImageElement; class SVGFilterElement; class SVGFitToViewBox; class SVGForeignObjectElement; diff --git a/Libraries/LibWeb/Painting/PaintableBox.cpp b/Libraries/LibWeb/Painting/PaintableBox.cpp index e82821fea56..4a29be34ee4 100644 --- a/Libraries/LibWeb/Painting/PaintableBox.cpp +++ b/Libraries/LibWeb/Painting/PaintableBox.cpp @@ -1695,7 +1695,8 @@ Optional PaintableBox::resolve_filter(CSS::Filter const& computed_f return; if (auto* filter_element = as_if(*maybe_filter)) { - auto new_filter = filter_element->gfx_filter(); + auto& layout_node = layout_node_with_style_and_box_metrics(); + auto new_filter = filter_element->gfx_filter(layout_node); if (!new_filter.has_value()) return; diff --git a/Libraries/LibWeb/SVG/SVGFEImageElement.cpp b/Libraries/LibWeb/SVG/SVGFEImageElement.cpp new file mode 100644 index 00000000000..d8068ea2a27 --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGFEImageElement.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2025, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "SVGFEImageElement.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Web::SVG { + +GC_DEFINE_ALLOCATOR(SVGFEImageElement); + +SVGFEImageElement::SVGFEImageElement(DOM::Document& document, DOM::QualifiedName qualified_name) + : SVGElement(document, qualified_name) +{ +} + +void SVGFEImageElement::initialize(JS::Realm& realm) +{ + WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGFEImageElement); + Base::initialize(realm); +} + +void SVGFEImageElement::visit_edges(Cell::Visitor& visitor) +{ + Base::visit_edges(visitor); + SVGFilterPrimitiveStandardAttributes::visit_edges(visitor); + SVGURIReferenceMixin::visit_edges(visitor); + visitor.visit(m_resource_request); +} + +void SVGFEImageElement::attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) +{ + Base::attribute_changed(name, old_value, value, namespace_); + + if (name == SVG::AttributeNames::href) { + if (namespace_ == Namespace::XLink && has_attribute_ns({}, name)) + return; + + auto href = value; + if (!namespace_.has_value() && !href.has_value()) + href = get_attribute_ns(SVG::AttributeNames::href, Namespace::XLink); + + process_href(href); + } +} + +void SVGFEImageElement::process_href(Optional const& href) +{ + if (!href.has_value()) { + m_href = {}; + return; + } + + m_href = document().encoding_parse_url(*href); + if (!m_href.has_value()) + return; + + m_resource_request = HTML::SharedResourceRequest::get_or_create(realm(), document().page(), *m_href); + m_resource_request->add_callbacks( + [this, resource_request = GC::Root { m_resource_request }] { + set_needs_style_update(true); + if (auto layout_node = this->layout_node()) + layout_node->set_needs_layout_update(DOM::SetNeedsLayoutReason::SVGImageFilterFetch); + }, + nullptr); + + if (m_resource_request->needs_fetching()) { + auto request = HTML::create_potential_CORS_request(vm(), *m_href, Fetch::Infrastructure::Request::Destination::Image, HTML::CORSSettingAttribute::NoCORS); + request->set_client(&document().relevant_settings_object()); + m_resource_request->fetch_resource(realm(), request); + } +} + +RefPtr SVGFEImageElement::current_image_bitmap(Gfx::IntSize size) const +{ + if (!m_resource_request) + return {}; + if (auto data = m_resource_request->image_data()) + return data->bitmap(0, size); + return {}; +} + +Optional SVGFEImageElement::content_rect() const +{ + auto bitmap = current_image_bitmap(); + if (!bitmap) + return {}; + auto layout_node = this->layout_node(); + if (!layout_node) + return {}; + auto width = layout_node->computed_values().width().to_px(*layout_node, 0); + if (width == 0) + width = bitmap->width(); + + auto height = layout_node->computed_values().height().to_px(*layout_node, 0); + if (height == 0) + height = bitmap->height(); + + auto x = layout_node->computed_values().x().to_px(*layout_node, 0); + auto y = layout_node->computed_values().y().to_px(*layout_node, 0); + return Gfx::enclosing_int_rect({ x, y, width, height }); +} + +} diff --git a/Libraries/LibWeb/SVG/SVGFEImageElement.h b/Libraries/LibWeb/SVG/SVGFEImageElement.h new file mode 100644 index 00000000000..cad06cbb2e3 --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGFEImageElement.h @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, Tim Ledbetter + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace Web::SVG { + +class SVGFEImageElement final + : public SVGElement + , public SVGFilterPrimitiveStandardAttributes + , public SVGURIReferenceMixin { + WEB_PLATFORM_OBJECT(SVGFEImageElement, SVGElement); + GC_DECLARE_ALLOCATOR(SVGFEImageElement); + +public: + virtual ~SVGFEImageElement() override = default; + + RefPtr current_image_bitmap(Gfx::IntSize = {}) const; + Optional content_rect() const; + +private: + SVGFEImageElement(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& value, Optional const& namespace_) override; + + void process_href(Optional const& href); + + Optional m_href; + + GC::Ptr m_resource_request; +}; + +}; diff --git a/Libraries/LibWeb/SVG/SVGFEImageElement.idl b/Libraries/LibWeb/SVG/SVGFEImageElement.idl new file mode 100644 index 00000000000..4589fd40e6a --- /dev/null +++ b/Libraries/LibWeb/SVG/SVGFEImageElement.idl @@ -0,0 +1,14 @@ +#import +#import +#import +#import + +// https://www.w3.org/TR/filter-effects-1/#feImageElement +[Exposed=Window] +interface SVGFEImageElement : SVGElement { + [FIXME] readonly attribute SVGAnimatedPreserveAspectRatio preserveAspectRatio; + [FIXME] readonly attribute SVGAnimatedString crossOrigin; +}; + +SVGFEImageElement includes SVGFilterPrimitiveStandardAttributes; +SVGFEImageElement includes SVGURIReference; diff --git a/Libraries/LibWeb/SVG/SVGFilterElement.cpp b/Libraries/LibWeb/SVG/SVGFilterElement.cpp index 070e14c2cc1..3f7eb78bf10 100644 --- a/Libraries/LibWeb/SVG/SVGFilterElement.cpp +++ b/Libraries/LibWeb/SVG/SVGFilterElement.cpp @@ -5,12 +5,16 @@ * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include +#include +#include #include #include #include +#include #include #include #include @@ -77,7 +81,7 @@ void SVGFilterElement::attribute_changed(FlyString const& name, Optional m_primitive_units = AttributeParser::parse_units(value.value_or({})); } -Optional SVGFilterElement::gfx_filter() +Optional SVGFilterElement::gfx_filter(Layout::NodeWithStyle const& referenced_node) { HashMap result_map; Optional root_filter; @@ -129,6 +133,27 @@ Optional SVGFilterElement::gfx_filter() root_filter = Gfx::Filter::blur(radius_x, radius_y, input); update_result_map(*blur_primitive); + } else if (auto* image_primitive = as_if(node)) { + auto bitmap = image_primitive->current_image_bitmap({}); + if (!bitmap) + return IterationDecision::Continue; + + auto src_rect = image_primitive->content_rect(); + if (!src_rect.has_value()) + return IterationDecision::Continue; + + auto* dom_node = referenced_node.dom_node(); + if (!dom_node) + return IterationDecision::Continue; + + auto* paintable_box = dom_node->paintable_box(); + if (!paintable_box) + return IterationDecision::Continue; + + auto dest_rect = Gfx::enclosing_int_rect(paintable_box->absolute_rect().to_type()); + auto scaling_mode = CSS::to_gfx_scaling_mode(paintable_box->computed_values().image_rendering(), *src_rect, dest_rect); + root_filter = Gfx::Filter::image(*bitmap, *src_rect, dest_rect, scaling_mode); + update_result_map(*image_primitive); } else if (auto* merge_primitive = as_if(node)) { Vector> merge_inputs; merge_primitive->template for_each_child_of_type([&](auto& merge_node) { diff --git a/Libraries/LibWeb/SVG/SVGFilterElement.h b/Libraries/LibWeb/SVG/SVGFilterElement.h index 6596a8c1ace..47122c42409 100644 --- a/Libraries/LibWeb/SVG/SVGFilterElement.h +++ b/Libraries/LibWeb/SVG/SVGFilterElement.h @@ -31,7 +31,7 @@ public: virtual void attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) override; - Optional gfx_filter(); + Optional gfx_filter(Layout::NodeWithStyle const& referenced_node); GC::Ref filter_units() const; GC::Ref primitive_units() const; diff --git a/Libraries/LibWeb/SVG/TagNames.h b/Libraries/LibWeb/SVG/TagNames.h index 61ee00c9556..d197fa1d9ff 100644 --- a/Libraries/LibWeb/SVG/TagNames.h +++ b/Libraries/LibWeb/SVG/TagNames.h @@ -20,6 +20,7 @@ namespace Web::SVG::TagNames { __ENUMERATE_SVG_TAG(feBlend) \ __ENUMERATE_SVG_TAG(feFlood) \ __ENUMERATE_SVG_TAG(feGaussianBlur) \ + __ENUMERATE_SVG_TAG(feImage) \ __ENUMERATE_SVG_TAG(feMerge) \ __ENUMERATE_SVG_TAG(feMergeNode) \ __ENUMERATE_SVG_TAG(feOffset) \ diff --git a/Libraries/LibWeb/idl_files.cmake b/Libraries/LibWeb/idl_files.cmake index 8f155f45fba..19c38b5e861 100644 --- a/Libraries/LibWeb/idl_files.cmake +++ b/Libraries/LibWeb/idl_files.cmake @@ -365,6 +365,7 @@ libweb_js_bindings(SVG/SVGEllipseElement) libweb_js_bindings(SVG/SVGFEBlendElement) libweb_js_bindings(SVG/SVGFEFloodElement) libweb_js_bindings(SVG/SVGFEGaussianBlurElement) +libweb_js_bindings(SVG/SVGFEImageElement) libweb_js_bindings(SVG/SVGFEMergeElement) libweb_js_bindings(SVG/SVGFEMergeNodeElement) libweb_js_bindings(SVG/SVGFEOffsetElement) diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/reference/effect-reference-feimage-001-ref.html b/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/reference/effect-reference-feimage-001-ref.html new file mode 100644 index 00000000000..9b982b3cfb4 --- /dev/null +++ b/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/reference/effect-reference-feimage-001-ref.html @@ -0,0 +1,2 @@ + + diff --git a/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/support/color-palette.png b/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/support/color-palette.png new file mode 100644 index 00000000000..68641b76771 Binary files /dev/null and b/Tests/LibWeb/Ref/expected/wpt-import/css/filter-effects/support/color-palette.png differ diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/effect-reference-feimage-001.html b/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/effect-reference-feimage-001.html new file mode 100644 index 00000000000..07fb395ab06 --- /dev/null +++ b/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/effect-reference-feimage-001.html @@ -0,0 +1,19 @@ + +CSS Filters: feImage and CSS reference filters. + + + + + +
+ + + + + diff --git a/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/support/color-palette.png b/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/support/color-palette.png new file mode 100644 index 00000000000..68641b76771 Binary files /dev/null and b/Tests/LibWeb/Ref/input/wpt-import/css/filter-effects/support/color-palette.png differ diff --git a/Tests/LibWeb/Text/expected/all-window-properties.txt b/Tests/LibWeb/Text/expected/all-window-properties.txt index 10b4cad29c7..55c7f739886 100644 --- a/Tests/LibWeb/Text/expected/all-window-properties.txt +++ b/Tests/LibWeb/Text/expected/all-window-properties.txt @@ -354,6 +354,7 @@ SVGEllipseElement SVGFEBlendElement SVGFEFloodElement SVGFEGaussianBlurElement +SVGFEImageElement SVGFEMergeElement SVGFEMergeNodeElement SVGFEOffsetElement