From b4810f47a3f48a1b976a88edf115ddb3d0c44f45 Mon Sep 17 00:00:00 2001 From: Jelle Raaijmakers Date: Thu, 6 Nov 2025 15:50:45 +0100 Subject: [PATCH] LibWeb: Hook up SVG component transfer filter to Skia --- Libraries/LibGfx/Filter.cpp | 23 ++++ Libraries/LibGfx/Filter.h | 1 + .../SVGComponentTransferFunctionElement.cpp | 116 ++++++++++++++++++ .../SVG/SVGComponentTransferFunctionElement.h | 7 ++ Libraries/LibWeb/SVG/SVGFilterElement.cpp | 33 ++++- .../svg-gradient-componentTransfer-ref.html | 10 ++ .../svg-gradient-componentTransfer-ref.png | Bin 0 -> 5006 bytes .../input/svg-gradient-componentTransfer.html | 75 +++++++++++ 8 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 Tests/LibWeb/Screenshot/expected/svg-gradient-componentTransfer-ref.html create mode 100644 Tests/LibWeb/Screenshot/images/svg-gradient-componentTransfer-ref.png create mode 100644 Tests/LibWeb/Screenshot/input/svg-gradient-componentTransfer.html diff --git a/Libraries/LibGfx/Filter.cpp b/Libraries/LibGfx/Filter.cpp index 833f018fa9b..c0cee62622c 100644 --- a/Libraries/LibGfx/Filter.cpp +++ b/Libraries/LibGfx/Filter.cpp @@ -195,6 +195,29 @@ Filter Filter::color_matrix(float matrix[20], Optional input) return Filter(Impl::create(SkImageFilters::ColorFilter(SkColorFilters::Matrix(matrix), input_skia))); } +Filter Filter::color_table(Optional a, Optional r, Optional g, + Optional b, Optional input) +{ + VERIFY(!a.has_value() || a->size() == 256); + VERIFY(!r.has_value() || r->size() == 256); + VERIFY(!g.has_value() || g->size() == 256); + VERIFY(!b.has_value() || b->size() == 256); + + sk_sp input_skia = input.has_value() ? input->m_impl->filter : nullptr; + + auto* a_table = a.has_value() ? a->data() : nullptr; + auto* r_table = r.has_value() ? r->data() : nullptr; + auto* g_table = g.has_value() ? g->data() : nullptr; + auto* b_table = b.has_value() ? b->data() : nullptr; + + // Color tables are applied in linear space by default, so we need to convert twice. + // FIXME: support sRGB space as well (i.e. don't perform these conversions). + auto srgb_to_linear = SkImageFilters::ColorFilter(SkColorFilters::SRGBToLinearGamma(), input_skia); + auto color_table = SkImageFilters::ColorFilter(SkColorFilters::TableARGB(a_table, r_table, g_table, b_table), srgb_to_linear); + auto linear_to_srgb = SkImageFilters::ColorFilter(SkColorFilters::LinearToSRGBGamma(), color_table); + return Filter(Impl::create(linear_to_srgb)); +} + Filter Filter::saturate(float value, Optional input) { sk_sp input_skia = input.has_value() ? input->m_impl->filter : nullptr; diff --git a/Libraries/LibGfx/Filter.h b/Libraries/LibGfx/Filter.h index 01e0387ef43..0bca39c7eb3 100644 --- a/Libraries/LibGfx/Filter.h +++ b/Libraries/LibGfx/Filter.h @@ -42,6 +42,7 @@ public: static Filter blur(float radius_x, float radius_y, Optional input = {}); static Filter color(ColorFilterType type, float amount, Optional input = {}); static Filter color_matrix(float matrix[20], Optional input = {}); + static Filter color_table(Optional a, Optional r, Optional g, Optional b, 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); diff --git a/Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.cpp b/Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.cpp index b76883c53c0..5cd457254d6 100644 --- a/Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.cpp +++ b/Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.cpp @@ -47,6 +47,9 @@ void SVGComponentTransferFunctionElement::attribute_changed(FlyString const& nam // FIXME: Support reflection instead of invalidating the list. if (name == AttributeNames::tableValues) m_table_values = {}; + + // Clear our cached color table on any attribute change. + m_cached_color_table.clear(); } void SVGComponentTransferFunctionElement::initialize(JS::Realm& realm) @@ -144,4 +147,117 @@ SVGComponentTransferFunctionElement::Type SVGComponentTransferFunctionElement::t return parse_type(get_attribute_value(AttributeNames::type)); } +Vector SVGComponentTransferFunctionElement::table_float_values() +{ + Vector values; + auto table_numbers = table_values()->base_val()->items(); + values.ensure_capacity(table_numbers.size()); + for (auto& svg_number : table_numbers) + values.unchecked_append(svg_number->value()); + return values; +} + +// https://drafts.fxtf.org/filter-effects/#element-attrdef-fecomponenttransfer-type +ReadonlyBytes SVGComponentTransferFunctionElement::color_table() +{ + if (m_cached_color_table.has_value()) + return m_cached_color_table.value(); + + ByteBuffer result; + result.resize(256); + + auto set_identity = [&result] { + for (int i = 0; i < 256; ++i) + result.data()[i] = i; + }; + auto to_u8 = [](float value) { + return AK::clamp_to(value * 255.f); + }; + + switch (type_from_attribute()) { + // https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-identity + case Type::Unknown: + case Type::Identity: + set_identity(); + break; + + // https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-table + case Type::Table: { + auto table_values = table_float_values(); + + // An empty list results in an identity transfer function. + if (table_values.is_empty()) { + set_identity(); + break; + } + + // For a value C < 1 find k such that: k/n <= C < (k+1)/n + // The result C' is given by: C' = vk + (C - k/n)*n * (vk+1 - vk) + auto const segments = table_values.size() - 1.f; + for (int i = 0; i < 256; ++i) { + // If C = 1 then: C' = vn. + if (i == 255 || segments == 0.f) { + result.data()[i] = to_u8(table_values.last()); + continue; + } + + auto offset = i / 255.f; + auto segment_index = static_cast(offset * segments); + auto segment_start = segment_index / segments; + auto offset_in_segment = offset - segment_start; + auto segment_length = 1.f / segments; + auto progress_in_segment = offset_in_segment / segment_length; + + auto segment_value = mix(table_values[segment_index], table_values[segment_index + 1], progress_in_segment); + result.data()[i] = to_u8(segment_value); + } + break; + } + + // https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-discrete + case Type::Discrete: { + auto table_values = table_float_values(); + + // An empty list results in an identity transfer function. + if (table_values.is_empty()) { + set_identity(); + break; + } + + // For a value C < 1 find k such that: k/n <= C < (k+1)/n + // The result C' is given by: C' = vk + (C - k/n)*n * (vk+1 - vk) + for (int i = 0; i < 255; ++i) { + auto index = static_cast(i / 255.f * table_values.size()); + result.data()[i] = to_u8(table_values[index]); + } + + // If C = 1 then: C' = vn. + result.data()[255] = to_u8(table_values.last()); + break; + } + + // https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-linear + case Type::Linear: { + auto slope = this->slope()->base_val(); + auto intercept = this->intercept()->base_val(); + for (int i = 0; i < 256; ++i) + result.data()[i] = to_u8(slope * i / 255.f + intercept); + break; + } + + // https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-gamma + case Type::Gamma: { + auto amplitude = this->amplitude()->base_val(); + auto exponent = this->exponent()->base_val(); + auto offset = this->offset()->base_val(); + for (int i = 0; i < 256; ++i) + result.data()[i] = to_u8(amplitude * pow(i / 255.f, exponent) + offset); + break; + } + } + + m_cached_color_table = move(result); + return m_cached_color_table.value(); +} + } diff --git a/Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.h b/Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.h index 97832e05035..ca67e68c274 100644 --- a/Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.h +++ b/Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.h @@ -6,6 +6,8 @@ #pragma once +#include +#include #include #include #include @@ -40,6 +42,9 @@ public: GC::Ref exponent(); GC::Ref offset(); + Vector table_float_values(); + ReadonlyBytes color_table(); + protected: SVGComponentTransferFunctionElement(DOM::Document&, DOM::QualifiedName); @@ -57,6 +62,8 @@ private: GC::Ptr m_amplitude; GC::Ptr m_exponent; GC::Ptr m_offset; + + Optional m_cached_color_table; }; } diff --git a/Libraries/LibWeb/SVG/SVGFilterElement.cpp b/Libraries/LibWeb/SVG/SVGFilterElement.cpp index 7d20b39d155..bb8dd394b30 100644 --- a/Libraries/LibWeb/SVG/SVGFilterElement.cpp +++ b/Libraries/LibWeb/SVG/SVGFilterElement.cpp @@ -13,11 +13,16 @@ #include #include #include +#include #include #include #include #include #include +#include +#include +#include +#include #include #include #include @@ -101,7 +106,33 @@ Optional SVGFilterElement::gfx_filter(Layout::NodeWithStyle const& root_filter = Gfx::Filter::blend(background, foreground, blend_mode); update_result_map(*blend_primitive); } else if (auto* component_transfer = as_if(node)) { - dbgln("FIXME: Implement support for SVGFEComponentTransferElement"); + auto input = resolve_input_filter(component_transfer->in1()->base_val()); + + // https://drafts.fxtf.org/filter-effects/#feComponentTransferElement + // * If more than one transfer function element of the same kind is specified, the last occurrence is to be + // used. + // * If any of the transfer function elements are unspecified, the feComponentTransfer must be processed as + // if those transfer function elements were specified with their type attributes set to identity. + Array, 4> argb_function_elements; + node.for_each_child([&](auto& child) { + if (auto* func_a = as_if(child)) + argb_function_elements[0] = func_a; + else if (auto* func_r = as_if(child)) + argb_function_elements[1] = func_r; + else if (auto* func_g = as_if(child)) + argb_function_elements[2] = func_g; + else if (auto* func_b = as_if(child)) + argb_function_elements[3] = func_b; + return IterationDecision::Continue; + }); + + root_filter = Gfx::Filter::color_table( + argb_function_elements[0] ? argb_function_elements[0]->color_table() : Optional {}, + argb_function_elements[1] ? argb_function_elements[1]->color_table() : Optional {}, + argb_function_elements[2] ? argb_function_elements[2]->color_table() : Optional {}, + argb_function_elements[3] ? argb_function_elements[3]->color_table() : Optional {}, + input); + update_result_map(*component_transfer); } else if (auto* composite_primitive = as_if(node)) { auto foreground = resolve_input_filter(composite_primitive->in1()->base_val()); auto background = resolve_input_filter(composite_primitive->in2()->base_val()); diff --git a/Tests/LibWeb/Screenshot/expected/svg-gradient-componentTransfer-ref.html b/Tests/LibWeb/Screenshot/expected/svg-gradient-componentTransfer-ref.html new file mode 100644 index 00000000000..154c84fcc32 --- /dev/null +++ b/Tests/LibWeb/Screenshot/expected/svg-gradient-componentTransfer-ref.html @@ -0,0 +1,10 @@ + + + diff --git a/Tests/LibWeb/Screenshot/images/svg-gradient-componentTransfer-ref.png b/Tests/LibWeb/Screenshot/images/svg-gradient-componentTransfer-ref.png new file mode 100644 index 0000000000000000000000000000000000000000..34e5223a7c80db9b03f2c470dce8863b20b4fc95 GIT binary patch literal 5006 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 Wra{p;c`E$QhH$+6Jd0Li`~L&bC|LRc literal 0 HcmV?d00001 diff --git a/Tests/LibWeb/Screenshot/input/svg-gradient-componentTransfer.html b/Tests/LibWeb/Screenshot/input/svg-gradient-componentTransfer.html new file mode 100644 index 00000000000..f3d263cc190 --- /dev/null +++ b/Tests/LibWeb/Screenshot/input/svg-gradient-componentTransfer.html @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + +