mirror of
https://github.com/RGBCube/serenity
synced 2025-07-26 02:17:34 +00:00
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)
This commit is contained in:
parent
5a12e9f222
commit
5df4e64eb7
6 changed files with 205 additions and 25 deletions
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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', ' ');
|
||||
};
|
||||
}
|
||||
|
||||
// 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 = [&] {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue