LibWeb: Paint/apply uses of SVG <clipPath>s

This works just like masks, with a few more (spec imposed) limitations.
All the style properties on the contents of a <clipPath> are ignored,
and instead we just paint the "raw geometry" (as all black), then apply
that as an alpha mask to the target element.

If the element has both a `mask` and `clip-path` set, we compute the
masking area as the intersection of the mask rect and clip rect, then
apply the clip mask to the `mask` (producing a combined mask).

Fixes #19648
Fixes #23006
This commit is contained in:
MacDue 2024-03-27 00:19:41 +00:00 committed by Andreas Kling
parent c1b5fe61d1
commit 8cec7ea221
Notes: sideshowbarker 2024-07-17 01:46:00 +09:00
3 changed files with 68 additions and 18 deletions

View File

@ -46,6 +46,16 @@ public:
return m_svg_transform;
}
bool draw_svg_geometry_for_clip_path() const
{
return m_draw_svg_geometry_for_clip_path;
}
void set_draw_svg_geometry_for_clip_path(bool draw_svg_geometry_for_clip_path)
{
m_draw_svg_geometry_for_clip_path = draw_svg_geometry_for_clip_path;
}
DevicePixels enclosing_device_pixels(CSSPixels css_pixels) const;
DevicePixels floored_device_pixels(CSSPixels css_pixels) const;
DevicePixels rounded_device_pixels(CSSPixels css_pixels) const;
@ -82,6 +92,7 @@ private:
bool m_should_show_line_box_borders { false };
bool m_should_paint_overlay { true };
bool m_focus { false };
bool m_draw_svg_geometry_for_clip_path { false };
Gfx::AffineTransform m_svg_transform;
u32 m_next_corner_clipper_id { 0 };
};

View File

@ -5,8 +5,10 @@
*/
#include <LibWeb/Layout/ImageBox.h>
#include <LibWeb/Layout/SVGClipBox.h>
#include <LibWeb/Layout/SVGMaskBox.h>
#include <LibWeb/Painting/CommandExecutorCPU.h>
#include <LibWeb/Painting/SVGClipPaintable.h>
#include <LibWeb/Painting/SVGGraphicsPaintable.h>
#include <LibWeb/Painting/StackingContext.h>
#include <LibWeb/SVG/SVGMaskElement.h>
@ -32,10 +34,18 @@ Layout::SVGGraphicsBox const& SVGGraphicsPaintable::layout_box() const
Optional<CSSPixelRect> SVGGraphicsPaintable::get_masking_area() const
{
auto const& graphics_element = verify_cast<SVG::SVGGraphicsElement const>(*dom_node());
auto* mask_box = graphics_element.layout_node()->first_child_of_type<Layout::SVGMaskBox>();
if (!mask_box)
return {};
return mask_box->dom_node().resolve_masking_area(mask_box->paintable_box()->absolute_border_box_rect());
Optional<CSSPixelRect> masking_area = {};
if (auto* mask_box = graphics_element.layout_node()->first_child_of_type<Layout::SVGMaskBox>())
masking_area = mask_box->dom_node().resolve_masking_area(mask_box->paintable_box()->absolute_border_box_rect());
if (auto* clip_box = graphics_element.layout_node()->first_child_of_type<Layout::SVGClipBox>()) {
// This is a bit ad-hoc, but if we have both a mask and a clip-path, intersect the two areas to find the masking area.
auto clip_area = clip_box->paintable_box()->absolute_border_box_rect();
if (masking_area.has_value())
masking_area = masking_area->intersected(clip_area);
else
masking_area = clip_area;
}
return masking_area;
}
static Gfx::Bitmap::MaskKind mask_type_to_gfx_mask_kind(CSS::MaskType mask_type)
@ -53,33 +63,47 @@ static Gfx::Bitmap::MaskKind mask_type_to_gfx_mask_kind(CSS::MaskType mask_type)
Optional<Gfx::Bitmap::MaskKind> SVGGraphicsPaintable::get_mask_type() const
{
auto const& graphics_element = verify_cast<SVG::SVGGraphicsElement const>(*dom_node());
auto mask = graphics_element.mask();
if (!mask)
return {};
return mask_type_to_gfx_mask_kind(mask->layout_node()->computed_values().mask_type());
if (auto mask = graphics_element.mask())
return mask_type_to_gfx_mask_kind(mask->layout_node()->computed_values().mask_type());
if (graphics_element.clip_path())
return Gfx::Bitmap::MaskKind::Alpha;
return {};
}
RefPtr<Gfx::Bitmap> SVGGraphicsPaintable::calculate_mask(PaintContext& context, CSSPixelRect const& masking_area) const
{
auto const& graphics_element = verify_cast<SVG::SVGGraphicsElement const>(*dom_node());
auto* mask_box = graphics_element.layout_node()->first_child_of_type<Layout::SVGMaskBox>();
VERIFY(mask_box);
auto mask_rect = context.enclosing_device_rect(masking_area);
RefPtr<Gfx::Bitmap> mask_bitmap = {};
auto& mask_paintable = static_cast<PaintableBox const&>(*mask_box->paintable());
auto mask_bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, mask_rect.size().to_type<int>());
if (mask_bitmap_or_error.is_error())
return {};
mask_bitmap = mask_bitmap_or_error.release_value();
{
auto paint_mask_or_clip = [&](PaintableBox const& paintable) -> RefPtr<Gfx::Bitmap> {
auto mask_bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, mask_rect.size().to_type<int>());
RefPtr<Gfx::Bitmap> mask_bitmap = {};
if (mask_bitmap_or_error.is_error())
return {};
mask_bitmap = mask_bitmap_or_error.release_value();
CommandList painting_commands;
RecordingPainter recording_painter(painting_commands);
recording_painter.translate(-mask_rect.location().to_type<int>());
auto paint_context = context.clone(recording_painter);
paint_context.set_svg_transform(graphics_element.get_transform());
StackingContext::paint_node_as_stacking_context(mask_paintable, paint_context);
paint_context.set_draw_svg_geometry_for_clip_path(is<SVGClipPaintable>(paintable));
StackingContext::paint_node_as_stacking_context(paintable, paint_context);
CommandExecutorCPU executor { *mask_bitmap };
painting_commands.execute(executor);
return mask_bitmap;
};
RefPtr<Gfx::Bitmap> mask_bitmap = {};
if (auto* mask_box = graphics_element.layout_node()->first_child_of_type<Layout::SVGMaskBox>()) {
auto& mask_paintable = static_cast<PaintableBox const&>(*mask_box->paintable());
mask_bitmap = paint_mask_or_clip(mask_paintable);
}
if (auto* clip_box = graphics_element.layout_node()->first_child_of_type<Layout::SVGClipBox>()) {
auto& clip_paintable = static_cast<PaintableBox const&>(*clip_box->paintable());
auto clip_bitmap = paint_mask_or_clip(clip_paintable);
// Combine the clip-path with the mask (if present).
if (mask_bitmap && clip_bitmap)
mask_bitmap->apply_mask(*clip_bitmap, Gfx::Bitmap::MaskKind::Alpha);
if (!mask_bitmap)
mask_bitmap = clip_bitmap;
}
return mask_bitmap;
}

View File

@ -91,6 +91,21 @@ void SVGPathPaintable::paint(PaintContext& context, PaintPhase phase) const
return Gfx::FloatRect { { 0, 0 }, svg_element_rect.size().to_type<float>() };
}();
if (context.draw_svg_geometry_for_clip_path()) {
// https://drafts.fxtf.org/css-masking/#ClipPathElement:
// The raw geometry of each child element exclusive of rendering properties such as fill, stroke, stroke-width
// within a clipPath conceptually defines a 1-bit mask (with the possible exception of anti-aliasing along
// the edge of the geometry) which represents the silhouette of the graphics associated with that element.
context.recording_painter().fill_path({
.path = closed_path(),
.color = Color::Black,
// FIXME: Support clip-rule.
.winding_rule = Gfx::Painter::WindingRule::Nonzero,
.translation = offset,
});
return;
}
SVG::SVGPaintContext paint_context {
.viewport = svg_viewport,
.path_bounding_box = computed_path()->bounding_box(),