From: Magnus Jacobsson Date: Tue, 16 Aug 2022 13:14:27 +0000 (+0200) Subject: tests: SvgAnalyzer: SVGElement: add retrieval of bounding box X-Git-Tag: 6.0.1~32^2~8 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=11dcb53e2b208fff9f190077771f42459b986d8c;p=graphviz tests: SvgAnalyzer: SVGElement: add retrieval of bounding box --- diff --git a/tests/svg_element.cpp b/tests/svg_element.cpp index 256687038..ac9ea4cde 100644 --- a/tests/svg_element.cpp +++ b/tests/svg_element.cpp @@ -1,3 +1,5 @@ +#include +#include #include #include @@ -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::max() / 2, + .y = std::numeric_limits::max() / 2, + .width = std::numeric_limits::lowest(), + .height = std::numeric_limits::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: diff --git a/tests/svg_element.h b/tests/svg_element.h index c8864c355..d22b102ee 100644 --- a/tests/svg_element.h +++ b/tests/svg_element.h @@ -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 m_bbox; }; } // namespace SVG