# coding=utf-8 """Test inkex `.bounding_box()` method functionality""" from copy import deepcopy import os import subprocess import pytest from inkex import ( BoundingBox, SvgDocumentElement, Circle, Rectangle, Group, PathElement, Transform, Path, Style, ) from inkex.tester import TestCase from tempfile import TemporaryDirectory from inkex.command import is_inkscape_available from inkex.tester.decorators import requires_inkscape try: from typing import Optional, Tuple except ImportError: pass DISABLE_STROKE_TESTS = True DISABLE_STROKE_CAP_TESTS = True DISABLE_INKSCAPE_QUERY_CHECK = not is_inkscape_available() skip_stroke_tests = pytest.mark.skipif( # pylint: disable=invalid-name DISABLE_STROKE_TESTS, reason="Bounding box tests with stroke are disabled" ) skip_stroke_cap_tests = pytest.mark.skipif( # pylint: disable=invalid-name DISABLE_STROKE_TESTS or DISABLE_STROKE_CAP_TESTS, reason="Bounding box tests with stroke-cap are disabled", ) class BoundingBoxTest(TestCase): """Test BoundingBox functionality""" atol = 3e-3 def assert_bounding_box_is_equal( self, obj, xscale, yscale, disable_inkscape_check=DISABLE_INKSCAPE_QUERY_CHECK ): """ Assert: bounding box of object is exactly expected_box or is close to it :param (ShapeElement) obj: object to calculate bounding box :param (Optional[Tuple[float,float]]) xscale: expected values of (xmin, xmax) :param (Optional[Tuple[float,float]]) yscale: expected values of (ymin, ymax) """ bounding_box = obj.bounding_box() if bounding_box is None: self.assertEqual((None, None), (xscale, yscale)) return box_array = list(bounding_box) expected_array = xscale, yscale def cmp(a, b, msg=None): self.assertEqual(len(a), len(b), msg=msg) for x, y, label in zip(a, b, ("x", "y")): self.assertDeepAlmostEqual( tuple(x), tuple(y), delta=self.atol, msg=msg + " (%s)" % label ) if not disable_inkscape_check: inkscape_array = self.get_inkscape_bounding_box(obj) if None not in inkscape_array: cmp(expected_array, inkscape_array, "expected != inkscape calculation") cmp(box_array, expected_array, "inkex.bounding_box != expected") def get_inkscape_bounding_box(self, obj): """ :param (ShapeElement) obj: :return: (xmin, xmax, ymin, ymax) parsed from `inkscape --query` output """ svg = SvgDocumentElement() obj = deepcopy(obj) obj_id = "testing-query-id" obj.set("id", obj_id) root = deepcopy(obj.getroottree().getroot()) svg.add(root) with TemporaryDirectory() as tmp: temp_svg = os.path.join(tmp, "tmp.svg") with open(temp_svg, "wb") as out: out.write(svg.tostring()) with open(os.devnull, "w") as devnull: output = subprocess.check_output( [ "inkscape", "--query-all", temp_svg, ], stderr=devnull, ) out_lines = output.decode("utf-8").split("\n") for line in out_lines: if line.startswith(obj_id): x, y, w, h = list(map(float, line.split(",")[1:])) return (x, x + w), (y, y + h) return None, None, None, None def test_bbox_empty_is_false(self): self.assertFalse(bool(BoundingBox())) def test_bbox_nonempty_is_true(self): self.assertTrue(bool(BoundingBox((0, 0), (0, 0)))) def test_bbox_empty_is_identity_for_addition(self): bbox = BoundingBox((0, 1), (2, 3)) self.assertEqual(BoundingBox() + bbox, bbox) def test_bbox_empty_is_zero_for_intersection(self): bbox = BoundingBox((0, 1), (2, 3)) self.assertEqual(BoundingBox() & bbox, BoundingBox()) def test_bbox_nonintersection_is_empty(self): bbox1 = BoundingBox((0, 1), (2, 3)) bbox2 = BoundingBox((2, 3), (1, 2)) self.assertEqual(bbox1 & bbox2, BoundingBox()) def test_bbox_empty_equivalent_to_none(self): bbox = BoundingBox((0, 1), (2, 3)) self.assertEqual(bbox + None, bbox + BoundingBox()) self.assertEqual(bbox & None, bbox & BoundingBox()) self.assertEqual(None + bbox, BoundingBox() + bbox) self.assertEqual(None & bbox, BoundingBox() & bbox) def test_circle_without_attributes(self): circle = Circle() self.assert_bounding_box_is_equal(circle, (0, 0), (0, 0)) def test_circle_with_radius(self): r = 10 circle = Circle(r=str(r)) self.assert_bounding_box_is_equal(circle, (-r, r), (-r, r)) def test_circle_with_cx(self): cx = 10 circle = Circle(cx=str(cx)) self.assert_bounding_box_is_equal(circle, (cx, cx), (0, 0)) def test_circle_with_cy(self): cy = 10 circle = Circle(cy=str(cy)) self.assert_bounding_box_is_equal(circle, (0, 0), (cy, cy)) def test_circle_without_center(self): r = 10 circle = Circle(r=str(r)) self.assert_bounding_box_is_equal(circle, (-r, r), (-r, r)) def test_regular_circle(self): r = 5 cx = 10 cy = 20 circle = Circle(r=str(r), cx=str(cx), cy=str(cy)) self.assert_bounding_box_is_equal(circle, (cx - r, cx + r), (cy - r, cy + r)) @skip_stroke_tests def test_circle_with_stroke(self): r = 5 cx = 10 cy = 20 stroke_half_width = 1.0 circle = Circle(r=str(r), cx=str(cx), cy=str(cy)) circle.style = Style("stroke-width:{};stroke:red".format(stroke_half_width * 2)) self.assert_bounding_box_is_equal( circle, (cx - (r + stroke_half_width), cx + (r + stroke_half_width)), (cy - (r + stroke_half_width), cy + (r + stroke_half_width)), ) @skip_stroke_tests def test_circle_with_stroke_scaled(self): r = 5 cx = 10 cy = 20 scale_x = 2 scale_y = 3 stroke_half_width = 1.0 circle = Circle(r=str(r), cx=str(cx), cy=str(cy)) circle.style = Style("stroke-width:{};stroke:red".format(stroke_half_width * 2)) circle.transform = Transform(scale=(scale_x, scale_y)) self.assert_bounding_box_is_equal( circle, ( scale_x * (cx - (r + stroke_half_width)), scale_x * (cx + (r + stroke_half_width)), ), ( scale_y * (cy - (r + stroke_half_width)), scale_y * (cy + (r + stroke_half_width)), ), ) def test_rectangle_without_attributes(self): rect = Rectangle() self.assert_bounding_box_is_equal(rect, (0, 0), (0, 0)) def test_rectangle_without_dimensions(self): x, y = 10, 15 w, h = 0, 0 rect = Rectangle(x=str(x), y=str(y)) self.assert_bounding_box_is_equal(rect, (x, x + w), (y, y + h)) def test_rectangle_without_coordinates(self): x, y = 0, 0 w, h = 7, 20 rect = Rectangle(width=str(w), height=str(h)) self.assert_bounding_box_is_equal(rect, (x, x + w), (y, y + h)) def test_regular_rectangle(self): x, y = 10, 20 w, h = 7, 20 rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) self.assert_bounding_box_is_equal(rect, (x, x + w), (y, y + h)) def test_regular_rectangle_scaled(self): x, y = 10, 20 w, h = 7, 20 scale_x = 2 scale_y = 3 rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) rect.transform = Transform(scale=(scale_x, scale_y)) self.assert_bounding_box_is_equal( rect, (scale_x * x, scale_x * (x + w)), (scale_y * y, scale_y * (y + h)) ) @skip_stroke_tests def test_regular_rectangle_with_stroke(self): x, y = 10, 20 w, h = 7, 20 stroke_half_width = 1 rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) rect.style = Style("stroke-width:{};stroke:red".format(stroke_half_width * 2)) self.assert_bounding_box_is_equal( rect, (x - stroke_half_width, x + w + stroke_half_width), (y - stroke_half_width, y + h + stroke_half_width), ) @skip_stroke_tests def test_regular_rectangle_with_stroke_scaled(self): x, y = 10, 20 w, h = 7, 20 stroke_half_width = 1 scale_x = 2 scale_y = 3 rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) rect.style = Style("stroke-width:{};stroke:red".format(stroke_half_width * 2)) rect.transform = Transform(scale=(scale_x, scale_y)) self.assert_bounding_box_is_equal( rect, (scale_x * (x - stroke_half_width), scale_x * (x + w + stroke_half_width)), (scale_y * (y - stroke_half_width), scale_y * (y + h + stroke_half_width)), ) def test_empty_path(self): path = PathElement() self.assert_bounding_box_is_equal(path, None, None) def test_path_with_move_commands_only(self): path = PathElement() path.set_path("M 0 0 " "m 100 100 " "M 200 200") self.assert_bounding_box_is_equal(path, (0, 200), (0, 200)) def test_path_straight_line(self): path = PathElement() path.set_path("M 0 0 " "L 10 10") self.assert_bounding_box_is_equal(path, (0, 10), (0, 10)) def test_path_two_straight_lines_abosolute(self): path = PathElement() path.set_path("M 0 0 " "L 10 10 " "M -1 1 " "L 10 10") self.assert_bounding_box_is_equal(path, (-1, 10), (0, 10)) def test_path_two_straight_lines_relative(self): path = PathElement() path.set_path("M 0 0 " "l 10 10 " "m -11 -9 " "l 12 12") self.assert_bounding_box_is_equal(path, (-1, 11), (0, 13)) def test_path_straight_line_scaled(self): path = PathElement() scale_x = 2 scale_y = 3 path.set_path("M 10 10 " "L 20 20") path.transform = Transform(scale=(scale_x, scale_y)) self.assert_bounding_box_is_equal( path, (scale_x * 10, 20 * scale_x), (scale_y * 10, 20 * scale_y) ) @skip_stroke_cap_tests def test_path_horizontal_line_stroke_butt_cap(self): path = PathElement() path.set_path("M 0 0 " "L 1 0") stroke_half_width = 1.0 path.style = Style("stroke-width:{};stroke:red".format(stroke_half_width * 2)) path.set("stroke-linecap", "butt") self.assert_bounding_box_is_equal( path, (0, 1), (-stroke_half_width, stroke_half_width) ) @skip_stroke_cap_tests def test_path_horizontal_line_stroke_round_cap(self): path = PathElement() path.set_path("M 0 0 " "L 1 0") stroke_half_width = 1.0 path.style = Style("stroke-width:{};stroke:red".format(stroke_half_width * 2)) path.set("stroke-linecap", "round") self.assert_bounding_box_is_equal( path, (-stroke_half_width, 1 + stroke_half_width), (-stroke_half_width, stroke_half_width), ) @skip_stroke_cap_tests def test_path_horizontal_line_stroke_square_cap(self): path = PathElement() path.set_path("M 0 0 " "L 1 0") stroke_half_width = 1.0 path.style = Style("stroke-width:{};stroke:red".format(stroke_half_width * 2)) path.set("stroke-linecap", "square") self.assert_bounding_box_is_equal( path, (-stroke_half_width, 1 + stroke_half_width), (-stroke_half_width, stroke_half_width), ) def test_empty_group(self): group = Group() self.assert_bounding_box_is_equal(group, None, None) def test_empty_group_with_translation(self): group = Group() group.transform = Transform(translate=(10, 15)) self.assert_bounding_box_is_equal(group, None, None) def test_group_with_regular_rect(self): group = Group() x, y = 10, 20 w, h = 7, 20 rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) group.add(rect) self.assert_bounding_box_is_equal(group, (x, x + w), (y, y + h)) def test_group_with_number_of_rects(self): group = Group() xmin, ymin = 1000, 1000 xmax, ymax = -1000, -1000 rects = [] for x, y, w, h in [ (10, 20, 5, 7), (30, 40, 5, 7), ]: rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) rects.append(rect) xmin = min(xmin, x) xmax = max(xmax, x + w) ymin = min(ymin, y) ymax = max(ymax, y + h) group.append(rect) self.assert_bounding_box_is_equal(group, (xmin, xmax), (ymin, ymax)) def test_group_with_number_of_rects_scaled(self): group = Group() scale_x, scale_y = 5, 10 xmin, ymin = 1000, 1000 xmax, ymax = -1000, -1000 rects = [] for x, y, w, h in [ (10, 20, 5, 7), (30, 40, 5, 7), ]: rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) rects.append(rect) xmin = min(xmin, x) xmax = max(xmax, x + w) ymin = min(ymin, y) ymax = max(ymax, y + h) group.add(rect) group.transform = Transform(scale=(scale_x, scale_y)) self.assert_bounding_box_is_equal( group, (scale_x * xmin, scale_x * xmax), (scale_y * ymin, scale_y * ymax) ) def test_group_with_number_of_rects_translated(self): group = Group() dx, dy = 5, 10 xmin, ymin = 1000, 1000 xmax, ymax = -1000, -1000 rects = [] for x, y, w, h in [ (10, 20, 5, 7), (30, 40, 5, 7), ]: rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) rects.append(rect) xmin = min(xmin, x) xmax = max(xmax, x + w) ymin = min(ymin, y) ymax = max(ymax, y + h) group.add(rect) group.transform = Transform(translate=(dx, dy)) self.assert_bounding_box_is_equal( group, (dx + xmin, dx + xmax), (dy + ymin, dy + ymax) ) def test_group_nested_transform(self): group = Group() x, y = 10, 20 w, h = 7, 20 scale = 2 rect = Rectangle(width=str(w), height=str(h), x=str(x), y=str(y)) rect.transform = Transform(rotate=45, scale=scale) group.add(rect) group.transform = Transform( rotate=-45 ) # rotation is compensated, but scale is not a = rect.composed_transform() self.assert_bounding_box_is_equal( group, (scale * x, scale * (x + w)), (scale * y, scale * (y + h)) ) def test_path_Arc_long_sweep_off(self): path = Path("M 10 20 A 10 20 0 1 0 20 15") path_element = PathElement() path_element.path = path self.assert_bounding_box_is_equal( path_element, (7.078, 20 + 7.078), (15.0, 15.0 + 39.127) ) def test_path_Arc_short_sweep_off(self): path = Path("M 10 20 A 10 20 0 0 0 20 15") path_element = PathElement() path_element.path = path self.assert_bounding_box_is_equal(path_element, (10, 20), (15, 15.0 + 5.873)) def test_path_Arc_short_sweep_on(self): path = Path("M 10 20 A 10 20 0 0 1 20 15") path_element = PathElement() path_element.path = path self.assert_bounding_box_is_equal( path_element, (10, 20), (14.127, 14.127 + 5.873) ) def test_path_Arc_long_sweep_on(self): path = Path("M 10 20 A 10 20 0 1 1 20 15") path_element = PathElement() path_element.path = path self.assert_bounding_box_is_equal( path_element, (2.922, 2.922 + 20), (-19.127, -19.127 + 39.127) ) def test_path_Arc_long_sweep_on_axis_x_25(self): path = Path("M 10 20 A 10 20 25 1 1 20 15") path_element = PathElement() path_element.path = path self.assert_bounding_box_is_equal( path_element, (4.723, 4.723 + 24.786), (-17.149, -17.149 + 37.149) ) def test_path_Move(self): path = Path("M 10 20") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (10, 10), (20, 20)) def test_path_move(self): path = Path("M 15 30 m 10 20") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (15, 25), (30, 50)) def test_path_Line(self): path = Path("M 15 30 L 10 20") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (10, 15), (20, 30)) def test_path_line(self): path = Path("M 15 30 l 10 20") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (15, 25), (30, 50)) def test_path_Zone(self): path = Path("M 15 30 Z") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (15, 15), (30, 30)) def test_path_Horz(self): path = Path("M 15 30 H 20") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (15, 20), (30, 30)) def test_path_horz(self): path = Path("M 15 30 h 20") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (15, 35), (30, 30)) def test_path_Vert(self): path = Path("M 15 30 V 20") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (15, 15), (20, 30)) def test_path_vert(self): path = Path("M 15 30 v 20") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (15, 15), (30, 50)) def test_path_Curve(self): path = Path("M10 10 C 20 20, 40 20, 50 10") pe = PathElement() pe.path = path self.assert_bounding_box_is_equal(pe, (10, 50), (10, 17.5)) @requires_inkscape def test_path_combined_1(self): path = Path("M 0 0 C 11 14 33 3 85 98 H 84 V 91 L 13 78 C 26 83 65 24 94 77") # path = Path("M 0 0 C 11 14 33 3 85 98") pe = PathElement() pe.path = path ibb = self.get_inkscape_bounding_box(pe) self.assert_bounding_box_is_equal(pe, *ibb, disable_inkscape_check=True) def test_path_TepidQuadratic(self): path = Path("M 10 5 Q 15 30 25 15 T 50 40") pe = PathElement() pe.path = path ibb = (10, 50), (5, 40) self.assert_bounding_box_is_equal(pe, *ibb) def test_path_TepidQuadratic_2(self): path = Path("M 10 5 Q 15 30 25 15 T 50 40 T 15 20") pe = PathElement() pe.path = path ibb = (10, 10 + 43.462), (5, 56) self.assert_bounding_box_is_equal(pe, *ibb) @requires_inkscape def test_random_path_1(self): import random from inkex.paths import ( Line, Vert, Horz, Curve, Move, Arc, Quadratic, TepidQuadratic, Smooth, ZoneClose, ) klasses = (Line, Vert, Horz, Curve, Move, Quadratic) # , ZoneClose, Arc def random_segment(klass): args = [random.randint(1, 100) for _ in range(klass.nargs)] if klass is Arc: args[2] = 0 # random.randint(0, 1) args[3] = 0 # random.randint(0, 1) args[4] = 0 # random.randint(0, 1) return klass(*args) random.seed(2128506) # random.seed(datetime.now()) n_trials = 10 n_elements = 15 for i in range(n_trials): path = Path() path.append(Move(0, 0)) for j in range(n_elements): k = random.choice(klasses) path.append(random_segment(k)) if k is Curve: while random.randint(0, 1) == 1: path.append(random_segment(Smooth)) if k is Quadratic: while random.randint(0, 1) == 1: path.append(random_segment(TepidQuadratic)) pe = PathElement() pe.path = path ibb = self.get_inkscape_bounding_box(pe) self.assert_bounding_box_is_equal(pe, *ibb, disable_inkscape_check=True)