|
@@ -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());
|
|
|
}
|