LibWeb: Implement <feImage> SVG filter

This commit is contained in:
Tim Ledbetter 2025-08-06 22:52:29 +01:00 committed by Sam Atkins
commit d4f05bc4ef
Notes: github-actions[bot] 2025-08-29 09:16:34 +00:00
19 changed files with 242 additions and 3 deletions

View file

@ -224,6 +224,15 @@ Filter Filter::hue_rotate(float angle_degrees, Optional<Filter const&> 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<Optional<Filter>> const& inputs)
{
Vector<sk_sp<SkImageFilter>> skia_filters;

View file

@ -9,6 +9,9 @@
#include <AK/NonnullOwnPtr.h>
#include <LibGfx/Color.h>
#include <LibGfx/CompositingAndBlendingOperator.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibGfx/Rect.h>
#include <LibGfx/ScalingMode.h>
namespace Gfx {
@ -40,6 +43,7 @@ public:
static Filter color_matrix(float matrix[20], Optional<Filter const&> input = {});
static Filter saturate(float value, Optional<Filter const&> input = {});
static Filter hue_rotate(float angle_degrees, Optional<Filter const&> 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<Optional<Filter>> const&);
static Filter offset(float dx, float dy, Optional<Filter const&> input = {});

View file

@ -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

View file

@ -94,6 +94,7 @@
#include <LibWeb/SVG/SVGFEBlendElement.h>
#include <LibWeb/SVG/SVGFEFloodElement.h>
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
#include <LibWeb/SVG/SVGFEImageElement.h>
#include <LibWeb/SVG/SVGFEMergeElement.h>
#include <LibWeb/SVG/SVGFEMergeNodeElement.h>
#include <LibWeb/SVG/SVGFEOffsetElement.h>
@ -474,6 +475,8 @@ static GC::Ref<SVG::SVGElement> create_svg_element(JS::Realm& realm, Document& d
return realm.create<SVG::SVGFEFloodElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feGaussianBlur)
return realm.create<SVG::SVGFEGaussianBlurElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feImage)
return realm.create<SVG::SVGFEImageElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feMerge)
return realm.create<SVG::SVGFEMergeElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feMergeNode)

View file

@ -106,6 +106,7 @@ enum class StyleInvalidationReason {
X(LayoutTreeUpdate) \
X(NavigableSetViewportSize) \
X(SVGImageElementFetchTheDocument) \
X(SVGImageFilterFetch) \
X(StyleChange)
enum class SetNeedsLayoutReason {

View file

@ -1051,6 +1051,7 @@ class SVGEllipseElement;
class SVGFEBlendElement;
class SVGFEFloodElement;
class SVGFEGaussianBlurElement;
class SVGFEImageElement;
class SVGFilterElement;
class SVGFitToViewBox;
class SVGForeignObjectElement;

View file

@ -1695,7 +1695,8 @@ Optional<Gfx::Filter> PaintableBox::resolve_filter(CSS::Filter const& computed_f
return;
if (auto* filter_element = as_if<SVG::SVGFilterElement>(*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;

View file

@ -0,0 +1,113 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "SVGFEImageElement.h"
#include <LibCore/Timer.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibWeb/Bindings/SVGFEImageElementPrototype.h>
#include <LibWeb/HTML/DecodedImageData.h>
#include <LibWeb/HTML/PotentialCORSRequest.h>
#include <LibWeb/HTML/SharedResourceRequest.h>
#include <LibWeb/Layout/SVGImageBox.h>
#include <LibWeb/Namespace.h>
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<String> const& old_value, Optional<String> const& value, Optional<FlyString> 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<String> 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<Gfx::ImmutableBitmap> 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<Gfx::IntRect> 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 });
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2025, Tim Ledbetter <tim.ledbetter@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.h>
#include <LibWeb/SVG/SVGURIReference.h>
namespace Web::SVG {
class SVGFEImageElement final
: public SVGElement
, public SVGFilterPrimitiveStandardAttributes<SVGFEImageElement>
, public SVGURIReferenceMixin<SupportsXLinkHref::Yes> {
WEB_PLATFORM_OBJECT(SVGFEImageElement, SVGElement);
GC_DECLARE_ALLOCATOR(SVGFEImageElement);
public:
virtual ~SVGFEImageElement() override = default;
RefPtr<Gfx::ImmutableBitmap> current_image_bitmap(Gfx::IntSize = {}) const;
Optional<Gfx::IntRect> 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<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
void process_href(Optional<String> const& href);
Optional<URL::URL> m_href;
GC::Ptr<HTML::SharedResourceRequest> m_resource_request;
};
};

View file

@ -0,0 +1,14 @@
#import <SVG/SVGAnimatedString.idl>
#import <SVG/SVGElement.idl>
#import <SVG/SVGFilterPrimitiveStandardAttributes.idl>
#import <SVG/SVGURIReference.idl>
// 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;

View file

@ -5,12 +5,16 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibGfx/ImmutableBitmap.h>
#include <LibWeb/Bindings/SVGFilterElementPrototype.h>
#include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/SVG/SVGFEBlendElement.h>
#include <LibWeb/SVG/SVGFEFloodElement.h>
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
#include <LibWeb/SVG/SVGFEImageElement.h>
#include <LibWeb/SVG/SVGFEMergeElement.h>
#include <LibWeb/SVG/SVGFEMergeNodeElement.h>
#include <LibWeb/SVG/SVGFEOffsetElement.h>
@ -77,7 +81,7 @@ void SVGFilterElement::attribute_changed(FlyString const& name, Optional<String>
m_primitive_units = AttributeParser::parse_units(value.value_or({}));
}
Optional<Gfx::Filter> SVGFilterElement::gfx_filter()
Optional<Gfx::Filter> SVGFilterElement::gfx_filter(Layout::NodeWithStyle const& referenced_node)
{
HashMap<String, Gfx::Filter> result_map;
Optional<Gfx::Filter> root_filter;
@ -129,6 +133,27 @@ Optional<Gfx::Filter> 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<SVGFEImageElement>(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<float>());
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<SVGFEMergeElement>(node)) {
Vector<Optional<Gfx::Filter>> merge_inputs;
merge_primitive->template for_each_child_of_type<SVGFEMergeNodeElement>([&](auto& merge_node) {

View file

@ -31,7 +31,7 @@ public:
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
Optional<Gfx::Filter> gfx_filter();
Optional<Gfx::Filter> gfx_filter(Layout::NodeWithStyle const& referenced_node);
GC::Ref<SVGAnimatedEnumeration> filter_units() const;
GC::Ref<SVGAnimatedEnumeration> primitive_units() const;

View file

@ -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) \

View file

@ -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)

View file

@ -0,0 +1,2 @@
<!DOCTYPE html>
<img src="../support/color-palette.png">

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<title>CSS Filters: feImage and CSS reference filters.</title>
<link rel="help" href="https://drafts.fxtf.org/filter-effects-1/#feImageElement">
<link rel="help" href="https://drafts.fxtf.org/filter-effects-1/#FilterProperty">
<link rel="match" href="../../../../expected/wpt-import/css/filter-effects/reference/effect-reference-feimage-001-ref.html">
<meta name="assert" content="This test ensures that CSS reference filters supports feImage."/>
<style>
#filtered {
width: 160px;
height: 90px;
filter: url(#imagereplace);
}
</style>
<div id="filtered"></div>
<svg width="0" height="0">
<filter id="imagereplace" x="0%" y="0%" width="100%" height="100%">
<feimage xlink:href="support/color-palette.png"/>
</filter>
</svg>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -354,6 +354,7 @@ SVGEllipseElement
SVGFEBlendElement
SVGFEFloodElement
SVGFEGaussianBlurElement
SVGFEImageElement
SVGFEMergeElement
SVGFEMergeNodeElement
SVGFEOffsetElement