/* * Copyright (c) 2021-2023, Andreas Kling * Copyright (c) 2022, Sam Atkins * Copyright (c) 2022, Tobias Christiansen * Copyright (c) 2023, MacDue * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Web::Layout { SVGFormattingContext::SVGFormattingContext(LayoutState& state, Box const& box, FormattingContext* parent) : FormattingContext(Type::SVG, state, box, parent) { } SVGFormattingContext::~SVGFormattingContext() = default; CSSPixels SVGFormattingContext::automatic_content_width() const { return 0; } CSSPixels SVGFormattingContext::automatic_content_height() const { return 0; } struct ViewBoxTransform { CSSPixelPoint offset; double 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 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() - CSSPixels::nearest_value_for(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 + of the element's ‘viewBox’ with the maximum X value of the SVG viewport. viewbox_transform.offset.translate_by((svg_box_state.content_width() - CSSPixels::nearest_value_for(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 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() - CSSPixels::nearest_value_for(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 + 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() - CSSPixels::nearest_value_for(view_box.height * viewbox_transform.scale_factor))); break; default: VERIFY_NOT_REACHED(); } } return viewbox_transform; } static bool is_container_element(Node const& node) { // https://svgwg.org/svg2-draft/struct.html#GroupsOverview auto* dom_node = node.dom_node(); if (!dom_node) return false; if (is(dom_node)) return true; if (is(dom_node)) return true; if (is(dom_node)) return true; if (is(dom_node)) return true; return false; } void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, AvailableSpace const& available_space) { auto& svg_svg_element = verify_cast(*box.dom_node()); auto svg_box_state = m_state.get(box); auto root_offset = svg_box_state.offset; box.for_each_child_of_type([&](BlockContainer const& child_box) { if (is(child_box.dom_node())) { Layout::BlockFormattingContext bfc(m_state, child_box, this); bfc.run(child_box, LayoutMode::Normal, available_space); auto& child_state = m_state.get_mutable(child_box); child_state.set_content_offset(child_state.offset.translated(root_offset)); } return IterationDecision::Continue; }); auto compute_viewbox_transform = [&](auto const& viewbox) -> Gfx::AffineTransform { if (!viewbox.has_value()) return {}; // FIXME: This should allow just one of width or height to be specified. // E.g. We should be able to layout where height is unspecified/auto. if (!svg_box_state.has_definite_width() || !svg_box_state.has_definite_height()) { dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Attempting to layout indefinitely sized SVG with a viewbox -- this likely won't work!"); } auto scale_width = svg_box_state.has_definite_width() ? svg_box_state.content_width() / viewbox->width : 1; auto scale_height = svg_box_state.has_definite_height() ? svg_box_state.content_height() / viewbox->height : 1; // The initial value for preserveAspectRatio is xMidYMid meet. auto preserve_aspect_ratio = svg_svg_element.preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {}); auto viewbox_offset_and_scale = scale_and_align_viewbox_content(preserve_aspect_ratio, *viewbox, { scale_width, scale_height }, svg_box_state); CSSPixelPoint offset = viewbox_offset_and_scale.offset; return Gfx::AffineTransform {}.translate(offset.to_type()).scale(viewbox_offset_and_scale.scale_factor, viewbox_offset_and_scale.scale_factor).translate({ -viewbox->min_x, -viewbox->min_y }); }; box.for_each_in_subtree([&](Node const& descendant) { if (is(descendant)) { auto const& graphics_box = static_cast(descendant); auto& dom_node = const_cast(graphics_box).dom_node(); auto viewbox = dom_node.view_box(); // https://svgwg.org/svg2-draft/coords.html#ViewBoxAttribute if (viewbox.has_value()) { if (viewbox->width < 0 || viewbox->height < 0) { // A negative value for or is an error and invalidates the ‘viewBox’ attribute. viewbox = {}; } else if (viewbox->width == 0 || viewbox->height == 0) { // A value of zero disables rendering of the element. return IterationDecision::Continue; } } auto& graphics_box_state = m_state.get_mutable(graphics_box); auto svg_transform = dom_node.get_transform(); Gfx::AffineTransform viewbox_transform = compute_viewbox_transform(viewbox); graphics_box_state.set_computed_svg_transforms(Painting::SVGGraphicsPaintable::ComputedTransforms(viewbox_transform, svg_transform)); auto to_css_pixels_transform = Gfx::AffineTransform {}.multiply(viewbox_transform).multiply(svg_transform); Gfx::Path path; if (is(descendant)) { path = static_cast(dom_node).get_path(); } else if (is(descendant)) { auto& text_element = static_cast(dom_node); auto& font = graphics_box.first_available_font(); auto text_contents = text_element.text_contents(); Utf8View text_utf8 { text_contents }; auto text_width = font.width(text_utf8); auto text_offset = text_element.get_offset(); // https://svgwg.org/svg2-draft/text.html#TextAnchoringProperties switch (text_element.text_anchor().value_or(SVG::TextAnchor::Start)) { case SVG::TextAnchor::Start: // The rendered characters are aligned such that the start of the resulting rendered text is at the initial // current text position. break; case SVG::TextAnchor::Middle: { // The rendered characters are shifted such that the geometric middle of the resulting rendered text // (determined from the initial and final current text position before applying the text-anchor property) // is at the initial current text position. text_offset.translate_by(-text_width / 2, 0); break; } case SVG::TextAnchor::End: { // The rendered characters are shifted such that the end of the resulting rendered text (final current text // position before applying the text-anchor property) is at the initial current text position. text_offset.translate_by(-text_width, 0); break; } default: VERIFY_NOT_REACHED(); } path.move_to(text_offset); path.text(text_utf8, font); } else if (is(descendant)) { auto& text_path_element = static_cast(dom_node); auto path_or_shape = text_path_element.path_or_shape(); if (!path_or_shape) return IterationDecision::Continue; auto& font = graphics_box.first_available_font(); auto text_contents = text_path_element.text_contents(); Utf8View text_utf8 { text_contents }; auto shape_path = const_cast(*path_or_shape).get_path(); path = shape_path.place_text_along(text_utf8, font); } auto path_bounding_box = to_css_pixels_transform.map(path.bounding_box()).to_type(); // Stroke increases the path's size by stroke_width/2 per side. CSSPixels stroke_width = CSSPixels::nearest_value_for(graphics_box.dom_node().visible_stroke_width() * viewbox_transform.x_scale()); path_bounding_box.inflate(stroke_width, stroke_width); graphics_box_state.set_content_offset(path_bounding_box.top_left()); graphics_box_state.set_content_width(path_bounding_box.width()); graphics_box_state.set_content_height(path_bounding_box.height()); graphics_box_state.set_computed_svg_path(move(path)); } else if (is(descendant)) { SVGFormattingContext nested_context(m_state, static_cast(descendant), this); nested_context.run(static_cast(descendant), layout_mode, available_space); } return IterationDecision::Continue; }); // https://svgwg.org/svg2-draft/struct.html#Groups // 5.2. Grouping: the ‘g’ element // The ‘g’ element is a container element for grouping together related graphics elements. box.for_each_in_subtree_of_type([&](Box const& descendant) { if (is_container_element(descendant)) { Gfx::BoundingBox bounding_box; descendant.for_each_in_subtree_of_type([&](Box const& child_of_svg_container) { auto& box_state = m_state.get(child_of_svg_container); bounding_box.add_point(box_state.offset); bounding_box.add_point(box_state.offset.translated(box_state.content_width(), box_state.content_height())); return IterationDecision::Continue; }); auto& box_state = m_state.get_mutable(descendant); box_state.set_content_x(bounding_box.x()); box_state.set_content_y(bounding_box.y()); box_state.set_content_width(bounding_box.width()); box_state.set_content_height(bounding_box.height()); } return IterationDecision::Continue; }); } }