Skip to content

Commit b4810f4

Browse files
committed
LibWeb: Hook up SVG component transfer filter to Skia
1 parent 70e98e7 commit b4810f4

File tree

8 files changed

+264
-1
lines changed

8 files changed

+264
-1
lines changed

Libraries/LibGfx/Filter.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,29 @@ Filter Filter::color_matrix(float matrix[20], Optional<Filter const&> input)
195195
return Filter(Impl::create(SkImageFilters::ColorFilter(SkColorFilters::Matrix(matrix), input_skia)));
196196
}
197197

198+
Filter Filter::color_table(Optional<ReadonlyBytes> a, Optional<ReadonlyBytes> r, Optional<ReadonlyBytes> g,
199+
Optional<ReadonlyBytes> b, Optional<Filter const&> input)
200+
{
201+
VERIFY(!a.has_value() || a->size() == 256);
202+
VERIFY(!r.has_value() || r->size() == 256);
203+
VERIFY(!g.has_value() || g->size() == 256);
204+
VERIFY(!b.has_value() || b->size() == 256);
205+
206+
sk_sp<SkImageFilter> input_skia = input.has_value() ? input->m_impl->filter : nullptr;
207+
208+
auto* a_table = a.has_value() ? a->data() : nullptr;
209+
auto* r_table = r.has_value() ? r->data() : nullptr;
210+
auto* g_table = g.has_value() ? g->data() : nullptr;
211+
auto* b_table = b.has_value() ? b->data() : nullptr;
212+
213+
// Color tables are applied in linear space by default, so we need to convert twice.
214+
// FIXME: support sRGB space as well (i.e. don't perform these conversions).
215+
auto srgb_to_linear = SkImageFilters::ColorFilter(SkColorFilters::SRGBToLinearGamma(), input_skia);
216+
auto color_table = SkImageFilters::ColorFilter(SkColorFilters::TableARGB(a_table, r_table, g_table, b_table), srgb_to_linear);
217+
auto linear_to_srgb = SkImageFilters::ColorFilter(SkColorFilters::LinearToSRGBGamma(), color_table);
218+
return Filter(Impl::create(linear_to_srgb));
219+
}
220+
198221
Filter Filter::saturate(float value, Optional<Filter const&> input)
199222
{
200223
sk_sp<SkImageFilter> input_skia = input.has_value() ? input->m_impl->filter : nullptr;

Libraries/LibGfx/Filter.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class Filter {
4242
static Filter blur(float radius_x, float radius_y, Optional<Filter const&> input = {});
4343
static Filter color(ColorFilterType type, float amount, Optional<Filter const&> input = {});
4444
static Filter color_matrix(float matrix[20], Optional<Filter const&> input = {});
45+
static Filter color_table(Optional<ReadonlyBytes> a, Optional<ReadonlyBytes> r, Optional<ReadonlyBytes> g, Optional<ReadonlyBytes> b, Optional<Filter const&> input = {});
4546
static Filter saturate(float value, Optional<Filter const&> input = {});
4647
static Filter hue_rotate(float angle_degrees, Optional<Filter const&> input = {});
4748
static Filter image(Gfx::ImmutableBitmap const& bitmap, Gfx::IntRect const& src_rect, Gfx::IntRect const& dest_rect, Gfx::ScalingMode scaling_mode);

Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.cpp

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ void SVGComponentTransferFunctionElement::attribute_changed(FlyString const& nam
4747
// FIXME: Support reflection instead of invalidating the list.
4848
if (name == AttributeNames::tableValues)
4949
m_table_values = {};
50+
51+
// Clear our cached color table on any attribute change.
52+
m_cached_color_table.clear();
5053
}
5154

5255
void SVGComponentTransferFunctionElement::initialize(JS::Realm& realm)
@@ -144,4 +147,117 @@ SVGComponentTransferFunctionElement::Type SVGComponentTransferFunctionElement::t
144147
return parse_type(get_attribute_value(AttributeNames::type));
145148
}
146149

150+
Vector<float> SVGComponentTransferFunctionElement::table_float_values()
151+
{
152+
Vector<float> values;
153+
auto table_numbers = table_values()->base_val()->items();
154+
values.ensure_capacity(table_numbers.size());
155+
for (auto& svg_number : table_numbers)
156+
values.unchecked_append(svg_number->value());
157+
return values;
158+
}
159+
160+
// https://drafts.fxtf.org/filter-effects/#element-attrdef-fecomponenttransfer-type
161+
ReadonlyBytes SVGComponentTransferFunctionElement::color_table()
162+
{
163+
if (m_cached_color_table.has_value())
164+
return m_cached_color_table.value();
165+
166+
ByteBuffer result;
167+
result.resize(256);
168+
169+
auto set_identity = [&result] {
170+
for (int i = 0; i < 256; ++i)
171+
result.data()[i] = i;
172+
};
173+
auto to_u8 = [](float value) {
174+
return AK::clamp_to<u8>(value * 255.f);
175+
};
176+
177+
switch (type_from_attribute()) {
178+
// https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-identity
179+
case Type::Unknown:
180+
case Type::Identity:
181+
set_identity();
182+
break;
183+
184+
// https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-table
185+
case Type::Table: {
186+
auto table_values = table_float_values();
187+
188+
// An empty list results in an identity transfer function.
189+
if (table_values.is_empty()) {
190+
set_identity();
191+
break;
192+
}
193+
194+
// For a value C < 1 find k such that: k/n <= C < (k+1)/n
195+
// The result C' is given by: C' = vk + (C - k/n)*n * (vk+1 - vk)
196+
auto const segments = table_values.size() - 1.f;
197+
for (int i = 0; i < 256; ++i) {
198+
// If C = 1 then: C' = vn.
199+
if (i == 255 || segments == 0.f) {
200+
result.data()[i] = to_u8(table_values.last());
201+
continue;
202+
}
203+
204+
auto offset = i / 255.f;
205+
auto segment_index = static_cast<size_t>(offset * segments);
206+
auto segment_start = segment_index / segments;
207+
auto offset_in_segment = offset - segment_start;
208+
auto segment_length = 1.f / segments;
209+
auto progress_in_segment = offset_in_segment / segment_length;
210+
211+
auto segment_value = mix(table_values[segment_index], table_values[segment_index + 1], progress_in_segment);
212+
result.data()[i] = to_u8(segment_value);
213+
}
214+
break;
215+
}
216+
217+
// https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-discrete
218+
case Type::Discrete: {
219+
auto table_values = table_float_values();
220+
221+
// An empty list results in an identity transfer function.
222+
if (table_values.is_empty()) {
223+
set_identity();
224+
break;
225+
}
226+
227+
// For a value C < 1 find k such that: k/n <= C < (k+1)/n
228+
// The result C' is given by: C' = vk + (C - k/n)*n * (vk+1 - vk)
229+
for (int i = 0; i < 255; ++i) {
230+
auto index = static_cast<size_t>(i / 255.f * table_values.size());
231+
result.data()[i] = to_u8(table_values[index]);
232+
}
233+
234+
// If C = 1 then: C' = vn.
235+
result.data()[255] = to_u8(table_values.last());
236+
break;
237+
}
238+
239+
// https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-linear
240+
case Type::Linear: {
241+
auto slope = this->slope()->base_val();
242+
auto intercept = this->intercept()->base_val();
243+
for (int i = 0; i < 256; ++i)
244+
result.data()[i] = to_u8(slope * i / 255.f + intercept);
245+
break;
246+
}
247+
248+
// https://drafts.fxtf.org/filter-effects/#attr-valuedef-type-gamma
249+
case Type::Gamma: {
250+
auto amplitude = this->amplitude()->base_val();
251+
auto exponent = this->exponent()->base_val();
252+
auto offset = this->offset()->base_val();
253+
for (int i = 0; i < 256; ++i)
254+
result.data()[i] = to_u8(amplitude * pow(i / 255.f, exponent) + offset);
255+
break;
256+
}
257+
}
258+
259+
m_cached_color_table = move(result);
260+
return m_cached_color_table.value();
261+
}
262+
147263
}

Libraries/LibWeb/SVG/SVGComponentTransferFunctionElement.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
#pragma once
88

9+
#include <AK/ByteBuffer.h>
10+
#include <AK/Optional.h>
911
#include <LibWeb/SVG/SVGAnimatedEnumeration.h>
1012
#include <LibWeb/SVG/SVGAnimatedNumber.h>
1113
#include <LibWeb/SVG/SVGAnimatedNumberList.h>
@@ -40,6 +42,9 @@ class SVGComponentTransferFunctionElement
4042
GC::Ref<SVGAnimatedNumber> exponent();
4143
GC::Ref<SVGAnimatedNumber> offset();
4244

45+
Vector<float> table_float_values();
46+
ReadonlyBytes color_table();
47+
4348
protected:
4449
SVGComponentTransferFunctionElement(DOM::Document&, DOM::QualifiedName);
4550

@@ -57,6 +62,8 @@ class SVGComponentTransferFunctionElement
5762
GC::Ptr<SVGAnimatedNumber> m_amplitude;
5863
GC::Ptr<SVGAnimatedNumber> m_exponent;
5964
GC::Ptr<SVGAnimatedNumber> m_offset;
65+
66+
Optional<ByteBuffer> m_cached_color_table;
6067
};
6168

6269
}

Libraries/LibWeb/SVG/SVGFilterElement.cpp

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@
1313
#include <LibWeb/DOM/Text.h>
1414
#include <LibWeb/Layout/Node.h>
1515
#include <LibWeb/Painting/PaintableBox.h>
16+
#include <LibWeb/SVG/SVGComponentTransferFunctionElement.h>
1617
#include <LibWeb/SVG/SVGFEBlendElement.h>
1718
#include <LibWeb/SVG/SVGFEColorMatrixElement.h>
1819
#include <LibWeb/SVG/SVGFEComponentTransferElement.h>
1920
#include <LibWeb/SVG/SVGFECompositeElement.h>
2021
#include <LibWeb/SVG/SVGFEFloodElement.h>
22+
#include <LibWeb/SVG/SVGFEFuncAElement.h>
23+
#include <LibWeb/SVG/SVGFEFuncBElement.h>
24+
#include <LibWeb/SVG/SVGFEFuncGElement.h>
25+
#include <LibWeb/SVG/SVGFEFuncRElement.h>
2126
#include <LibWeb/SVG/SVGFEGaussianBlurElement.h>
2227
#include <LibWeb/SVG/SVGFEImageElement.h>
2328
#include <LibWeb/SVG/SVGFEMergeElement.h>
@@ -101,7 +106,33 @@ Optional<Gfx::Filter> SVGFilterElement::gfx_filter(Layout::NodeWithStyle const&
101106
root_filter = Gfx::Filter::blend(background, foreground, blend_mode);
102107
update_result_map(*blend_primitive);
103108
} else if (auto* component_transfer = as_if<SVGFEComponentTransferElement>(node)) {
104-
dbgln("FIXME: Implement support for SVGFEComponentTransferElement");
109+
auto input = resolve_input_filter(component_transfer->in1()->base_val());
110+
111+
// https://drafts.fxtf.org/filter-effects/#feComponentTransferElement
112+
// * If more than one transfer function element of the same kind is specified, the last occurrence is to be
113+
// used.
114+
// * If any of the transfer function elements are unspecified, the feComponentTransfer must be processed as
115+
// if those transfer function elements were specified with their type attributes set to identity.
116+
Array<GC::Ptr<SVGComponentTransferFunctionElement>, 4> argb_function_elements;
117+
node.for_each_child([&](auto& child) {
118+
if (auto* func_a = as_if<SVGFEFuncAElement>(child))
119+
argb_function_elements[0] = func_a;
120+
else if (auto* func_r = as_if<SVGFEFuncRElement>(child))
121+
argb_function_elements[1] = func_r;
122+
else if (auto* func_g = as_if<SVGFEFuncGElement>(child))
123+
argb_function_elements[2] = func_g;
124+
else if (auto* func_b = as_if<SVGFEFuncBElement>(child))
125+
argb_function_elements[3] = func_b;
126+
return IterationDecision::Continue;
127+
});
128+
129+
root_filter = Gfx::Filter::color_table(
130+
argb_function_elements[0] ? argb_function_elements[0]->color_table() : Optional<ReadonlyBytes> {},
131+
argb_function_elements[1] ? argb_function_elements[1]->color_table() : Optional<ReadonlyBytes> {},
132+
argb_function_elements[2] ? argb_function_elements[2]->color_table() : Optional<ReadonlyBytes> {},
133+
argb_function_elements[3] ? argb_function_elements[3]->color_table() : Optional<ReadonlyBytes> {},
134+
input);
135+
update_result_map(*component_transfer);
105136
} else if (auto* composite_primitive = as_if<SVGFECompositeElement>(node)) {
106137
auto foreground = resolve_input_filter(composite_primitive->in1()->base_val());
107138
auto background = resolve_input_filter(composite_primitive->in2()->base_val());
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<style>
3+
* {
4+
margin: 0;
5+
}
6+
body {
7+
background-color: white;
8+
}
9+
</style>
10+
<img src="../images/svg-gradient-componentTransfer-ref.png">
4.89 KB
Loading
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<!DOCTYPE html>
2+
<link rel="match" href="../expected/svg-gradient-componentTransfer-ref.html" />
3+
<style>
4+
svg {
5+
margin: 5px;
6+
}
7+
</style>
8+
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
9+
<defs>
10+
<linearGradient id="r" x2="0" y2="100%">
11+
<stop offset="0" stop-color="#f00"/>
12+
<stop offset="0.5" stop-color="rgba(0,255,0,0.5)"/>
13+
<stop offset="1" stop-color="#00f"/>
14+
</linearGradient>
15+
<filter id="f">
16+
<feComponentTransfer>
17+
<feFuncR type="identity"></feFuncR>
18+
<feFuncG type="identity"></feFuncG>
19+
<feFuncB type="identity"></feFuncB>
20+
<feFuncA type="identity"></feFuncA>
21+
</feComponentTransfer>
22+
</filter>
23+
</defs>
24+
<rect width="100%" height="100%" fill="url(#r)" filter="url(#f)"/>
25+
</svg>
26+
<script>
27+
const testCases = [
28+
// Empty component transfer, should default to identity
29+
{},
30+
// Explicit identity
31+
{all: {type: "identity"}},
32+
// Regular table
33+
{R: {type: "table", attrs: {tableValues: "0 0.1 1"}}, G: {type: "table", attrs: {tableValues: "1 0.25 0"}}},
34+
// Table with single value (Firefox and Chrome disagree here)
35+
{G: {type: "table", attrs: {tableValues: "1"}}},
36+
// Empty table, should default to identity
37+
{B: {type: "table", attrs: {tableValues: ""}}},
38+
// Regular discrete
39+
{B: {type: "discrete", attrs: {tableValues: "0 0.8 1"}}, A: {type: "linear", attrs: {slope: 2, intercept: .4}}},
40+
// Regular linear
41+
{R: {type: "linear", attrs: {slope: 0.1, intercept: 0.1}}},
42+
// Regular gamma
43+
{all: {type: "gamma", attrs: {amplitude: 2, exponent: 2.5, offset: 0.05}}},
44+
];
45+
46+
const svgTemplate = document.querySelector("svg");
47+
svgTemplate.remove();
48+
49+
const funcs = ["R", "G", "B", "A"];
50+
let i = 0;
51+
for (const testCase of testCases) {
52+
const testSvg = svgTemplate.cloneNode(true);
53+
const filterId = `f-${i}`;
54+
const gradientId = `r-${i++}`;
55+
testSvg.querySelector("linearGradient").setAttribute("id", gradientId);
56+
testSvg.querySelector("filter").setAttribute("id", filterId);
57+
testSvg.querySelector("rect").setAttribute("fill", `url(#${gradientId})`);
58+
testSvg.querySelector("rect").setAttribute("filter", `url(#${filterId})`);
59+
60+
const funcsConfig = Object.fromEntries(funcs.map(func => [func, testCase[func] || testCase.all]));
61+
for (const [func, funcConfig] of Object.entries(funcsConfig)) {
62+
const funcElement = testSvg.querySelector(`feFunc${func}`);
63+
if (funcConfig === undefined) {
64+
funcElement.remove();
65+
continue;
66+
}
67+
funcElement.setAttribute("type", funcConfig.type);
68+
for (const [attrName, attrValue] of Object.entries(funcConfig.attrs || {})) {
69+
funcElement.setAttribute(attrName, attrValue);
70+
}
71+
}
72+
73+
document.body.appendChild(testSvg);
74+
}
75+
</script>

0 commit comments

Comments
 (0)