瀏覽代碼

LibWeb: Implement SVG `preserveAspectRatio` attribute

This attribute is used to define how the viewBox should be scaled.
Previously the behaviour implemented was that of "xMidYMid meet", now
all of them work (expect none :P).

With this the Discord login backend is now correctly scaled/positioned.

This also brings our SVG code a little closer to the spec! With spec
comments and all :^)

(Minor non-visible update to layout tests)
MacDue 2 年之前
父節點
當前提交
5df4e64eb7

+ 3 - 3
Tests/LibWeb/Layout/expected/svg-transforms-and-viewboxes.txt

@@ -48,13 +48,13 @@ Viewport <#document> at (0,0) content-size 800x600 children: not-inline
       TextNode <#text>
       SVGSVGBox <svg> at (50,250) content-size 200x200 children: inline
         TextNode <#text>
-        SVGGeometryBox <rect> at (120.588233,320.588256) content-size 58.823524x58.823528 children: not-inline
+        SVGGeometryBox <rect> at (120.588233,320.588256) content-size 58.823524x58.823532 children: not-inline
         TextNode <#text>
         TextNode <#text>
-        SVGGeometryBox <rect> at (52.443771,310.373626) content-size 68.144462x68.144454 children: not-inline
+        SVGGeometryBox <rect> at (52.443771,310.373657) content-size 68.144462x68.144454 children: not-inline
         TextNode <#text>
         TextNode <#text>
-        SVGGeometryBox <rect> at (179.411773,321.481903) content-size 68.14447x68.144462 children: not-inline
+        SVGGeometryBox <rect> at (179.411773,321.481903) content-size 68.14447x68.14447 children: not-inline
         TextNode <#text>
       TextNode <#text>
       SVGSVGBox <svg> at (258,250) content-size 200x200 children: inline

+ 108 - 16
Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp

@@ -32,9 +32,105 @@ CSSPixels SVGFormattingContext::automatic_content_height() const
     return 0;
 }
 
+struct ViewBoxTransform {
+    CSSPixelPoint offset;
+    float scale_factor;
+};
+
+// https://svgwg.org/svg2-draft/coords.html#PreserveAspectRatioAttribute
+static ViewBoxTransform scale_and_align_viewbox_content(SVG::PreserveAspectRatio const& preserve_aspect_ratio,
+    SVG::ViewBox const& view_box, Gfx::FloatSize viewbox_scale, auto const& svg_box_state)
+{
+    ViewBoxTransform viewbox_transform {};
+
+    switch (preserve_aspect_ratio.meet_or_slice) {
+    case SVG::PreserveAspectRatio::MeetOrSlice::Meet:
+        // meet (the default) - Scale the graphic such that:
+        // - aspect ratio is preserved
+        // - the entire ‘viewBox’ is visible within the SVG viewport
+        // - the ‘viewBox’ is scaled up as much as possible, while still meeting the other criteria
+        viewbox_transform.scale_factor = min(viewbox_scale.width(), viewbox_scale.height());
+        break;
+    case SVG::PreserveAspectRatio::MeetOrSlice::Slice:
+        // slice - Scale the graphic such that:
+        // aspect ratio is preserved
+        // the entire SVG viewport is covered by the ‘viewBox’
+        // the ‘viewBox’ is scaled down as much as possible, while still meeting the other criteria
+        viewbox_transform.scale_factor = max(viewbox_scale.width(), viewbox_scale.height());
+        break;
+    default:
+        VERIFY_NOT_REACHED();
+    }
+
+    // Handle X alignment:
+    if (svg_box_state.has_definite_width()) {
+        switch (preserve_aspect_ratio.align) {
+        case SVG::PreserveAspectRatio::Align::xMinYMin:
+        case SVG::PreserveAspectRatio::Align::xMinYMid:
+        case SVG::PreserveAspectRatio::Align::xMinYMax:
+            // Align the <min-x> of the element's ‘viewBox’ with the smallest X value of the SVG viewport.
+            viewbox_transform.offset.translate_by(0, 0);
+            break;
+        case SVG::PreserveAspectRatio::Align::None: {
+            // Do not force uniform scaling. Scale the graphic content of the given element non-uniformly
+            // if necessary such that the element's bounding box exactly matches the SVG viewport rectangle.
+            // FIXME: None is unimplemented (treat as xMidYMid)
+            [[fallthrough]];
+        }
+        case SVG::PreserveAspectRatio::Align::xMidYMin:
+        case SVG::PreserveAspectRatio::Align::xMidYMid:
+        case SVG::PreserveAspectRatio::Align::xMidYMax:
+            // Align the midpoint X value of the element's ‘viewBox’ with the midpoint X value of the SVG viewport.
+            viewbox_transform.offset.translate_by((svg_box_state.content_width() - (view_box.width * viewbox_transform.scale_factor)) / 2, 0);
+            break;
+        case SVG::PreserveAspectRatio::Align::xMaxYMin:
+        case SVG::PreserveAspectRatio::Align::xMaxYMid:
+        case SVG::PreserveAspectRatio::Align::xMaxYMax:
+            // Align the <min-x>+<width> of the element's ‘viewBox’ with the maximum X value of the SVG viewport.
+            viewbox_transform.offset.translate_by((svg_box_state.content_width() - (view_box.width * viewbox_transform.scale_factor)), 0);
+            break;
+        default:
+            VERIFY_NOT_REACHED();
+        }
+    }
+
+    if (svg_box_state.has_definite_width()) {
+        switch (preserve_aspect_ratio.align) {
+        case SVG::PreserveAspectRatio::Align::xMinYMin:
+        case SVG::PreserveAspectRatio::Align::xMidYMin:
+        case SVG::PreserveAspectRatio::Align::xMaxYMin:
+            // Align the <min-y> of the element's ‘viewBox’ with the smallest Y value of the SVG viewport.
+            viewbox_transform.offset.translate_by(0, 0);
+            break;
+        case SVG::PreserveAspectRatio::Align::None: {
+            // Do not force uniform scaling. Scale the graphic content of the given element non-uniformly
+            // if necessary such that the element's bounding box exactly matches the SVG viewport rectangle.
+            // FIXME: None is unimplemented (treat as xMidYMid)
+            [[fallthrough]];
+        }
+        case SVG::PreserveAspectRatio::Align::xMinYMid:
+        case SVG::PreserveAspectRatio::Align::xMidYMid:
+        case SVG::PreserveAspectRatio::Align::xMaxYMid:
+            // Align the midpoint Y value of the element's ‘viewBox’ with the midpoint Y value of the SVG viewport.
+            viewbox_transform.offset.translate_by(0, (svg_box_state.content_height() - (view_box.height * viewbox_transform.scale_factor)) / 2);
+            break;
+        case SVG::PreserveAspectRatio::Align::xMinYMax:
+        case SVG::PreserveAspectRatio::Align::xMidYMax:
+        case SVG::PreserveAspectRatio::Align::xMaxYMax:
+            // Align the <min-y>+<height> of the element's ‘viewBox’ with the maximum Y value of the SVG viewport.
+            viewbox_transform.offset.translate_by(0, (svg_box_state.content_height() - (view_box.height * viewbox_transform.scale_factor)));
+            break;
+        default:
+            VERIFY_NOT_REACHED();
+        }
+    }
+
+    return viewbox_transform;
+}
+
 void SVGFormattingContext::run(Box const& box, LayoutMode, [[maybe_unused]] AvailableSpace const& available_space)
 {
-    // FIXME: This entire thing is an ad-hoc hack.
+    // FIXME: This a bunch of this thing is an ad-hoc hack.
 
     auto& svg_svg_element = verify_cast<SVG::SVGSVGElement>(*box.dom_node());
 
@@ -59,37 +155,33 @@ void SVGFormattingContext::run(Box const& box, LayoutMode, [[maybe_unused]] Avai
             auto& dom_node = const_cast<SVGGeometryBox&>(geometry_box).dom_node();
 
             auto& path = dom_node.get_path();
-            auto transform = dom_node.get_transform();
+            auto path_transform = dom_node.get_transform();
 
+            float viewbox_scale = 1;
             auto& maybe_view_box = svg_svg_element.view_box();
-            float viewbox_scale = 1.0f;
-
-            CSSPixelPoint offset {};
             if (maybe_view_box.has_value()) {
-                auto view_box = maybe_view_box.value();
                 // FIXME: This should allow just one of width or height to be specified.
                 // E.g. We should be able to layout <svg width="100%"> where height is unspecified/auto.
                 if (!svg_box_state.has_definite_width() || !svg_box_state.has_definite_height()) {
                     dbgln("FIXME: Attempting to layout indefinitely sized SVG with a viewbox -- this likely won't work!");
                 }
+
+                auto view_box = maybe_view_box.value();
                 auto scale_width = svg_box_state.has_definite_width() ? svg_box_state.content_width().value() / view_box.width : 1;
                 auto scale_height = svg_box_state.has_definite_height() ? svg_box_state.content_height().value() / view_box.height : 1;
-                viewbox_scale = min(scale_width, scale_height);
-
-                // Center the viewbox within the SVG element:
-                if (svg_box_state.has_definite_width())
-                    offset.translate_by((svg_box_state.content_width() - (view_box.width * viewbox_scale)) / 2, 0);
-                if (svg_box_state.has_definite_height())
-                    offset.translate_by(0, (svg_box_state.content_height() - (view_box.height * viewbox_scale)) / 2);
 
-                transform = Gfx::AffineTransform {}.scale(viewbox_scale, viewbox_scale).translate({ -view_box.min_x, -view_box.min_y }).multiply(transform);
+                // The initial value for preserveAspectRatio is xMidYMid meet.
+                auto preserve_aspect_ratio = svg_svg_element.preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {});
+                auto viewbox_transform = scale_and_align_viewbox_content(preserve_aspect_ratio, view_box, { scale_width, scale_height }, svg_box_state);
+                path_transform = Gfx::AffineTransform {}.translate(viewbox_transform.offset.to_type<float>()).scale(viewbox_transform.scale_factor, viewbox_transform.scale_factor).translate({ -view_box.min_x, -view_box.min_y }).multiply(path_transform);
+                viewbox_scale = viewbox_transform.scale_factor;
             }
 
             // Stroke increases the path's size by stroke_width/2 per side.
-            auto path_bounding_box = transform.map(path.bounding_box()).to_type<CSSPixels>();
+            auto path_bounding_box = path_transform.map(path.bounding_box()).to_type<CSSPixels>();
             CSSPixels stroke_width = geometry_box.dom_node().visible_stroke_width() * viewbox_scale;
             path_bounding_box.inflate(stroke_width, stroke_width);
-            geometry_box_state.set_content_offset(path_bounding_box.top_left() + offset);
+            geometry_box_state.set_content_offset(path_bounding_box.top_left());
             geometry_box_state.set_content_width(path_bounding_box.width());
             geometry_box_state.set_content_height(path_bounding_box.height());
         }

+ 67 - 6
Userland/Libraries/LibWeb/SVG/AttributeParser.cpp

@@ -397,16 +397,77 @@ int AttributeParser::parse_sign()
     return 1;
 }
 
-// https://drafts.csswg.org/css-transforms/#svg-syntax
-Optional<Vector<Transform>> AttributeParser::parse_transform()
+static bool whitespace(char c)
 {
     // wsp:
     // Either a U+000A LINE FEED, U+000D CARRIAGE RETURN, U+0009 CHARACTER TABULATION, or U+0020 SPACE.
-    auto wsp = [](char c) {
-        return AK::first_is_one_of(c, '\n', '\r', '\t', '\f', ' ');
-    };
+    return AK::first_is_one_of(c, '\n', '\r', '\t', '\f', ' ');
+}
+
+// https://svgwg.org/svg2-draft/coords.html#PreserveAspectRatioAttribute
+Optional<PreserveAspectRatio> AttributeParser::parse_preserve_aspect_ratio(StringView input)
+{
+    // <align> <meetOrSlice>?
+    GenericLexer lexer { input };
+    lexer.ignore_while(whitespace);
+    auto align_string = lexer.consume_until(whitespace);
+    if (align_string.is_empty())
+        return {};
+    lexer.ignore_while(whitespace);
+    auto meet_or_slice_string = lexer.consume_until(whitespace);
+
+    // <align> =
+    //     none
+    //     | xMinYMin | xMidYMin | xMaxYMin
+    //     | xMinYMid | xMidYMid | xMaxYMid
+    //     | xMinYMax | xMidYMax | xMaxYMax
+    auto align = [&]() -> Optional<PreserveAspectRatio::Align> {
+        if (align_string == "none"sv)
+            return PreserveAspectRatio::Align::None;
+        if (align_string == "xMinYMin"sv)
+            return PreserveAspectRatio::Align::xMinYMin;
+        if (align_string == "xMidYMin"sv)
+            return PreserveAspectRatio::Align::xMidYMin;
+        if (align_string == "xMaxYMin"sv)
+            return PreserveAspectRatio::Align::xMaxYMin;
+        if (align_string == "xMinYMid"sv)
+            return PreserveAspectRatio::Align::xMinYMid;
+        if (align_string == "xMidYMid"sv)
+            return PreserveAspectRatio::Align::xMidYMid;
+        if (align_string == "xMaxYMid"sv)
+            return PreserveAspectRatio::Align::xMaxYMid;
+        if (align_string == "xMinYMax"sv)
+            return PreserveAspectRatio::Align::xMinYMax;
+        if (align_string == "xMidYMax"sv)
+            return PreserveAspectRatio::Align::xMidYMax;
+        if (align_string == "xMaxYMax"sv)
+            return PreserveAspectRatio::Align::xMaxYMax;
+        return {};
+    }();
+
+    if (!align.has_value())
+        return {};
+
+    // <meetOrSlice> = meet | slice
+    auto meet_or_slice = [&]() -> Optional<PreserveAspectRatio::MeetOrSlice> {
+        if (meet_or_slice_string.is_empty() || meet_or_slice_string == "meet"sv)
+            return PreserveAspectRatio::MeetOrSlice::Meet;
+        if (meet_or_slice_string == "slice"sv)
+            return PreserveAspectRatio::MeetOrSlice::Slice;
+        return {};
+    }();
+
+    if (!meet_or_slice.has_value())
+        return {};
+
+    return PreserveAspectRatio { *align, *meet_or_slice };
+}
+
+// https://drafts.csswg.org/css-transforms/#svg-syntax
+Optional<Vector<Transform>> AttributeParser::parse_transform()
+{
     auto consume_whitespace = [&] {
-        m_lexer.consume_while(wsp);
+        m_lexer.ignore_while(whitespace);
     };
 
     auto consume_comma_whitespace = [&] {

+ 22 - 0
Userland/Libraries/LibWeb/SVG/AttributeParser.h

@@ -67,6 +67,27 @@ struct Transform {
     Operation operation;
 };
 
+struct PreserveAspectRatio {
+    enum class Align {
+        None,
+        xMinYMin,
+        xMidYMin,
+        xMaxYMin,
+        xMinYMid,
+        xMidYMid,
+        xMaxYMid,
+        xMinYMax,
+        xMidYMax,
+        xMaxYMax
+    };
+    enum class MeetOrSlice {
+        Meet,
+        Slice
+    };
+    Align align { Align::xMidYMid };
+    MeetOrSlice meet_or_slice { MeetOrSlice::Meet };
+};
+
 class AttributeParser final {
 public:
     ~AttributeParser() = default;
@@ -77,6 +98,7 @@ public:
     static Vector<Gfx::FloatPoint> parse_points(StringView input);
     static Vector<PathInstruction> parse_path_data(StringView input);
     static Optional<Vector<Transform>> parse_transform(StringView input);
+    static Optional<PreserveAspectRatio> parse_preserve_aspect_ratio(StringView input);
 
 private:
     AttributeParser(StringView source);

+ 2 - 0
Userland/Libraries/LibWeb/SVG/SVGSVGElement.cpp

@@ -73,6 +73,8 @@ void SVGSVGElement::parse_attribute(DeprecatedFlyString const& name, DeprecatedS
 
     if (name.equals_ignoring_ascii_case(SVG::AttributeNames::viewBox))
         m_view_box = try_parse_view_box(value);
+    if (name.equals_ignoring_ascii_case(SVG::AttributeNames::preserveAspectRatio))
+        m_preserve_aspect_ratio = AttributeParser::parse_preserve_aspect_ratio(value);
 }
 
 }

+ 3 - 0
Userland/Libraries/LibWeb/SVG/SVGSVGElement.h

@@ -7,6 +7,7 @@
 #pragma once
 
 #include <LibGfx/Bitmap.h>
+#include <LibWeb/SVG/AttributeParser.h>
 #include <LibWeb/SVG/SVGGraphicsElement.h>
 #include <LibWeb/SVG/ViewBox.h>
 
@@ -24,6 +25,7 @@ public:
     virtual bool is_svg_container() const override { return true; }
 
     Optional<ViewBox> const& view_box() const { return m_view_box; }
+    Optional<PreserveAspectRatio> const& preserve_aspect_ratio() const { return m_preserve_aspect_ratio; }
 
 private:
     SVGSVGElement(DOM::Document&, DOM::QualifiedName);
@@ -35,6 +37,7 @@ private:
     virtual void parse_attribute(DeprecatedFlyString const& name, DeprecatedString const& value) override;
 
     Optional<ViewBox> m_view_box;
+    Optional<PreserveAspectRatio> m_preserve_aspect_ratio;
 };
 
 }