+#include <cassert>
+#include <limits>
#include <stdexcept>
#include <fmt/format.h>
}
}
+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()) {
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:
double y;
double width;
double height;
+ void extend(const SVGPoint &point);
+ void extend(const SVGRect &other);
};
struct SVGMatrix {
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;
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