From 97fa0be16e56993142d3e238a3cdf31139feaf9a Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Thu, 10 Jul 2025 17:13:52 +0200 Subject: [PATCH] LibWeb: Implement SVGAnimatedNumber --- Libraries/LibWeb/SVG/AttributeNames.h | 2 - Libraries/LibWeb/SVG/SVGAnimatedNumber.cpp | 132 +++++++++++++++++- Libraries/LibWeb/SVG/SVGAnimatedNumber.h | 38 +++-- .../LibWeb/SVG/SVGFEGaussianBlurElement.cpp | 25 ++-- .../LibWeb/SVG/SVGFEGaussianBlurElement.h | 6 +- .../LibWeb/SVG/SVGFEGaussianBlurElement.idl | 4 +- Libraries/LibWeb/SVG/SVGGradientElement.cpp | 2 +- Libraries/LibWeb/SVG/SVGStopElement.cpp | 25 ++-- Libraries/LibWeb/SVG/SVGStopElement.h | 9 +- .../SVG/svg-feGaussianBlur-stdDeviation.txt | 8 ++ .../SVG/svg-feGaussianBlur-stdDeviation.html | 31 ++++ 11 files changed, 235 insertions(+), 47 deletions(-) create mode 100644 Tests/LibWeb/Text/expected/SVG/svg-feGaussianBlur-stdDeviation.txt create mode 100644 Tests/LibWeb/Text/input/SVG/svg-feGaussianBlur-stdDeviation.html diff --git a/Libraries/LibWeb/SVG/AttributeNames.h b/Libraries/LibWeb/SVG/AttributeNames.h index 42b4528bd6d..628a6edb5f2 100644 --- a/Libraries/LibWeb/SVG/AttributeNames.h +++ b/Libraries/LibWeb/SVG/AttributeNames.h @@ -79,8 +79,6 @@ namespace Web::SVG::AttributeNames { __ENUMERATE_SVG_ATTRIBUTE(spreadMethod, "spreadMethod") \ __ENUMERATE_SVG_ATTRIBUTE(startOffset, "startOffset") \ __ENUMERATE_SVG_ATTRIBUTE(stdDeviation, "stdDeviation") \ - __ENUMERATE_SVG_ATTRIBUTE(stdDeviationX, "stdDeviationX") \ - __ENUMERATE_SVG_ATTRIBUTE(stdDeviationY, "stdDeviationY") \ __ENUMERATE_SVG_ATTRIBUTE(stitchTiles, "stitchTiles") \ __ENUMERATE_SVG_ATTRIBUTE(stopColor, "stop-color") \ __ENUMERATE_SVG_ATTRIBUTE(stopOpacity, "stop-opacity") \ diff --git a/Libraries/LibWeb/SVG/SVGAnimatedNumber.cpp b/Libraries/LibWeb/SVG/SVGAnimatedNumber.cpp index f619f83be96..d198f9e892c 100644 --- a/Libraries/LibWeb/SVG/SVGAnimatedNumber.cpp +++ b/Libraries/LibWeb/SVG/SVGAnimatedNumber.cpp @@ -1,35 +1,157 @@ /* * Copyright (c) 2023, MacDue + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ #include #include +#include #include namespace Web::SVG { GC_DEFINE_ALLOCATOR(SVGAnimatedNumber); -GC::Ref SVGAnimatedNumber::create(JS::Realm& realm, float base_val, float anim_val) +GC::Ref SVGAnimatedNumber::create(JS::Realm& realm, GC::Ref element, + FlyString reflected_attribute, float initial_value, SupportsSecondValue supports_second_value, + ValueRepresented value_represented) { - return realm.create(realm, base_val, anim_val); + return realm.create(realm, element, move(reflected_attribute), initial_value, + supports_second_value, value_represented); } -SVGAnimatedNumber::SVGAnimatedNumber(JS::Realm& realm, float base_val, float anim_val) +SVGAnimatedNumber::SVGAnimatedNumber(JS::Realm& realm, GC::Ref element, FlyString reflected_attribute, + float initial_value, SupportsSecondValue supports_second_value, ValueRepresented value_represented) : PlatformObject(realm) - , m_base_val(base_val) - , m_anim_val(anim_val) + , m_element(element) + , m_reflected_attribute(move(reflected_attribute)) + , m_initial_value(initial_value) + , m_supports_second_value(supports_second_value) + , m_value_represented(value_represented) { } SVGAnimatedNumber::~SVGAnimatedNumber() = default; +// https://svgwg.org/svg2-draft/types.html#__svg__SVGAnimatedNumber__baseVal +float SVGAnimatedNumber::base_val() const +{ + // On getting baseVal or animVal, the following steps are run: + return get_base_or_anim_value(); +} + +// // https://svgwg.org/svg2-draft/types.html#__svg__SVGAnimatedNumber__baseVal +void SVGAnimatedNumber::set_base_val(float new_value) +{ + // 1. Let value be the value being assigned to baseVal. + auto value = new_value; + + // 2. Let new be a list of numbers. + Vector new_; + + // 3. If the reflected attribute is defined to take an number followed by an optional second number, then: + if (m_supports_second_value == SupportsSecondValue::Yes) { + // 1. Let current be the value of the reflected attribute (using the attribute's initial value if it is not + // present or invalid). + auto current = m_element->get_attribute_value(m_reflected_attribute); + auto current_values = MUST(current.split(' ')); + + // 2. Let first be the first number in current. + auto first = current_values.size() > 0 ? parse_value_or_initial(current_values[0]) : m_initial_value; + + // 3. Let second be the second number in current if it has been explicitly specified, and if not, the implicit + // value as described in the definition of the attribute. + // LB-NOTE: All known usages of specify that a missing second number defaults to the + // value of the first number. + auto second = current_values.size() > 1 && !current_values[1].is_empty() + ? parse_value_or_initial(current_values[1]) + : first; + + // 4. If this SVGAnimatedNumber object reflects the first number, then set first to value. Otherwise, set second + // to value. + if (m_value_represented == ValueRepresented::First) + first = value; + else + second = value; + + // 5. Append first to new. + new_.unchecked_append(first); + + // 6. Append second to new. + new_.unchecked_append(second); + } + + // 4. Otherwise, the reflected attribute is defined to take a single number value. Append value to new. + else { + new_.unchecked_append(value); + } + + // 5. Set the content attribute to a string consisting of each number in new serialized to an implementation + // specific string that, if parsed as an using CSS syntax, would return the value closest to the number + // (given the implementation's supported Precisionreal number precision), joined and separated by a single U+0020 + // SPACE character. + auto new_attribute_value = MUST(String::join(' ', new_)); + m_element->set_attribute_value(m_reflected_attribute, new_attribute_value); +} + +// https://svgwg.org/svg2-draft/types.html#__svg__SVGAnimatedNumber__animVal +float SVGAnimatedNumber::anim_val() const +{ + // On getting baseVal or animVal, the following steps are run: + return get_base_or_anim_value(); +} + +float SVGAnimatedNumber::parse_value_or_initial(StringView number_value) const +{ + auto value = AttributeParser::parse_number_percentage(number_value); + if (!value.has_value()) + return m_initial_value; + return value.release_value().value(); +} + +// https://svgwg.org/svg2-draft/types.html#__svg__SVGAnimatedNumber__baseVal +float SVGAnimatedNumber::get_base_or_anim_value() const +{ + // 1. Let value be the value of the reflected attribute (using the attribute's initial value if it is not present or + // invalid). + auto value = m_element->get_attribute_value(m_reflected_attribute); + + // 2. If the reflected attribute is defined to take an number followed by an optional second number, then: + if (m_supports_second_value == SupportsSecondValue::Yes) { + // 1. If this SVGAnimatedNumber object reflects the first number, then return the first value in value. + auto values = MUST(value.split(' ')); + if (values.is_empty()) + return m_initial_value; + if (m_value_represented == ValueRepresented::First) + return parse_value_or_initial(values[0]); + + // 2. Otherwise, this SVGAnimatedNumber object reflects the second number. Return the second value in value if + // it has been explicitly specified, and if not, return the implicit value as described in the definition of + // the attribute. + // LB-NOTE: All known usages of specify that a missing second number defaults to the + // value of the first number. + VERIFY(m_value_represented == ValueRepresented::Second); + if (values.size() > 1 && !values[1].is_empty()) + return parse_value_or_initial(values[1]); + return parse_value_or_initial(values[0]); + } + + // 3. Otherwise, the reflected attribute is defined to take a single number value. Return value. + return parse_value_or_initial(value); +} + void SVGAnimatedNumber::initialize(JS::Realm& realm) { WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGAnimatedNumber); Base::initialize(realm); } +void SVGAnimatedNumber::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_element); +} + } diff --git a/Libraries/LibWeb/SVG/SVGAnimatedNumber.h b/Libraries/LibWeb/SVG/SVGAnimatedNumber.h index 8d29ad31a67..2f74f4dbeef 100644 --- a/Libraries/LibWeb/SVG/SVGAnimatedNumber.h +++ b/Libraries/LibWeb/SVG/SVGAnimatedNumber.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2023, MacDue + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -7,32 +8,49 @@ #pragma once #include -#include +#include namespace Web::SVG { -// https://www.w3.org/TR/SVG11/types.html#InterfaceSVGAnimatedNumber +// https://svgwg.org/svg2-draft/types.html#InterfaceSVGAnimatedNumber class SVGAnimatedNumber final : public Bindings::PlatformObject { WEB_PLATFORM_OBJECT(SVGAnimatedNumber, Bindings::PlatformObject); GC_DECLARE_ALLOCATOR(SVGAnimatedNumber); public: - [[nodiscard]] static GC::Ref create(JS::Realm&, float base_val, float anim_val); + enum class SupportsSecondValue : u8 { + Yes, + No, + }; + enum class ValueRepresented : u8 { + First, + Second, + }; + + [[nodiscard]] static GC::Ref create(JS::Realm&, GC::Ref, + FlyString reflected_attribute, float initial_value, SupportsSecondValue = SupportsSecondValue::No, + ValueRepresented = ValueRepresented::First); virtual ~SVGAnimatedNumber() override; - float base_val() const { return m_base_val; } - float anim_val() const { return m_anim_val; } + float base_val() const; + void set_base_val(float); - void set_base_val(float base_val) { m_base_val = base_val; } - void set_anim_val(float anim_val) { m_anim_val = anim_val; } + float anim_val() const; private: - SVGAnimatedNumber(JS::Realm&, float base_val, float anim_val); + SVGAnimatedNumber(JS::Realm&, GC::Ref, FlyString, float, SupportsSecondValue, ValueRepresented); virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Visitor&) override; - float m_base_val; - float m_anim_val; + float parse_value_or_initial(StringView) const; + float get_base_or_anim_value() const; + + GC::Ref m_element; + FlyString m_reflected_attribute; + float m_initial_value; + SupportsSecondValue m_supports_second_value; + ValueRepresented m_value_represented; }; } diff --git a/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.cpp b/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.cpp index 11c5b5c77a0..85e784a7413 100644 --- a/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.cpp +++ b/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.cpp @@ -4,11 +4,10 @@ * SPDX-License-Identifier: BSD-2-Clause */ -#include "SVGAnimatedEnumeration.h" - #include #include #include +#include #include namespace Web::SVG { @@ -31,6 +30,8 @@ void SVGFEGaussianBlurElement::visit_edges(Cell::Visitor& visitor) Base::visit_edges(visitor); SVGFilterPrimitiveStandardAttributes::visit_edges(visitor); visitor.visit(m_in1); + visitor.visit(m_std_deviation_x); + visitor.visit(m_std_deviation_y); } GC::Ref SVGFEGaussianBlurElement::in1() @@ -41,16 +42,24 @@ GC::Ref SVGFEGaussianBlurElement::in1() return *m_in1; } -GC::Ref SVGFEGaussianBlurElement::std_deviation_x() const +// https://drafts.fxtf.org/filter-effects/#element-attrdef-fegaussianblur-stddeviation +GC::Ref SVGFEGaussianBlurElement::std_deviation_x() { - // FIXME: Resolve the actual value from AttributeNames::stdDeviationX. - return SVGAnimatedNumber::create(realm(), 125.0f, 125.0f); + if (!m_std_deviation_x) { + m_std_deviation_x = SVGAnimatedNumber::create(realm(), *this, AttributeNames::stdDeviation, 0.f, + SVGAnimatedNumber::SupportsSecondValue::Yes, SVGAnimatedNumber::ValueRepresented::First); + } + return *m_std_deviation_x; } -GC::Ref SVGFEGaussianBlurElement::std_deviation_y() const +// https://drafts.fxtf.org/filter-effects/#element-attrdef-fegaussianblur-stddeviation +GC::Ref SVGFEGaussianBlurElement::std_deviation_y() { - // FIXME: Resolve the actual value from AttributeNames::stdDeviationY. - return SVGAnimatedNumber::create(realm(), 125.0f, 125.0f); + if (!m_std_deviation_y) { + m_std_deviation_y = SVGAnimatedNumber::create(realm(), *this, AttributeNames::stdDeviation, 0.f, + SVGAnimatedNumber::SupportsSecondValue::Yes, SVGAnimatedNumber::ValueRepresented::Second); + } + return *m_std_deviation_y; } GC::Ref SVGFEGaussianBlurElement::edge_mode() const diff --git a/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.h b/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.h index 4c91af40f59..c38c804c9aa 100644 --- a/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.h +++ b/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.h @@ -22,8 +22,8 @@ public: virtual ~SVGFEGaussianBlurElement() override = default; GC::Ref in1(); - GC::Ref std_deviation_x() const; - GC::Ref std_deviation_y() const; + GC::Ref std_deviation_x(); + GC::Ref std_deviation_y(); GC::Ref edge_mode() const; private: @@ -33,6 +33,8 @@ private: virtual void visit_edges(Cell::Visitor&) override; GC::Ptr m_in1; + GC::Ptr m_std_deviation_x; + GC::Ptr m_std_deviation_y; }; } diff --git a/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.idl b/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.idl index 1886e5096b6..6df9c9c749f 100644 --- a/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.idl +++ b/Libraries/LibWeb/SVG/SVGFEGaussianBlurElement.idl @@ -1,12 +1,14 @@ #import +#import #import +#import #import // https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEGaussianBlurElement [Exposed=Window] interface SVGFEGaussianBlurElement : SVGElement { - // Edge Mode Values + // Edge Mode Values const unsigned short SVG_EDGEMODE_UNKNOWN = 0; const unsigned short SVG_EDGEMODE_DUPLICATE = 1; const unsigned short SVG_EDGEMODE_WRAP = 2; diff --git a/Libraries/LibWeb/SVG/SVGGradientElement.cpp b/Libraries/LibWeb/SVG/SVGGradientElement.cpp index fe862012307..699f6b9ee38 100644 --- a/Libraries/LibWeb/SVG/SVGGradientElement.cpp +++ b/Libraries/LibWeb/SVG/SVGGradientElement.cpp @@ -108,7 +108,7 @@ void SVGGradientElement::add_color_stops(Painting::SVGGradientPaintStyle& paint_ // https://svgwg.org/svg2-draft/pservers.html#StopNotes // Gradient offset values less than 0 (or less than 0%) are rounded up to 0%. // Gradient offset values greater than 1 (or greater than 100%) are rounded down to 100%. - float stop_offset = AK::clamp(stop.stop_offset().value(), 0.0f, 1.0f); + float stop_offset = AK::clamp(stop.stop_offset(), 0.0f, 1.0f); // Each gradient offset value is required to be equal to or greater than the previous gradient // stop's offset value. If a given gradient stop's offset value is not equal to or greater than all diff --git a/Libraries/LibWeb/SVG/SVGStopElement.cpp b/Libraries/LibWeb/SVG/SVGStopElement.cpp index 436ddac3639..82a37118b94 100644 --- a/Libraries/LibWeb/SVG/SVGStopElement.cpp +++ b/Libraries/LibWeb/SVG/SVGStopElement.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2023, MacDue + * Copyright (c) 2025, Jelle Raaijmakers * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,7 +10,6 @@ #include #include #include -#include #include namespace Web::SVG { @@ -21,15 +21,6 @@ SVGStopElement::SVGStopElement(DOM::Document& document, DOM::QualifiedName quali { } -void SVGStopElement::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::offset) { - m_offset = AttributeParser::parse_number_percentage(value.value_or(String {})); - } -} - bool SVGStopElement::is_presentational_hint(FlyString const& name) const { if (Base::is_presentational_hint(name)) @@ -69,10 +60,12 @@ float SVGStopElement::stop_opacity() const return 1; } -GC::Ref SVGStopElement::offset() const +// https://svgwg.org/svg2-draft/pservers.html#StopElementOffsetAttribute +GC::Ref SVGStopElement::offset() { - // FIXME: Implement this properly. - return SVGAnimatedNumber::create(realm(), 0, 0); + if (!m_stop_offset) + m_stop_offset = SVGAnimatedNumber::create(realm(), *this, AttributeNames::offset, 0.f); + return *m_stop_offset; } void SVGStopElement::initialize(JS::Realm& realm) @@ -81,4 +74,10 @@ void SVGStopElement::initialize(JS::Realm& realm) Base::initialize(realm); } +void SVGStopElement::visit_edges(Visitor& visitor) +{ + Base::visit_edges(visitor); + visitor.visit(m_stop_offset); +} + } diff --git a/Libraries/LibWeb/SVG/SVGStopElement.h b/Libraries/LibWeb/SVG/SVGStopElement.h index 1560b0868eb..6aa45453a31 100644 --- a/Libraries/LibWeb/SVG/SVGStopElement.h +++ b/Libraries/LibWeb/SVG/SVGStopElement.h @@ -21,14 +21,12 @@ class SVGStopElement final : public SVGElement { public: virtual ~SVGStopElement() override = default; - virtual void attribute_changed(FlyString const& name, Optional const& old_value, Optional const& value, Optional const& namespace_) override; - - GC::Ref offset() const; + GC::Ref offset(); virtual bool is_presentational_hint(FlyString const&) const override; virtual void apply_presentational_hints(GC::Ref) const override; - NumberPercentage stop_offset() const { return m_offset.value_or(NumberPercentage::create_number(0)); } + float stop_offset() { return offset()->base_val(); } Gfx::Color stop_color() const; float stop_opacity() const; @@ -36,8 +34,9 @@ private: SVGStopElement(DOM::Document&, DOM::QualifiedName); virtual void initialize(JS::Realm&) override; + virtual void visit_edges(Visitor&) override; - Optional m_offset; + GC::Ptr m_stop_offset; }; } diff --git a/Tests/LibWeb/Text/expected/SVG/svg-feGaussianBlur-stdDeviation.txt b/Tests/LibWeb/Text/expected/SVG/svg-feGaussianBlur-stdDeviation.txt new file mode 100644 index 00000000000..bc5b6dccb71 --- /dev/null +++ b/Tests/LibWeb/Text/expected/SVG/svg-feGaussianBlur-stdDeviation.txt @@ -0,0 +1,8 @@ +gb1 stdDeviationX: 125 stdDeviationY: 125 +gb2 stdDeviationX: 50 stdDeviationY: 100.0999984741211 +gb3 stdDeviationX: 0 stdDeviationY: 0 +gb4 stdDeviationX: 0 stdDeviationY: 0 +gb5 stdDeviationX: 1 stdDeviationY: 2 +gm stdDeviationX: 50 stdDeviationY: 0 +gm stdDeviationX: 50 stdDeviationY: 3 +gm stdDeviationX: 2 stdDeviationY: 0 diff --git a/Tests/LibWeb/Text/input/SVG/svg-feGaussianBlur-stdDeviation.html b/Tests/LibWeb/Text/input/SVG/svg-feGaussianBlur-stdDeviation.html new file mode 100644 index 00000000000..98bcc7b9bdf --- /dev/null +++ b/Tests/LibWeb/Text/input/SVG/svg-feGaussianBlur-stdDeviation.html @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + +