LibWeb/SVG: Implement resolution for a subset of SVG filters

This commit is contained in:
Lucien Fiorini 2025-07-09 17:19:32 +02:00 committed by Sam Atkins
commit 635adc8aa7
Notes: github-actions[bot] 2025-07-09 17:08:27 +00:00
10 changed files with 161 additions and 17 deletions

View file

@ -50,17 +50,20 @@ Filter Filter::compose(Filter const& outer, Filter const& inner)
return Filter(Impl::create(filter)); return Filter(Impl::create(filter));
} }
Filter Filter::blend(Filter const& background, Filter const& foreground, Gfx::CompositingAndBlendingOperator mode) Filter Filter::blend(Optional<Filter const&> background, Optional<Filter const&> foreground, Gfx::CompositingAndBlendingOperator mode)
{ {
auto filter = SkImageFilters::Blend(to_skia_blender(mode), background.m_impl->filter, foreground.m_impl->filter); 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::Blend(to_skia_blender(mode), background_skia, foreground_skia);
return Filter(Impl::create(filter)); return Filter(Impl::create(filter));
} }
Filter Filter::blur(float radius, Optional<Filter const&> input) Filter Filter::blur(float radius_x, float radius_y, Optional<Filter const&> input)
{ {
sk_sp<SkImageFilter> input_skia = input.has_value() ? input->m_impl->filter : nullptr; sk_sp<SkImageFilter> input_skia = input.has_value() ? input->m_impl->filter : nullptr;
auto filter = SkImageFilters::Blur(radius, radius, input_skia); auto filter = SkImageFilters::Blur(radius_x, radius_y, input_skia);
return Filter(Impl::create(filter)); return Filter(Impl::create(filter));
} }

View file

@ -32,10 +32,10 @@ public:
~Filter(); ~Filter();
static Filter compose(Filter const& outer, Filter const& inner); static Filter compose(Filter const& outer, Filter const& inner);
static Filter blend(Filter const& background, Filter const& foreground, CompositingAndBlendingOperator mode); static Filter blend(Optional<Filter const&> background, Optional<Filter const&> foreground, CompositingAndBlendingOperator mode);
static Filter flood(Gfx::Color color, float opacity); static Filter flood(Gfx::Color color, float opacity);
static Filter drop_shadow(float offset_x, float offset_y, float radius, Gfx::Color color, Optional<Filter const&> input = {}); static Filter drop_shadow(float offset_x, float offset_y, float radius, Gfx::Color color, Optional<Filter const&> input = {});
static Filter blur(float radius, Optional<Filter const&> input = {}); static Filter blur(float radius_x, float radius_y, Optional<Filter const&> input = {});
static Filter color(ColorFilterType type, float amount, Optional<Filter const&> input = {}); static Filter color(ColorFilterType type, float amount, Optional<Filter const&> input = {});
static Filter color_matrix(float matrix[20], Optional<Filter const&> input = {}); static Filter color_matrix(float matrix[20], Optional<Filter const&> input = {});
static Filter saturate(float value, Optional<Filter const&> input = {}); static Filter saturate(float value, Optional<Filter const&> input = {});

View file

@ -28,7 +28,7 @@ public:
bool has_filters() const { return m_filter_value_list; } bool has_filters() const { return m_filter_value_list; }
bool is_none() const { return !has_filters(); } bool is_none() const { return !has_filters(); }
ReadonlySpan<FilterFunction> filters() const ReadonlySpan<FilterValue> filters() const
{ {
VERIFY(has_filters()); VERIFY(has_filters());
return m_filter_value_list->filter_value_list().span(); return m_filter_value_list->filter_value_list().span();

View file

@ -5049,11 +5049,11 @@ RefPtr<CSSStyleValue const> Parser::parse_filter_value_list_value(TokenStream<Co
return {}; return {};
}; };
auto parse_filter_function = [&](auto filter_token, auto const& function_values) -> Optional<FilterFunction> { auto parse_filter_function = [&](auto filter_token, auto const& function_values) -> Optional<FilterValue> {
TokenStream tokens { function_values }; TokenStream tokens { function_values };
tokens.discard_whitespace(); tokens.discard_whitespace();
auto if_no_more_tokens_return = [&](auto filter) -> Optional<FilterFunction> { auto if_no_more_tokens_return = [&](auto filter) -> Optional<FilterValue> {
tokens.discard_whitespace(); tokens.discard_whitespace();
if (tokens.has_next_token()) if (tokens.has_next_token())
return {}; return {};
@ -5149,12 +5149,19 @@ RefPtr<CSSStyleValue const> Parser::parse_filter_value_list_value(TokenStream<Co
} }
}; };
Vector<FilterFunction> filter_value_list {}; Vector<FilterValue> filter_value_list {};
while (tokens.has_next_token()) { while (tokens.has_next_token()) {
tokens.discard_whitespace(); tokens.discard_whitespace();
if (!tokens.has_next_token()) if (!tokens.has_next_token())
break; break;
auto url_function = parse_url_function(tokens);
if (url_function.has_value()) {
filter_value_list.append(*url_function);
continue;
}
auto& token = tokens.consume_a_token(); auto& token = tokens.consume_a_token();
if (!token.is_function()) if (!token.is_function())
return nullptr; return nullptr;

View file

@ -98,6 +98,9 @@ String FilterValueListStyleValue::to_string(SerializationMode) const
}()); }());
builder.append(color.amount.to_string()); builder.append(color.amount.to_string());
},
[&](CSS::URL const& url) {
builder.append(url.to_string());
}); });
builder.append(')'); builder.append(')');
first = false; first = false;

View file

@ -15,6 +15,7 @@
#include <LibWeb/CSS/Length.h> #include <LibWeb/CSS/Length.h>
#include <LibWeb/CSS/Number.h> #include <LibWeb/CSS/Number.h>
#include <LibWeb/CSS/PercentageOr.h> #include <LibWeb/CSS/PercentageOr.h>
#include <LibWeb/CSS/URL.h>
namespace Web::CSS { namespace Web::CSS {
@ -53,18 +54,18 @@ struct Color {
}; };
using FilterFunction = Variant<FilterOperation::Blur, FilterOperation::DropShadow, FilterOperation::HueRotate, FilterOperation::Color>; using FilterValue = Variant<FilterOperation::Blur, FilterOperation::DropShadow, FilterOperation::HueRotate, FilterOperation::Color, URL>;
class FilterValueListStyleValue final : public StyleValueWithDefaultOperators<FilterValueListStyleValue> { class FilterValueListStyleValue final : public StyleValueWithDefaultOperators<FilterValueListStyleValue> {
public: public:
static ValueComparingNonnullRefPtr<FilterValueListStyleValue const> create( static ValueComparingNonnullRefPtr<FilterValueListStyleValue const> create(
Vector<FilterFunction> filter_value_list) Vector<FilterValue> filter_value_list)
{ {
VERIFY(filter_value_list.size() >= 1); VERIFY(filter_value_list.size() >= 1);
return adopt_ref(*new (nothrow) FilterValueListStyleValue(move(filter_value_list))); return adopt_ref(*new (nothrow) FilterValueListStyleValue(move(filter_value_list)));
} }
Vector<FilterFunction> const& filter_value_list() const { return m_filter_value_list; } Vector<FilterValue> const& filter_value_list() const { return m_filter_value_list; }
virtual String to_string(SerializationMode) const override; virtual String to_string(SerializationMode) const override;
@ -73,14 +74,14 @@ public:
bool properties_equal(FilterValueListStyleValue const& other) const { return m_filter_value_list == other.m_filter_value_list; } bool properties_equal(FilterValueListStyleValue const& other) const { return m_filter_value_list == other.m_filter_value_list; }
private: private:
FilterValueListStyleValue(Vector<FilterFunction> filter_value_list) FilterValueListStyleValue(Vector<FilterValue> filter_value_list)
: StyleValueWithDefaultOperators(Type::FilterValueList) : StyleValueWithDefaultOperators(Type::FilterValueList)
, m_filter_value_list(move(filter_value_list)) , m_filter_value_list(move(filter_value_list))
{ {
} }
// FIXME: No support for SVG filters yet // FIXME: No support for SVG filters yet
Vector<FilterFunction> m_filter_value_list; Vector<FilterValue> m_filter_value_list;
}; };
} }

View file

@ -1049,7 +1049,7 @@ void CanvasRenderingContext2D::set_filter(String filter)
item.visit( item.visit(
[&](CSS::FilterOperation::Blur const& blur_filter) { [&](CSS::FilterOperation::Blur const& blur_filter) {
float radius = blur_filter.resolved_radius(*layout_node); float radius = blur_filter.resolved_radius(*layout_node);
auto new_filter = Gfx::Filter::blur(radius); auto new_filter = Gfx::Filter::blur(radius, radius);
drawing_state().filter = drawing_state().filter.has_value() drawing_state().filter = drawing_state().filter.has_value()
? Gfx::Filter::compose(new_filter, *drawing_state().filter) ? Gfx::Filter::compose(new_filter, *drawing_state().filter)
@ -1093,6 +1093,11 @@ void CanvasRenderingContext2D::set_filter(String filter)
drawing_state().filter = drawing_state().filter.has_value() drawing_state().filter = drawing_state().filter.has_value()
? Gfx::Filter::compose(new_filter, *drawing_state().filter) ? Gfx::Filter::compose(new_filter, *drawing_state().filter)
: new_filter; : new_filter;
},
[&](CSS::URL const& url) {
(void)url;
// FIXME: Resolve the SVG filter
dbgln("FIXME: SVG filters are not implemented for Canvas2D");
}); });
} }

View file

@ -32,6 +32,7 @@
#include <LibWeb/Layout/TextNode.h> #include <LibWeb/Layout/TextNode.h>
#include <LibWeb/Layout/Viewport.h> #include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Page/Page.h> #include <LibWeb/Page/Page.h>
#include <LibWeb/SVG/SVGFilterElement.h>
#include <LibWeb/SVG/SVGForeignObjectElement.h> #include <LibWeb/SVG/SVGForeignObjectElement.h>
namespace Web::Layout { namespace Web::Layout {
@ -591,7 +592,8 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
for (auto const& filter : computed_filter.filters()) { for (auto const& filter : computed_filter.filters()) {
filter.visit( filter.visit(
[&](CSS::FilterOperation::Blur const& blur) { [&](CSS::FilterOperation::Blur const& blur) {
auto new_filter = Gfx::Filter::blur(blur.resolved_radius(*this)); auto resolved_radius = blur.resolved_radius(*this);
auto new_filter = Gfx::Filter::blur(resolved_radius, resolved_radius);
resolved_filter = resolved_filter.has_value() resolved_filter = resolved_filter.has_value()
? Gfx::Filter::compose(new_filter, *resolved_filter) ? Gfx::Filter::compose(new_filter, *resolved_filter)
@ -627,6 +629,39 @@ void NodeWithStyle::apply_style(CSS::ComputedProperties const& computed_style)
resolved_filter = resolved_filter.has_value() resolved_filter = resolved_filter.has_value()
? Gfx::Filter::compose(new_filter, *resolved_filter) ? Gfx::Filter::compose(new_filter, *resolved_filter)
: new_filter; : new_filter;
},
[&](CSS::URL const& css_url) {
// FIXME: This is not the right place to resolve SVG filters. Some filter primitives
// wont work if the filter is referenced before its defined because some parameters
// are passed by CSS property. Ideally they should be resolved in another pass or
// lazily.
auto& url_string = css_url.url();
if (url_string.is_empty() || !url_string.starts_with('#'))
return;
auto fragment_or_error = url_string.substring_from_byte_offset(1);
if (fragment_or_error.is_error())
return;
// FIXME: Support urls that are not only composed of a fragment.
auto maybe_filter = document().get_element_by_id(fragment_or_error.value());
if (!maybe_filter)
return;
if (auto* filter_element = as_if<SVG::SVGFilterElement>(*maybe_filter)) {
Optional<Gfx::Filter> new_filter = filter_element->gfx_filter();
if (!new_filter.has_value())
return;
resolved_filter = resolved_filter.has_value()
? Gfx::Filter::compose(*new_filter, *resolved_filter)
: new_filter;
}
}); });
} }
return resolved_filter; return resolved_filter;

View file

@ -1,11 +1,15 @@
/* /*
* Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org> * Copyright (c) 2025, Jelle Raaijmakers <jelle@ladybird.org>
* Copyright (c) 2025, Lucien Fiorini <lucienfiorini@gmail.com>
* *
* SPDX-License-Identifier: BSD-2-Clause * SPDX-License-Identifier: BSD-2-Clause
*/ */
#include <LibWeb/Bindings/SVGFilterElementPrototype.h> #include <LibWeb/Bindings/SVGFilterElementPrototype.h>
#include <LibWeb/CSS/Parser/Parser.h> #include <LibWeb/CSS/Parser/Parser.h>
#include <LibWeb/SVG/SVGFEBlendElement.h>
#include <LibWeb/SVG/SVGFEFloodElement.h>
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
#include <LibWeb/SVG/SVGFilterElement.h> #include <LibWeb/SVG/SVGFilterElement.h>
namespace Web::SVG { namespace Web::SVG {
@ -23,6 +27,12 @@ void SVGFilterElement::initialize(JS::Realm& realm)
Base::initialize(realm); Base::initialize(realm);
} }
void SVGFilterElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
SVGURIReferenceMixin::visit_edges(visitor);
}
void SVGFilterElement::apply_presentational_hints(GC::Ref<CSS::CascadedProperties> cascaded_properties) const void SVGFilterElement::apply_presentational_hints(GC::Ref<CSS::CascadedProperties> cascaded_properties) const
{ {
Base::apply_presentational_hints(cascaded_properties); Base::apply_presentational_hints(cascaded_properties);
@ -63,6 +73,82 @@ void SVGFilterElement::attribute_changed(FlyString const& name, Optional<String>
m_primitive_units = AttributeParser::parse_units(value.value_or({})); m_primitive_units = AttributeParser::parse_units(value.value_or({}));
} }
Gfx::Filter SVGFilterElement::gfx_filter()
{
HashMap<String, Gfx::Filter> result_map;
Optional<Gfx::Filter> root_filter;
// https://www.w3.org/TR/filter-effects-1/#element-attrdef-filter-primitive-in
auto resolve_input_filter = [&](String const& name) -> Optional<Gfx::Filter> {
// TODO: Add missing ones.
if (name == "SourceGraphic"sv)
return {};
if (name == "SourceAlpha"sv) {
float matrix[20] = {
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 1, 0
};
return Gfx::Filter::color_matrix(matrix);
}
auto filter_from_map = result_map.get(name).copy();
if (filter_from_map.has_value())
return filter_from_map;
return root_filter;
};
for_each_child([&](auto& node) {
if (is<SVGFEFloodElement>(node)) {
auto& flood_primitive = static_cast<SVGFEFloodElement&>(node);
root_filter = Gfx::Filter::flood(flood_primitive.flood_color(), flood_primitive.flood_opacity());
auto result = flood_primitive.result()->base_val();
if (!result.is_empty()) {
result_map.set(result, *root_filter);
}
} else if (is<SVGFEBlendElement>(node)) {
auto& blend_primitive = static_cast<SVGFEBlendElement&>(node);
auto foreground = resolve_input_filter(blend_primitive.in1()->base_val());
auto background = resolve_input_filter(blend_primitive.in2()->base_val());
// FIXME: Actually resolve the blend mode
auto blend_mode = Gfx::CompositingAndBlendingOperator::Normal;
root_filter = Gfx::Filter::blend(background, foreground, blend_mode);
auto result = blend_primitive.result()->base_val();
if (!result.is_empty()) {
result_map.set(result, *root_filter);
}
} else if (is<SVGFEGaussianBlurElement>(node)) {
auto& blur_primitive = static_cast<SVGFEGaussianBlurElement&>(node);
auto input = resolve_input_filter(blur_primitive.in1()->base_val());
auto radius_x = blur_primitive.std_deviation_x()->base_val();
auto radius_y = blur_primitive.std_deviation_y()->base_val();
root_filter = Gfx::Filter::blur(radius_x, radius_y, input);
auto result = blur_primitive.result()->base_val();
if (!result.is_empty()) {
result_map.set(result, *root_filter);
}
}
return IterationDecision::Continue;
});
return *root_filter;
}
// https://drafts.fxtf.org/filter-effects/#element-attrdef-filter-filterunits // https://drafts.fxtf.org/filter-effects/#element-attrdef-filter-filterunits
GC::Ref<SVGAnimatedEnumeration> SVGFilterElement::filter_units() const GC::Ref<SVGAnimatedEnumeration> SVGFilterElement::filter_units() const
{ {

View file

@ -6,6 +6,7 @@
#pragma once #pragma once
#include <LibGfx/Filter.h>
#include <LibWeb/SVG/AttributeParser.h> #include <LibWeb/SVG/AttributeParser.h>
#include <LibWeb/SVG/SVGAnimatedEnumeration.h> #include <LibWeb/SVG/SVGAnimatedEnumeration.h>
#include <LibWeb/SVG/SVGAnimatedLength.h> #include <LibWeb/SVG/SVGAnimatedLength.h>
@ -30,6 +31,8 @@ public:
virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override; virtual void attribute_changed(FlyString const& name, Optional<String> const& old_value, Optional<String> const& value, Optional<FlyString> const& namespace_) override;
Gfx::Filter gfx_filter();
GC::Ref<SVGAnimatedEnumeration> filter_units() const; GC::Ref<SVGAnimatedEnumeration> filter_units() const;
GC::Ref<SVGAnimatedEnumeration> primitive_units() const; GC::Ref<SVGAnimatedEnumeration> primitive_units() const;
GC::Ref<SVGAnimatedLength> x() const; GC::Ref<SVGAnimatedLength> x() const;
@ -41,6 +44,7 @@ private:
SVGFilterElement(DOM::Document&, DOM::QualifiedName); SVGFilterElement(DOM::Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override; virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
Optional<SVGUnits> m_filter_units {}; Optional<SVGUnits> m_filter_units {};
Optional<SVGUnits> m_primitive_units {}; Optional<SVGUnits> m_primitive_units {};