LibWeb: Implement SVGFEColorMatrixElement and feColorMatrix

This commit is contained in:
Pavel Shliak 2025-09-22 02:47:28 +04:00 committed by Jelle Raaijmakers
parent b50b89b4a8
commit 5ee1031b89
Notes: github-actions[bot] 2025-10-23 11:37:46 +00:00
21 changed files with 367 additions and 0 deletions

View file

@ -912,6 +912,7 @@ set(SOURCES
SVG/SVGElement.cpp
SVG/SVGEllipseElement.cpp
SVG/SVGFEBlendElement.cpp
SVG/SVGFEColorMatrixElement.cpp
SVG/SVGFECompositeElement.cpp
SVG/SVGFEFloodElement.cpp
SVG/SVGFEGaussianBlurElement.cpp

View file

@ -94,6 +94,7 @@
#include <LibWeb/SVG/SVGDescElement.h>
#include <LibWeb/SVG/SVGEllipseElement.h>
#include <LibWeb/SVG/SVGFEBlendElement.h>
#include <LibWeb/SVG/SVGFEColorMatrixElement.h>
#include <LibWeb/SVG/SVGFECompositeElement.h>
#include <LibWeb/SVG/SVGFEFloodElement.h>
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
@ -478,6 +479,8 @@ static GC::Ref<SVG::SVGElement> create_svg_element(JS::Realm& realm, Document& d
return realm.create<SVG::SVGFECompositeElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feFlood)
return realm.create<SVG::SVGFEFloodElement>(document, move(qualified_name));
if (local_name == SVG::TagNames::feColorMatrix)
return realm.create<SVG::SVGFEColorMatrixElement>(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)

View file

@ -1098,6 +1098,7 @@ class SVGDescElement;
class SVGElement;
class SVGEllipseElement;
class SVGFEBlendElement;
class SVGFEColorMatrixElement;
class SVGFECompositeElement;
class SVGFEFloodElement;
class SVGFEGaussianBlurElement;

View file

@ -94,6 +94,7 @@ namespace Web::SVG::AttributeNames {
__ENUMERATE_SVG_ATTRIBUTE(targetY, "targetY") \
__ENUMERATE_SVG_ATTRIBUTE(textLength, "textLength") \
__ENUMERATE_SVG_ATTRIBUTE(type, "type") \
__ENUMERATE_SVG_ATTRIBUTE(values, "values") \
__ENUMERATE_SVG_ATTRIBUTE(version, "version") \
__ENUMERATE_SVG_ATTRIBUTE(viewBox, "viewBox") \
__ENUMERATE_SVG_ATTRIBUTE(viewTarget, "viewTarget") \

View file

@ -0,0 +1,69 @@
/*
* Copyright (c) 2025, Pavel Shliak <shlyakpavel@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/Bindings/SVGFEColorMatrixElementPrototype.h>
#include <LibWeb/SVG/SVGAnimatedEnumeration.h>
#include <LibWeb/SVG/SVGAnimatedString.h>
#include <LibWeb/SVG/SVGFEColorMatrixElement.h>
namespace Web::SVG {
GC_DEFINE_ALLOCATOR(SVGFEColorMatrixElement);
SVGFEColorMatrixElement::SVGFEColorMatrixElement(DOM::Document& document, DOM::QualifiedName qualified_name)
: SVGElement(document, qualified_name)
{
}
void SVGFEColorMatrixElement::initialize(JS::Realm& realm)
{
WEB_SET_PROTOTYPE_FOR_INTERFACE(SVGFEColorMatrixElement);
Base::initialize(realm);
}
void SVGFEColorMatrixElement::visit_edges(Cell::Visitor& visitor)
{
Base::visit_edges(visitor);
SVGFilterPrimitiveStandardAttributes::visit_edges(visitor);
visitor.visit(m_in1);
visitor.visit(m_values);
}
GC::Ref<SVGAnimatedString> SVGFEColorMatrixElement::in1()
{
if (!m_in1)
m_in1 = SVGAnimatedString::create(realm(), *this, AttributeNames::in);
return *m_in1;
}
GC::Ref<SVGAnimatedEnumeration> SVGFEColorMatrixElement::type() const
{
// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEColorMatrixElement
// Map the 'type' attribute to the IDL enumeration values.
// Defaults to MATRIX when omitted.
auto type_attribute = attribute(AttributeNames::type).value_or(String {});
u16 enum_value = SVGFEColorMatrixElement::SVG_FECOLORMATRIX_TYPE_UNKNOWN;
if (type_attribute.is_empty() || type_attribute.equals_ignoring_ascii_case("matrix"sv))
enum_value = SVGFEColorMatrixElement::SVG_FECOLORMATRIX_TYPE_MATRIX;
else if (type_attribute.equals_ignoring_ascii_case("saturate"sv))
enum_value = SVGFEColorMatrixElement::SVG_FECOLORMATRIX_TYPE_SATURATE;
else if (type_attribute.equals_ignoring_ascii_case("hueRotate"sv))
enum_value = SVGFEColorMatrixElement::SVG_FECOLORMATRIX_TYPE_HUEROTATE;
else if (type_attribute.equals_ignoring_ascii_case("luminanceToAlpha"sv))
enum_value = SVGFEColorMatrixElement::SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA;
return SVGAnimatedEnumeration::create(realm(), enum_value);
}
GC::Ref<SVGAnimatedString> SVGFEColorMatrixElement::values()
{
if (!m_values)
m_values = SVGAnimatedString::create(realm(), *this, AttributeNames::values);
return *m_values;
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2025, Pavel Shliak <shlyakpavel@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibWeb/SVG/SVGElement.h>
#include <LibWeb/SVG/SVGFilterPrimitiveStandardAttributes.h>
namespace Web::SVG {
class SVGAnimatedEnumeration;
class SVGAnimatedString;
// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEColorMatrixElement
class SVGFEColorMatrixElement final
: public SVGElement
, public SVGFilterPrimitiveStandardAttributes<SVGFEColorMatrixElement> {
WEB_PLATFORM_OBJECT(SVGFEColorMatrixElement, SVGElement);
GC_DECLARE_ALLOCATOR(SVGFEColorMatrixElement);
public:
virtual ~SVGFEColorMatrixElement() override = default;
static constexpr unsigned short SVG_FECOLORMATRIX_TYPE_UNKNOWN = 0;
static constexpr unsigned short SVG_FECOLORMATRIX_TYPE_MATRIX = 1;
static constexpr unsigned short SVG_FECOLORMATRIX_TYPE_SATURATE = 2;
static constexpr unsigned short SVG_FECOLORMATRIX_TYPE_HUEROTATE = 3;
static constexpr unsigned short SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA = 4;
// IDL attributes
GC::Ref<SVGAnimatedString> in1();
GC::Ref<SVGAnimatedEnumeration> type() const;
GC::Ref<SVGAnimatedString> values();
private:
SVGFEColorMatrixElement(DOM::Document&, DOM::QualifiedName);
virtual void initialize(JS::Realm&) override;
virtual void visit_edges(Cell::Visitor&) override;
GC::Ptr<SVGAnimatedString> m_in1;
GC::Ptr<SVGAnimatedString> m_values;
};
}

View file

@ -0,0 +1,23 @@
#import <SVG/SVGAnimatedString.idl>
#import <SVG/SVGAnimatedEnumeration.idl>
#import <SVG/SVGElement.idl>
#import <SVG/SVGFilterPrimitiveStandardAttributes.idl>
// https://www.w3.org/TR/filter-effects-1/#InterfaceSVGFEColorMatrixElement
[Exposed=Window]
interface SVGFEColorMatrixElement : SVGElement {
// Color Matrix Types
const unsigned short SVG_FECOLORMATRIX_TYPE_UNKNOWN = 0;
const unsigned short SVG_FECOLORMATRIX_TYPE_MATRIX = 1;
const unsigned short SVG_FECOLORMATRIX_TYPE_SATURATE = 2;
const unsigned short SVG_FECOLORMATRIX_TYPE_HUEROTATE = 3;
const unsigned short SVG_FECOLORMATRIX_TYPE_LUMINANCETOALPHA = 4;
readonly attribute SVGAnimatedString in1;
readonly attribute SVGAnimatedEnumeration type;
// FIXME: Use SVGAnimatedNumberList when implemented.
readonly attribute SVGAnimatedString values;
};
SVGFEColorMatrixElement includes SVGFilterPrimitiveStandardAttributes;

View file

@ -5,6 +5,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringConversions.h>
#include <LibGfx/ImmutableBitmap.h>
#include <LibWeb/Bindings/SVGFilterElementPrototype.h>
#include <LibWeb/CSS/Parser/Parser.h>
@ -12,6 +13,7 @@
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/SVG/SVGFEBlendElement.h>
#include <LibWeb/SVG/SVGFEColorMatrixElement.h>
#include <LibWeb/SVG/SVGFECompositeElement.h>
#include <LibWeb/SVG/SVGFEFloodElement.h>
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
@ -140,6 +142,83 @@ Optional<Gfx::Filter> SVGFilterElement::gfx_filter(Layout::NodeWithStyle const&
root_filter = Gfx::Filter::blur(radius_x, radius_y, input);
update_result_map(*blur_primitive);
} else if (auto* colormatrix_primitive = as_if<SVGFEColorMatrixElement>(node)) {
auto in_attr = colormatrix_primitive->in1()->base_val();
auto input = resolve_input_filter(in_attr);
auto type_value = colormatrix_primitive->attribute(AttributeNames::type).value_or(String {});
auto values_value = colormatrix_primitive->attribute(AttributeNames::values).value_or(String {});
// Default type is "matrix" per spec.
if (type_value.is_empty() || type_value.equals_ignoring_ascii_case("matrix"sv)) {
// Parse up to 20 numbers; if we don't get a full 4x5, skip applying.
float matrix[20] = { 0 };
size_t count = 0;
StringView sv = values_value;
auto skip_leading_whitespace = [&] {
sv = sv.trim_whitespace(AK::TrimMode::Left);
};
auto consume_comma_and_whitespace = [&] {
if (!sv.is_empty() && sv[0] == ',')
sv = sv.substring_view(1);
skip_leading_whitespace();
};
skip_leading_whitespace();
while (!sv.is_empty() && count < 20) {
// Parse the next number without trimming (we already trimmed on the left).
auto result = AK::parse_first_number<float>(sv, AK::TrimWhitespace::No);
if (!result.has_value())
break;
matrix[count++] = result->value;
// Advance exactly past the number just parsed, then consume optional comma + whitespace.
sv = sv.substring_view(result->characters_parsed);
consume_comma_and_whitespace();
}
if (count == 20) {
root_filter = Gfx::Filter::color_matrix(matrix, input);
update_result_map(*colormatrix_primitive);
} else {
// If invalid or missing, treat as identity (no-op) if we already have an input.
if (input.has_value()) {
root_filter = input;
update_result_map(*colormatrix_primitive);
}
}
} else if (type_value.equals_ignoring_ascii_case("saturate"sv)) {
// values: single number s (1 = original)
float s = 1.0f;
if (!values_value.is_empty()) {
if (auto parsed = AK::parse_number<float>(values_value, AK::TrimWhitespace::Yes); parsed.has_value())
s = *parsed;
}
root_filter = Gfx::Filter::saturate(s, input);
update_result_map(*colormatrix_primitive);
} else if (type_value.equals_ignoring_ascii_case("hueRotate"sv)) {
// values: angle in degrees
float angle_degrees = 0.0f;
if (!values_value.is_empty()) {
if (auto parsed = AK::parse_number<float>(values_value, AK::TrimWhitespace::Yes); parsed.has_value())
angle_degrees = *parsed;
}
root_filter = Gfx::Filter::hue_rotate(angle_degrees, input);
update_result_map(*colormatrix_primitive);
} else if (type_value.equals_ignoring_ascii_case("luminanceToAlpha"sv)) {
// values ignored; convert luminance to alpha and zero RGB.
float matrix[20] = {
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0, 0, 0, 0, 0,
0.2126f, 0.7152f, 0.0722f, 0, 0
};
root_filter = Gfx::Filter::color_matrix(matrix, input);
update_result_map(*colormatrix_primitive);
} else {
// Unknown 'type' value on feColorMatrix; skip creating a filter and log.
dbgln("SVGFEColorMatrixElement: Unknown type '{}' — skipping filter primitive", type_value);
}
} else if (auto* image_primitive = as_if<SVGFEImageElement>(node)) {
auto bitmap = image_primitive->current_image_bitmap({});
if (!bitmap)

View file

@ -18,6 +18,7 @@ namespace Web::SVG::TagNames {
__ENUMERATE_SVG_TAG(desc) \
__ENUMERATE_SVG_TAG(ellipse) \
__ENUMERATE_SVG_TAG(feBlend) \
__ENUMERATE_SVG_TAG(feColorMatrix) \
__ENUMERATE_SVG_TAG(feComposite) \
__ENUMERATE_SVG_TAG(feFlood) \
__ENUMERATE_SVG_TAG(feGaussianBlur) \

View file

@ -388,6 +388,7 @@ libweb_js_bindings(SVG/SVGImageElement)
libweb_js_bindings(SVG/SVGCircleElement)
libweb_js_bindings(SVG/SVGEllipseElement)
libweb_js_bindings(SVG/SVGFEBlendElement)
libweb_js_bindings(SVG/SVGFEColorMatrixElement)
libweb_js_bindings(SVG/SVGFECompositeElement)
libweb_js_bindings(SVG/SVGFEFloodElement)
libweb_js_bindings(SVG/SVGFEGaussianBlurElement)

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<body style="margin:0; background:white">
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- hueRotate(90deg) of #cc3366 -> approx #665F00 -->
<rect x="10" y="10" width="180" height="180" fill="#665F00"/>
</svg>
</body>
</html>

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<body style="margin:0; background:white">
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- luminanceToAlpha of red (alpha=0.2126) over white -> approx #C9C9C9 -->
<rect x="10" y="10" width="180" height="180" fill="#C9C9C9"/>
</svg>
</body>
</html>

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<body style="margin:0; background:white">
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<rect x="10" y="10" width="180" height="180" fill="#336699"/>
</svg>
<!-- Identity matrix should be a no-op. -->
</body>
</html>

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<body style="margin:0; background:white">
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Inverted #336699 -> #CC9966 -->
<rect x="10" y="10" width="180" height="180" fill="#CC9966"/>
</svg>
</body>
</html>

View file

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<body style="margin:0; background:white">
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Approximate saturate(2) of #6d98cc -> #479DFF -->
<rect x="10" y="10" width="180" height="180" fill="#479DFF"/>
</svg>
</body>
</html>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-fecolormatrix-huerotate-ref.html" />
<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-12000">
<style>
html, body { margin: 0; background: white; }
</style>
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="cm_hue">
<feColorMatrix type="hueRotate" values="90"/>
</filter>
</defs>
<g filter="url(#cm_hue)">
<rect x="10" y="10" width="180" height="180" fill="#cc3366"/>
</g>
<!-- Core: feColorMatrix hueRotate in degrees -->
</svg>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-fecolormatrix-luminance-to-alpha-ref.html" />
<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-12000">
<style>
html, body { margin: 0; background: white; }
</style>
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="cm_luma">
<feColorMatrix type="luminanceToAlpha"/>
</filter>
</defs>
<!-- Red square filtered to alpha; will render as black with alpha equal to luminance -->
<g filter="url(#cm_luma)">
<rect x="10" y="10" width="180" height="180" fill="#ff0000"/>
</g>
</svg>

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-fecolormatrix-matrix-identity-ref.html" />
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-10000">
<style>
html, body { margin: 0; background: white; }
</style>
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="cm_identity">
<feColorMatrix type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 1 0"/>
</filter>
</defs>
<g filter="url(#cm_identity)">
<rect x="10" y="10" width="180" height="180" fill="#336699"/>
</g>
Sorry, your browser does not support inline SVG.
<!-- Core: feColorMatrix identity should be a visual no-op. -->
</svg>

View file

@ -0,0 +1,22 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-fecolormatrix-matrix-invert-ref.html" />
<meta name="fuzzy" content="maxDifference=0-1;totalPixels=0-10000">
<style>
html, body { margin: 0; background: white; }
</style>
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="cm_invert">
<!-- Invert: R' = 1 - R, etc. -->
<feColorMatrix type="matrix"
values="-1 0 0 0 1
0 -1 0 0 1
0 0 -1 0 1
0 0 0 1 0"/>
</filter>
</defs>
<g filter="url(#cm_invert)">
<rect x="10" y="10" width="180" height="180" fill="#336699"/>
</g>
<!-- Core: feColorMatrix matrix with bias in normalized 0..1. -->
</svg>

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<link rel="match" href="../expected/svg-fecolormatrix-saturate-ref.html" />
<meta name="fuzzy" content="maxDifference=0-2;totalPixels=0-12000">
<style>
html, body { margin: 0; background: white; }
</style>
<svg width="200" height="200" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="cm_saturate">
<feColorMatrix type="saturate" values="2"/>
</filter>
</defs>
<g filter="url(#cm_saturate)">
<rect x="10" y="10" width="180" height="180" fill="#6d98cc"/>
</g>
<!-- Core: feColorMatrix saturate -->
</svg>

View file

@ -376,6 +376,7 @@ SVGDescElement
SVGElement
SVGEllipseElement
SVGFEBlendElement
SVGFEColorMatrixElement
SVGFECompositeElement
SVGFEFloodElement
SVGFEGaussianBlurElement