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 00000000000..34e5223a7c8 Binary files /dev/null and b/Tests/LibWeb/Screenshot/images/svg-gradient-componentTransfer-ref.png differ 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 @@ + + + + + + + + + + + + + + + + + + + + + +