summaryrefslogtreecommitdiff
path: root/Userland/Libraries/LibWeb
diff options
context:
space:
mode:
authorMacDue <macdue@dueutil.tech>2023-04-17 01:20:24 +0100
committerAndreas Kling <kling@serenityos.org>2023-04-17 07:32:31 +0200
commit5df4e64eb7e7a2556d282b634d66adf7d8edc5ec (patch)
tree343eb6dca7a90f903fe82ea536a1fc23684bbc08 /Userland/Libraries/LibWeb
parent5a12e9f22292cd6b6393b5d4b09165ebdee3a8bb (diff)
downloadserenity-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.cpp124
-rw-r--r--Userland/Libraries/LibWeb/SVG/AttributeParser.cpp73
-rw-r--r--Userland/Libraries/LibWeb/SVG/AttributeParser.h22
-rw-r--r--Userland/Libraries/LibWeb/SVG/SVGSVGElement.cpp2
-rw-r--r--Userland/Libraries/LibWeb/SVG/SVGSVGElement.h3
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;
};
}