LibWeb: Apply visual effects in SVG mask subtrees

Build an accumulated visual context tree when recording SVG mask and
clip subtrees. Mask content can contain filters, opacity, and blending,
but the nested display list previously had an empty visual context tree.
Filtered mask contents therefore painted as if those effects did not
exist.

Resolve filters for the nested paintables and attach their accumulated
visual context before recording the mask display list. Add direct and
SVG use based coverage for alpha-inverting filters inside masks.
This commit is contained in:
Andreas Kling 2026-05-31 12:10:57 +02:00 committed by Andreas Kling
parent 7b159143df
commit 4cb58762df
5 changed files with 114 additions and 1 deletions

View file

@ -4,9 +4,13 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibWeb/DOM/Document.h>
#include <LibWeb/Layout/SVGClipBox.h>
#include <LibWeb/Layout/SVGMaskBox.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/Blending.h>
#include <LibWeb/Painting/DisplayListRecorder.h>
#include <LibWeb/Painting/ResolvedCSSFilter.h>
#include <LibWeb/Painting/SVGClipPaintable.h>
#include <LibWeb/Painting/SVGGraphicsPaintable.h>
#include <LibWeb/Painting/SVGPaintable.h>
@ -68,6 +72,43 @@ static Gfx::MaskKind mask_type_to_gfx_mask_kind(CSS::MaskType mask_type)
}
}
static void build_nested_svg_visual_context_tree_for_subtree(AccumulatedVisualContextTree& visual_context_tree, PaintableBox& paintable_box, VisualContextIndex inherited_state, float pixel_ratio)
{
auto const& computed_values = paintable_box.computed_values();
if (computed_values.filter().has_filters())
paintable_box.set_filter(resolve_css_filter(computed_values.filter(), paintable_box));
else
paintable_box.set_filter({});
auto gfx_filter = to_gfx_filter(paintable_box.filter(), pixel_ratio);
EffectsData effects {
computed_values.opacity(),
mix_blend_mode_to_compositing_and_blending_operator(computed_values.mix_blend_mode()),
move(gfx_filter)
};
auto own_state = inherited_state;
if (effects.needs_layer())
own_state = visual_context_tree.append(move(effects), inherited_state);
paintable_box.set_accumulated_visual_context(own_state);
paintable_box.set_accumulated_visual_context_for_descendants(own_state);
paintable_box.for_each_child_of_type<PaintableBox>([&](PaintableBox& child) {
build_nested_svg_visual_context_tree_for_subtree(visual_context_tree, child, own_state, pixel_ratio);
return IterationDecision::Continue;
});
}
static AccumulatedVisualContextTree build_nested_svg_visual_context_tree(PaintableBox& root_paintable)
{
auto visual_context_tree = AccumulatedVisualContextTree::create();
auto pixel_ratio = root_paintable.document().page().client().device_pixels_per_css_pixel();
build_nested_svg_visual_context_tree_for_subtree(visual_context_tree, root_paintable, {}, pixel_ratio);
return visual_context_tree;
}
Optional<Gfx::MaskKind> SVGMaskable::get_svg_mask_type() const
{
auto const& graphics_element = as<SVG::SVGGraphicsElement const>(*dom_node_of_svg());
@ -84,7 +125,7 @@ static Optional<DisplayListResource> paint_mask_or_clip_to_display_list(
bool is_clip_path)
{
auto mask_rect = context.enclosing_device_rect(area);
auto visual_context_tree = AccumulatedVisualContextTree::create();
auto visual_context_tree = build_nested_svg_visual_context_tree(const_cast<PaintableBox&>(paintable));
auto display_list = DisplayList::create(visual_context_tree);
DisplayListRecorder display_list_recorder(*display_list, visual_context_tree, context.display_list_recorder().resource_storage());
display_list_recorder.translate(-mask_rect.location().to_type<int>());

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<style>
body {
margin: 0;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<rect width="100" height="100" fill="rgb(88, 101, 242)"></rect>
<rect x="25" y="25" width="50" height="50" fill="white"></rect>
</svg>

View file

@ -0,0 +1,10 @@
<!DOCTYPE html>
<style>
body {
margin: 0;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<rect width="100" height="100" fill="rgb(88, 101, 242)"></rect>
<rect x="25" y="25" width="50" height="50" fill="white"></rect>
</svg>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<link rel="match" href="../../expected/svg/mask-filter-alpha-use-ref.html" />
<style>
body {
margin: 0;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100">
<defs>
<clippath id="clip">
<rect width="100" height="100"></rect>
</clippath>
<g id="cutout" clip-path="url(#clip)">
<rect x="25" y="25" width="50" height="50" fill="white"></rect>
</g>
<filter id="invert-alpha">
<feComponentTransfer>
<feFuncA type="table" tableValues="1 0"></feFuncA>
</feComponentTransfer>
</filter>
<mask id="mask" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<g filter="url(#invert-alpha)">
<rect width="100" height="100" fill="white" opacity="0"></rect>
<use xlink:href="#cutout"></use>
</g>
</mask>
</defs>
<rect width="100" height="100" fill="rgb(88, 101, 242)" mask="url(#mask)"></rect>
</svg>

View file

@ -0,0 +1,23 @@
<!DOCTYPE html>
<link rel="match" href="../../expected/svg/mask-filter-alpha-ref.html" />
<style>
body {
margin: 0;
}
</style>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<defs>
<filter id="invert-alpha">
<feComponentTransfer>
<feFuncA type="table" tableValues="1 0"></feFuncA>
</feComponentTransfer>
</filter>
<mask id="mask" mask-type="alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
<g filter="url(#invert-alpha)">
<rect width="100" height="100" fill="white" opacity="0"></rect>
<rect x="25" y="25" width="50" height="50" fill="white"></rect>
</g>
</mask>
</defs>
<rect width="100" height="100" fill="rgb(88, 101, 242)" mask="url(#mask)"></rect>
</svg>