diff options
author | MacDue <macdue@dueutil.tech> | 2023-04-17 01:20:24 +0100 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2023-04-17 07:32:31 +0200 |
commit | 5df4e64eb7e7a2556d282b634d66adf7d8edc5ec (patch) | |
tree | 343eb6dca7a90f903fe82ea536a1fc23684bbc08 /Userland/Libraries/LibWeb | |
parent | 5a12e9f22292cd6b6393b5d4b09165ebdee3a8bb (diff) | |
download | serenity-5df4e64eb7e7a2556d282b634d66adf7d8edc5ec.zip |
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)
Diffstat (limited to 'Userland/Libraries/LibWeb')
-rw-r--r-- | Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp | 124 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/AttributeParser.cpp | 73 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/AttributeParser.h | 22 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/SVGSVGElement.cpp | 2 | ||||
-rw-r--r-- | Userland/Libraries/LibWeb/SVG/SVGSVGElement.h | 3 |
5 files changed, 202 insertions, 22 deletions
diff --git a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp b/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp index f215dcf801..7e7bd1991e 100644 --- a/Userland/Libraries/LibWeb/Layout/SVGFormattingContext.cpp +++ b/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()); } diff --git a/Userland/Libraries/LibWeb/SVG/AttributeParser.cpp b/Userland/Libraries/LibWeb/SVG/AttributeParser.cpp index 05ca7537f4..644f2843a5 100644 --- a/Userland/Libraries/LibWeb/SVG/AttributeParser.cpp +++ b/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 = [&] { diff --git a/Userland/Libraries/LibWeb/SVG/AttributeParser.h b/Userland/Libraries/LibWeb/SVG/AttributeParser.h index e7b5e075c5..164b184a9a 100644 --- a/Userland/Libraries/LibWeb/SVG/AttributeParser.h +++ b/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); diff --git a/Userland/Libraries/LibWeb/SVG/SVGSVGElement.cpp b/Userland/Libraries/LibWeb/SVG/SVGSVGElement.cpp index abfab45dbf..69b89bdfbc 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGSVGElement.cpp +++ b/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); } } diff --git a/Userland/Libraries/LibWeb/SVG/SVGSVGElement.h b/Userland/Libraries/LibWeb/SVG/SVGSVGElement.h index 7f03fda104..a214843f8f 100644 --- a/Userland/Libraries/LibWeb/SVG/SVGSVGElement.h +++ b/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; }; } |