summaryrefslogtreecommitdiffstats
path: root/share/extensions/tests/test_inkex_bounding_box.py
diff options
context:
space:
mode:
Diffstat (limited to 'share/extensions/tests/test_inkex_bounding_box.py')
-rw-r--r--share/extensions/tests/test_inkex_bounding_box.py668
1 files changed, 668 insertions, 0 deletions
diff --git a/share/extensions/tests/test_inkex_bounding_box.py b/share/extensions/tests/test_inkex_bounding_box.py
new file mode 100644
index 0000000..e074476
--- /dev/null
+++ b/share/extensions/tests/test_inkex_bounding_box.py
@@ -0,0 +1,668 @@
+# 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 inkex.utils 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)