]> granicus.if.org Git - graphviz/commitdiff
tests: SvgAnalyzer: SVGElement: add retrieval of bounding box
authorMagnus Jacobsson <Magnus.Jacobsson@berotec.se>
Tue, 16 Aug 2022 13:14:27 +0000 (15:14 +0200)
committerMagnus Jacobsson <Magnus.Jacobsson@berotec.se>
Tue, 23 Aug 2022 06:19:35 +0000 (08:19 +0200)
tests/svg_element.cpp
tests/svg_element.h

index 2566870385f0bead106c26c90cdecaaffeb3133f..ac9ea4cde3633e91991fc967aa279629ce94308c 100644 (file)
@@ -1,3 +1,5 @@
+#include <cassert>
+#include <limits>
 #include <stdexcept>
 
 #include <fmt/format.h>
@@ -55,6 +57,103 @@ static std::string to_graphviz_color(const std::string &color) {
   }
 }
 
+SVG::SVGRect SVG::SVGElement::bbox(bool throw_if_bbox_not_defined) {
+  if (!m_bbox.has_value()) {
+    // negative width and height bbox that will be imediately replaced by the
+    // first bbox found
+    m_bbox = {.x = std::numeric_limits<double>::max() / 2,
+              .y = std::numeric_limits<double>::max() / 2,
+              .width = std::numeric_limits<double>::lowest(),
+              .height = std::numeric_limits<double>::lowest()};
+    switch (type) {
+    case SVG::SVGElementType::Group:
+      // SVG group bounding box is detemined solely by its children
+      break;
+    case SVG::SVGElementType::Ellipse: {
+      m_bbox = {
+          .x = attributes.cx - attributes.rx,
+          .y = attributes.cy - attributes.ry,
+          .width = attributes.rx * 2,
+          .height = attributes.ry * 2,
+      };
+      break;
+    }
+    case SVG::SVGElementType::Polygon:
+    case SVG::SVGElementType::Polyline: {
+      for (const auto &point : attributes.points) {
+        m_bbox->extend(point);
+      }
+      break;
+    }
+    case SVG::SVGElementType::Path: {
+      if (path_points.empty()) {
+        throw std::runtime_error{"No points for 'path' element"};
+      }
+      for (const auto &point : path_points) {
+        m_bbox->extend(point);
+      }
+      break;
+    }
+    case SVG::SVGElementType::Rect: {
+      m_bbox = {
+          .x = attributes.x,
+          .y = attributes.y,
+          .width = attributes.width,
+          .height = attributes.height,
+      };
+      break;
+    }
+    case SVG::SVGElementType::Text: {
+      m_bbox = text_bbox();
+      break;
+    }
+    case SVG::SVGElementType::Title:
+      // title has no size
+      if (throw_if_bbox_not_defined) {
+        throw std::runtime_error{"A 'title' element has no bounding box"};
+      }
+      break;
+    default:
+      throw std::runtime_error{
+          fmt::format("Unhandled svg element type {}", tag(type))};
+    }
+
+    const auto throw_if_child_bbox_is_not_defined = false;
+    for (auto &child : children) {
+      const auto child_bbox = child.bbox(throw_if_child_bbox_is_not_defined);
+      m_bbox->extend(child_bbox);
+    }
+  }
+
+  return *m_bbox;
+}
+
+SVG::SVGRect SVG::SVGElement::text_bbox() const {
+  assert(type == SVG::SVGElementType::Text && "Not a 'text' element");
+
+  if (attributes.font_family != "Courier,monospace") {
+    throw std::runtime_error(
+        fmt::format("Cannot calculate bounding box for font \"{}\"",
+                    attributes.font_family));
+  }
+
+  // Epirically determined font metrics for the Courier font
+  const auto courier_width_per_pt = 0.6;
+  const auto courier_height_per_pt = 1.2;
+  const auto descent_per_pt = 1.0 / 3.0;
+  const auto font_width = attributes.font_size * courier_width_per_pt;
+  const auto font_height = attributes.font_size * courier_height_per_pt;
+  const auto descent = attributes.font_size * descent_per_pt;
+
+  const SVG::SVGRect bbox = {
+      .x = attributes.x - font_width * text.size() / 2,
+      .y = attributes.y - font_height + descent,
+      .width = font_width * text.size(),
+      .height = font_height,
+  };
+  return bbox;
+}
+
 void SVG::SVGElement::append_attribute(std::string &output,
                                        const std::string &attribute) const {
   if (attribute.empty()) {
@@ -242,6 +341,30 @@ SVG::SVGElement::stroke_to_graphviz_color(const std::string &color) const {
   return to_graphviz_color(color);
 }
 
+void SVG::SVGRect::extend(const SVGPoint &point) {
+  const auto xmin = std::min(x, point.x);
+  const auto ymin = std::min(y, point.y);
+  const auto xmax = std::max(x + width, point.x);
+  const auto ymax = std::max(y + height, point.y);
+
+  x = xmin;
+  y = ymin;
+  width = xmax - xmin;
+  height = ymax - ymin;
+}
+
+void SVG::SVGRect::extend(const SVG::SVGRect &other) {
+  const auto xmin = std::min(x, other.x);
+  const auto ymin = std::min(y, other.y);
+  const auto xmax = std::max(x + width, other.x + other.width);
+  const auto ymax = std::max(y + height, other.y + other.height);
+
+  x = xmin;
+  y = ymin;
+  width = xmax - xmin;
+  height = ymax - ymin;
+}
+
 std::string_view SVG::tag(SVGElementType type) {
   switch (type) {
   case SVG::SVGElementType::Circle:
index c8864c3554e063e44da969c1521dc948b50267b6..d22b102eed8a1ebe1bd25c5caa86da4842b8d1e9 100644 (file)
@@ -18,6 +18,8 @@ struct SVGRect {
   double y;
   double width;
   double height;
+  void extend(const SVGPoint &point);
+  void extend(const SVGRect &other);
 };
 
 struct SVGMatrix {
@@ -75,6 +77,13 @@ public:
   SVGElement() = delete;
   explicit SVGElement(SVG::SVGElementType type);
 
+  /// Return the bounding box of the element and its children. The bounding box
+  /// is calculated and stored the first time this function is called and later
+  /// calls will return the already calculated value. If this function is called
+  /// for an SVG element for which the bounding box is not defined, it will
+  /// throw an exception unless the `throw_if_bbox_not_defined` parameter is
+  /// `false`.
+  SVG::SVGRect bbox(bool throw_if_bbox_not_defined = true);
   std::string to_string(std::size_t indent_size) const;
 
   SVGAttributes attributes;
@@ -105,8 +114,13 @@ private:
   std::string points_attribute_to_string() const;
   std::string stroke_attribute_to_string() const;
   std::string stroke_to_graphviz_color(const std::string &color) const;
+  SVG::SVGRect text_bbox() const;
   void to_string_impl(std::string &output, std::size_t indent_size,
                       std::size_t current_indent) const;
+
+  /// The bounding box of the element and its children. Stored the first time
+  /// it's computed
+  std::optional<SVG::SVGRect> m_bbox;
 };
 
 } // namespace SVG