From 8461791ce2d15e7ba30cb7220a76c2b5bf8503d0 Mon Sep 17 00:00:00 2001 From: Tobias Christiansen Date: Sun, 27 Feb 2022 21:00:04 +0100 Subject: [PATCH] LibWeb: Add support for 'view-box' attribute to SVGs This patch begins the support for the 'view-box' attribute that can be attached to 's. The FormattingContext determines the size of the Element according to the specified 'width' and 'height' or if they are not given by the 'viewbox' or by the bounding box of the path if nothing is specified. When we try to paint a SVG Path that belongs to a that has the 'view-box' and a specified 'height'/'width', all the parts of the path get scaled/moved accordingly. There probably are many edge cases and bugs still to be found, but this is a nice start. :^) --- .../LibWeb/Layout/SVGFormattingContext.cpp | 55 +++++++++++-- .../LibWeb/Layout/SVGGeometryBox.cpp | 82 ++++++++++++++++++- .../Libraries/LibWeb/Layout/SVGGeometryBox.h | 3 + 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp index 6bcdfee60e..00ce1fca2e 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp @@ -1,6 +1,7 @@ /* * Copyright (c) 2021, Andreas Kling * Copyright (c) 2022, Sam Atkins + * Copyright (c) 2022, Tobias Christiansen * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,6 +9,7 @@ #include #include #include +#include namespace Web::Layout { @@ -22,20 +24,57 @@ SVGFormattingContext::~SVGFormattingContext() void SVGFormattingContext::run(Box const& box, LayoutMode) { - box.for_each_in_subtree_of_type([&](auto const& descendant) { + box.for_each_in_subtree_of_type([&](SVGBox const& descendant) { if (is(descendant)) { auto const& geometry_box = static_cast(descendant); - auto& path = const_cast(geometry_box).dom_node().get_path(); - auto bounding_box = path.bounding_box(); + + auto& geometry_box_state = m_state.get_mutable(geometry_box); + + auto& dom_node = const_cast(geometry_box).dom_node(); + + SVG::SVGSVGElement* svg_element = dom_node.first_ancestor_of_type(); + + if (svg_element->has_attribute(HTML::AttributeNames::width) && svg_element->has_attribute(HTML::AttributeNames::width)) { + geometry_box_state.offset = { 0, 0 }; + auto& layout_node = static_cast(*(svg_element->layout_node())); + + // FIXME: Allow for relative lengths here + geometry_box_state.content_width = layout_node.computed_values().width().value().resolved(layout_node, { 0, CSS::Length::Type::Px }).to_px(layout_node); + geometry_box_state.content_height = layout_node.computed_values().height().value().resolved(layout_node, { 0, CSS::Length::Type::Px }).to_px(layout_node); + + return IterationDecision::Continue; + } + + // FIXME: Allow for one of {width, height} to not be specified} + if (svg_element->has_attribute(HTML::AttributeNames::width)) { + } + + if (svg_element->has_attribute(HTML::AttributeNames::height)) { + } + + auto& path = dom_node.get_path(); + auto path_bounding_box = path.bounding_box(); // Stroke increases the path's size by stroke_width/2 per side. auto stroke_width = geometry_box.dom_node().stroke_width().value_or(0); - bounding_box.inflate(stroke_width, stroke_width); + path_bounding_box.inflate(stroke_width, stroke_width); - auto& geometry_box_state = m_state.get_mutable(geometry_box); - geometry_box_state.offset = bounding_box.top_left(); - geometry_box_state.content_width = bounding_box.width(); - geometry_box_state.content_height = bounding_box.height(); + auto& maybe_view_box = svg_element->view_box(); + + if (maybe_view_box.has_value()) { + auto view_box = maybe_view_box.value(); + Gfx::FloatPoint viewbox_offset = { view_box.min_x, view_box.min_y }; + geometry_box_state.offset = path_bounding_box.top_left() + viewbox_offset; + + geometry_box_state.content_width = view_box.width; + geometry_box_state.content_height = view_box.height; + + return IterationDecision::Continue; + } + + geometry_box_state.offset = path_bounding_box.top_left(); + geometry_box_state.content_width = path_bounding_box.width(); + geometry_box_state.content_height = path_bounding_box.height(); } return IterationDecision::Continue; diff --git a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp index 0f6c4a774f..6f20b7867f 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp +++ b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2020, Matthew Olsson + * Copyright (c) 2022, Tobias Christiansen * * SPDX-License-Identifier: BSD-2-Clause */ @@ -8,6 +9,7 @@ #include #include #include +#include namespace Web::Layout { @@ -27,7 +29,6 @@ void SVGGeometryBox::paint(PaintContext& context, PaintPhase phase) return; auto& geometry_element = dom_node(); - auto& path = geometry_element.get_path(); Gfx::AntiAliasingPainter painter { context.painter() }; auto& svg_context = context.svg_context(); @@ -35,6 +36,56 @@ void SVGGeometryBox::paint(PaintContext& context, PaintPhase phase) auto offset = svg_context.svg_element_position(); painter.translate(offset); + SVG::SVGSVGElement* svg_element = geometry_element.first_ancestor_of_type(); + auto maybe_view_box = svg_element->view_box(); + + context.painter().add_clip_rect((Gfx::Rect)absolute_rect()); + + Gfx::Path path = geometry_element.get_path(); + + if (maybe_view_box.has_value()) { + Gfx::Path new_path; + auto scaling = viewbox_scaling(); + auto origin = viewbox_origin(); + + auto transform_point = [&scaling, &origin](Gfx::FloatPoint const& point) -> Gfx::FloatPoint { + auto new_point = point; + new_point.translate_by({ -origin.x(), -origin.y() }); + new_point.scale_by(scaling); + return new_point; + }; + + for (auto& segment : path.segments()) { + switch (segment.type()) { + case Gfx::Segment::Type::Invalid: + break; + case Gfx::Segment::Type::MoveTo: + new_path.move_to(transform_point(segment.point())); + break; + case Gfx::Segment::Type::LineTo: + new_path.line_to(transform_point(segment.point())); + break; + case Gfx::Segment::Type::QuadraticBezierCurveTo: { + auto& quadratic_bezier_segment = static_cast(segment); + new_path.quadratic_bezier_curve_to(transform_point(quadratic_bezier_segment.through()), transform_point(quadratic_bezier_segment.point())); + break; + } + case Gfx::Segment::Type::CubicBezierCurveTo: { + auto& cubic_bezier_segment = static_cast(segment); + new_path.cubic_bezier_curve_to(transform_point(cubic_bezier_segment.through_0()), transform_point(cubic_bezier_segment.through_1()), transform_point(cubic_bezier_segment.point())); + break; + } + case Gfx::Segment::Type::EllipticalArcTo: { + auto& elliptical_arc_segment = static_cast(segment); + new_path.elliptical_arc_to(transform_point(elliptical_arc_segment.point()), elliptical_arc_segment.radii().scaled(scaling, scaling), elliptical_arc_segment.x_axis_rotation(), false, false); + break; + } + } + } + + path = new_path; + } + if (auto fill_color = geometry_element.fill_color().value_or(svg_context.fill_color()); fill_color.alpha() > 0) { // We need to fill the path before applying the stroke, however the filled // path must be closed, whereas the stroke path may not necessary be closed. @@ -57,6 +108,35 @@ void SVGGeometryBox::paint(PaintContext& context, PaintPhase phase) } painter.translate(-offset); + context.painter().clear_clip_rect(); +} + +float SVGGeometryBox::viewbox_scaling() const +{ + auto* svg_box = dom_node().first_ancestor_of_type(); + + if (!svg_box || !svg_box->view_box().has_value()) + return 1; + + auto view_box = svg_box->view_box().value(); + + bool has_specified_width = svg_box->has_attribute(HTML::AttributeNames::width); + auto specified_width = content_width(); + + bool has_specified_height = svg_box->has_attribute(HTML::AttributeNames::height); + auto specified_height = content_height(); + + auto scale_width = has_specified_width ? specified_width / view_box.width : 1; + auto scale_height = has_specified_height ? specified_height / view_box.height : 1; + + return min(scale_width, scale_height); +} +Gfx::FloatPoint SVGGeometryBox::viewbox_origin() const +{ + auto* svg_box = dom_node().first_ancestor_of_type(); + if (!svg_box || !svg_box->view_box().has_value()) + return { 0, 0 }; + return { svg_box->view_box().value().min_x, svg_box->view_box().value().min_y }; } } diff --git a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h index 197750d1b5..e53ee44df5 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h +++ b/Userland/Libraries/LibWeb/Layout/SVGGeometryBox.h @@ -21,6 +21,9 @@ public: virtual void paint(PaintContext& context, PaintPhase phase) override; + float viewbox_scaling() const; + Gfx::FloatPoint viewbox_origin() const; + private: virtual bool is_svg_geometry_box() const final { return true; } };