SVGFormattingContext.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. /*
  2. * Copyright (c) 2021-2023, Andreas Kling <kling@serenityos.org>
  3. * Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
  4. * Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
  5. * Copyright (c) 2023, MacDue <macdue@dueutil.tech>
  6. *
  7. * SPDX-License-Identifier: BSD-2-Clause
  8. */
  9. #include <AK/Debug.h>
  10. #include <LibWeb/Layout/BlockFormattingContext.h>
  11. #include <LibWeb/Layout/SVGFormattingContext.h>
  12. #include <LibWeb/Layout/SVGGeometryBox.h>
  13. #include <LibWeb/Layout/SVGSVGBox.h>
  14. #include <LibWeb/Layout/SVGTextBox.h>
  15. #include <LibWeb/SVG/SVGForeignObjectElement.h>
  16. #include <LibWeb/SVG/SVGSVGElement.h>
  17. #include <LibWeb/SVG/SVGSymbolElement.h>
  18. #include <LibWeb/SVG/SVGUseElement.h>
  19. namespace Web::Layout {
  20. SVGFormattingContext::SVGFormattingContext(LayoutState& state, Box const& box, FormattingContext* parent)
  21. : FormattingContext(Type::SVG, state, box, parent)
  22. {
  23. }
  24. SVGFormattingContext::~SVGFormattingContext() = default;
  25. CSSPixels SVGFormattingContext::automatic_content_width() const
  26. {
  27. return 0;
  28. }
  29. CSSPixels SVGFormattingContext::automatic_content_height() const
  30. {
  31. return 0;
  32. }
  33. struct ViewBoxTransform {
  34. CSSPixelPoint offset;
  35. double scale_factor;
  36. };
  37. // https://svgwg.org/svg2-draft/coords.html#PreserveAspectRatioAttribute
  38. static ViewBoxTransform scale_and_align_viewbox_content(SVG::PreserveAspectRatio const& preserve_aspect_ratio,
  39. SVG::ViewBox const& view_box, Gfx::FloatSize viewbox_scale, auto const& svg_box_state)
  40. {
  41. ViewBoxTransform viewbox_transform {};
  42. switch (preserve_aspect_ratio.meet_or_slice) {
  43. case SVG::PreserveAspectRatio::MeetOrSlice::Meet:
  44. // meet (the default) - Scale the graphic such that:
  45. // - aspect ratio is preserved
  46. // - the entire ‘viewBox’ is visible within the SVG viewport
  47. // - the ‘viewBox’ is scaled up as much as possible, while still meeting the other criteria
  48. viewbox_transform.scale_factor = min(viewbox_scale.width(), viewbox_scale.height());
  49. break;
  50. case SVG::PreserveAspectRatio::MeetOrSlice::Slice:
  51. // slice - Scale the graphic such that:
  52. // aspect ratio is preserved
  53. // the entire SVG viewport is covered by the ‘viewBox’
  54. // the ‘viewBox’ is scaled down as much as possible, while still meeting the other criteria
  55. viewbox_transform.scale_factor = max(viewbox_scale.width(), viewbox_scale.height());
  56. break;
  57. default:
  58. VERIFY_NOT_REACHED();
  59. }
  60. // Handle X alignment:
  61. if (svg_box_state.has_definite_width()) {
  62. switch (preserve_aspect_ratio.align) {
  63. case SVG::PreserveAspectRatio::Align::xMinYMin:
  64. case SVG::PreserveAspectRatio::Align::xMinYMid:
  65. case SVG::PreserveAspectRatio::Align::xMinYMax:
  66. // Align the <min-x> of the element's ‘viewBox’ with the smallest X value of the SVG viewport.
  67. viewbox_transform.offset.translate_by(0, 0);
  68. break;
  69. case SVG::PreserveAspectRatio::Align::None: {
  70. // Do not force uniform scaling. Scale the graphic content of the given element non-uniformly
  71. // if necessary such that the element's bounding box exactly matches the SVG viewport rectangle.
  72. // FIXME: None is unimplemented (treat as xMidYMid)
  73. [[fallthrough]];
  74. }
  75. case SVG::PreserveAspectRatio::Align::xMidYMin:
  76. case SVG::PreserveAspectRatio::Align::xMidYMid:
  77. case SVG::PreserveAspectRatio::Align::xMidYMax:
  78. // Align the midpoint X value of the element's ‘viewBox’ with the midpoint X value of the SVG viewport.
  79. viewbox_transform.offset.translate_by((svg_box_state.content_width() - (view_box.width * viewbox_transform.scale_factor)) / 2, 0);
  80. break;
  81. case SVG::PreserveAspectRatio::Align::xMaxYMin:
  82. case SVG::PreserveAspectRatio::Align::xMaxYMid:
  83. case SVG::PreserveAspectRatio::Align::xMaxYMax:
  84. // Align the <min-x>+<width> of the element's ‘viewBox’ with the maximum X value of the SVG viewport.
  85. viewbox_transform.offset.translate_by((svg_box_state.content_width() - (view_box.width * viewbox_transform.scale_factor)), 0);
  86. break;
  87. default:
  88. VERIFY_NOT_REACHED();
  89. }
  90. }
  91. if (svg_box_state.has_definite_width()) {
  92. switch (preserve_aspect_ratio.align) {
  93. case SVG::PreserveAspectRatio::Align::xMinYMin:
  94. case SVG::PreserveAspectRatio::Align::xMidYMin:
  95. case SVG::PreserveAspectRatio::Align::xMaxYMin:
  96. // Align the <min-y> of the element's ‘viewBox’ with the smallest Y value of the SVG viewport.
  97. viewbox_transform.offset.translate_by(0, 0);
  98. break;
  99. case SVG::PreserveAspectRatio::Align::None: {
  100. // Do not force uniform scaling. Scale the graphic content of the given element non-uniformly
  101. // if necessary such that the element's bounding box exactly matches the SVG viewport rectangle.
  102. // FIXME: None is unimplemented (treat as xMidYMid)
  103. [[fallthrough]];
  104. }
  105. case SVG::PreserveAspectRatio::Align::xMinYMid:
  106. case SVG::PreserveAspectRatio::Align::xMidYMid:
  107. case SVG::PreserveAspectRatio::Align::xMaxYMid:
  108. // Align the midpoint Y value of the element's ‘viewBox’ with the midpoint Y value of the SVG viewport.
  109. viewbox_transform.offset.translate_by(0, (svg_box_state.content_height() - (view_box.height * viewbox_transform.scale_factor)) / 2);
  110. break;
  111. case SVG::PreserveAspectRatio::Align::xMinYMax:
  112. case SVG::PreserveAspectRatio::Align::xMidYMax:
  113. case SVG::PreserveAspectRatio::Align::xMaxYMax:
  114. // Align the <min-y>+<height> of the element's ‘viewBox’ with the maximum Y value of the SVG viewport.
  115. viewbox_transform.offset.translate_by(0, (svg_box_state.content_height() - (view_box.height * viewbox_transform.scale_factor)));
  116. break;
  117. default:
  118. VERIFY_NOT_REACHED();
  119. }
  120. }
  121. return viewbox_transform;
  122. }
  123. static bool should_ensure_creation_of_paintable(Node const& node)
  124. {
  125. if (is<SVGTextBox>(node))
  126. return true;
  127. if (is<SVGGraphicsBox>(node))
  128. return true;
  129. if (node.dom_node()) {
  130. if (is<SVG::SVGUseElement>(*node.dom_node()))
  131. return true;
  132. if (is<SVG::SVGSymbolElement>(*node.dom_node()))
  133. return true;
  134. }
  135. return false;
  136. }
  137. void SVGFormattingContext::run(Box const& box, LayoutMode layout_mode, AvailableSpace const& available_space)
  138. {
  139. auto& svg_svg_element = verify_cast<SVG::SVGSVGElement>(*box.dom_node());
  140. auto svg_box_state = m_state.get(box);
  141. auto root_offset = svg_box_state.offset;
  142. box.for_each_child_of_type<BlockContainer>([&](BlockContainer const& child_box) {
  143. if (is<SVG::SVGForeignObjectElement>(child_box.dom_node())) {
  144. Layout::BlockFormattingContext bfc(m_state, child_box, this);
  145. bfc.run(child_box, LayoutMode::Normal, available_space);
  146. auto& child_state = m_state.get_mutable(child_box);
  147. child_state.set_content_offset(child_state.offset.translated(root_offset));
  148. }
  149. return IterationDecision::Continue;
  150. });
  151. box.for_each_in_subtree([&](Node const& descendant) {
  152. if (is<SVGGeometryBox>(descendant)) {
  153. auto const& geometry_box = static_cast<SVGGeometryBox const&>(descendant);
  154. auto& geometry_box_state = m_state.get_mutable(geometry_box);
  155. auto& dom_node = const_cast<SVGGeometryBox&>(geometry_box).dom_node();
  156. auto& path = dom_node.get_path();
  157. auto path_transform = dom_node.get_transform();
  158. double viewbox_scale = 1;
  159. auto maybe_view_box = dom_node.view_box();
  160. if (maybe_view_box.has_value()) {
  161. // FIXME: This should allow just one of width or height to be specified.
  162. // E.g. We should be able to layout <svg width="100%"> where height is unspecified/auto.
  163. if (!svg_box_state.has_definite_width() || !svg_box_state.has_definite_height()) {
  164. dbgln_if(LIBWEB_CSS_DEBUG, "FIXME: Attempting to layout indefinitely sized SVG with a viewbox -- this likely won't work!");
  165. }
  166. auto view_box = maybe_view_box.value();
  167. auto scale_width = svg_box_state.has_definite_width() ? svg_box_state.content_width() / view_box.width : 1;
  168. auto scale_height = svg_box_state.has_definite_height() ? svg_box_state.content_height() / view_box.height : 1;
  169. // The initial value for preserveAspectRatio is xMidYMid meet.
  170. auto preserve_aspect_ratio = svg_svg_element.preserve_aspect_ratio().value_or(SVG::PreserveAspectRatio {});
  171. auto viewbox_transform = scale_and_align_viewbox_content(preserve_aspect_ratio, view_box, { scale_width, scale_height }, svg_box_state);
  172. 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);
  173. viewbox_scale = viewbox_transform.scale_factor;
  174. }
  175. // Stroke increases the path's size by stroke_width/2 per side.
  176. auto path_bounding_box = path_transform.map(path.bounding_box()).to_type<CSSPixels>();
  177. CSSPixels stroke_width = static_cast<double>(geometry_box.dom_node().visible_stroke_width()) * viewbox_scale;
  178. path_bounding_box.inflate(stroke_width, stroke_width);
  179. geometry_box_state.set_content_offset(path_bounding_box.top_left());
  180. geometry_box_state.set_content_width(path_bounding_box.width());
  181. geometry_box_state.set_content_height(path_bounding_box.height());
  182. } else if (is<SVGSVGBox>(descendant)) {
  183. SVGFormattingContext nested_context(m_state, static_cast<SVGSVGBox const&>(descendant), this);
  184. nested_context.run(static_cast<SVGSVGBox const&>(descendant), layout_mode, available_space);
  185. } else if (should_ensure_creation_of_paintable(descendant)) {
  186. // NOTE: This hack creates a layout state to ensure the existence of
  187. // a paintable in LayoutState::commit().
  188. m_state.get_mutable(static_cast<NodeWithStyleAndBoxModelMetrics const&>(descendant));
  189. }
  190. return IterationDecision::Continue;
  191. });
  192. // https://svgwg.org/svg2-draft/struct.html#Groups
  193. // 5.2. Grouping: the ‘g’ element
  194. // The ‘g’ element is a container element for grouping together related graphics elements.
  195. box.for_each_in_subtree_of_type<Box>([&](Box const& descendant) {
  196. if (is<SVGGraphicsBox>(descendant) && !is<SVGGeometryBox>(descendant) && !is<SVGTextBox>(descendant)) {
  197. auto const& svg_graphics_box = static_cast<SVGGraphicsBox const&>(descendant);
  198. auto& graphics_box_state = m_state.get_mutable(svg_graphics_box);
  199. auto smallest_x_position = CSSPixels(0);
  200. auto smallest_y_position = CSSPixels(0);
  201. auto greatest_x_position = CSSPixels(0);
  202. auto greatest_y_position = CSSPixels(0);
  203. descendant.for_each_in_subtree_of_type<Box>([&](Box const& child_of_svg_container) {
  204. auto& box_state = m_state.get_mutable(child_of_svg_container);
  205. smallest_x_position = box_state.offset.x();
  206. smallest_y_position = box_state.offset.y();
  207. return IterationDecision::Break;
  208. });
  209. descendant.for_each_in_subtree_of_type<Box>([&](Box const& child_of_svg_container) {
  210. auto& box_state = m_state.get_mutable(child_of_svg_container);
  211. smallest_x_position = min(smallest_x_position, box_state.offset.x());
  212. smallest_y_position = min(smallest_y_position, box_state.offset.y());
  213. greatest_x_position = max(greatest_x_position, box_state.offset.x() + box_state.content_width());
  214. greatest_y_position = max(greatest_y_position, box_state.offset.y() + box_state.content_height());
  215. return IterationDecision::Continue;
  216. });
  217. graphics_box_state.set_content_x(smallest_x_position);
  218. graphics_box_state.set_content_y(smallest_y_position);
  219. graphics_box_state.set_content_width(greatest_x_position - smallest_x_position);
  220. graphics_box_state.set_content_height(greatest_y_position - smallest_y_position);
  221. }
  222. return IterationDecision::Continue;
  223. });
  224. }
  225. }