Skip to content

Commit

Permalink
Merge pull request #28 from meerk40t/tatarize-bbox
Browse files Browse the repository at this point in the history
Bounding Box Transforms
  • Loading branch information
tatarize authored Dec 22, 2019
2 parents 90922af + 3e0894d commit 909eaf5
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 38 deletions.
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = svg.elements
version = 0.7.3
version = 0.7.4
description = Svg Elements Parsing
long_description_content_type=text/markdown
long_description = file: README.md
Expand Down
126 changes: 90 additions & 36 deletions svg/elements/svg_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -1577,7 +1577,7 @@ def __eq__(self, other):
if isinstance(other, str):
try:
other = Point(other)
except IndexError: # This string doesn't parse to a point.
except IndexError: # This string doesn't parse to a point.
return False
if isinstance(other, (Point, list, tuple)):
b0 = other[0]
Expand Down Expand Up @@ -2509,6 +2509,15 @@ def __imul__(self, other):
self.transform *= other
return self

def __abs__(self):
"""
The absolute value is taken to be the actual shape transformed.
:return: transformed version of the given shape.
"""
m = copy(self)
m.reify()
return m

def reify(self):
"""
Realizes the transform to the attributes. Such that the attributes become actualized and the transform
Expand All @@ -2526,6 +2535,15 @@ def render(self, **kwargs):
self.transform.render(**kwargs)
return self

def bbox(self, transformed=True):
"""
Returns the bounding box of the given object.
:param transformed: whether this is the transformed bounds or default.
:return:
"""
raise NotImplementedError

@property
def rotation(self):
if not self.apply:
Expand Down Expand Up @@ -2636,11 +2654,6 @@ def __iadd__(self, other):

__add__ = __iadd__

def __abs__(self):
m = copy(self)
m.reify()
return m

def __matmul__(self, other):
m = copy(self)
m.__imatmul__(other)
Expand Down Expand Up @@ -2681,7 +2694,19 @@ def d(self, relative=False, transformed=True):
return Path(self.segments(transformed=transformed)).d(relative=relative)

def bbox(self, transformed=True):
return Path(self).bbox(transformed=transformed)
"""
Get the bounding box for the given shape.
"""
bbs = [seg.bbox() for seg in self.segments(transformed=transformed) if not isinstance(Close, Move)]
try:
xmins, ymins, xmaxs, ymaxs = list(zip(*bbs))
except ValueError:
return None # No bounding box items existed. So no bounding box.
xmin = min(xmins)
xmax = max(xmaxs)
ymin = min(ymins)
ymax = max(ymaxs)
return xmin, ymin, xmax, ymax

def _init_shape(self, *args):
"""
Expand Down Expand Up @@ -4995,19 +5020,6 @@ def as_points(self):
else:
yield p

def bbox(self, transformed=True):
"""returns a bounding box for the input Path"""
bbs = [seg.bbox() for seg in self.segments(transformed=transformed) if not isinstance(Close, Move)]
try:
xmins, ymins, xmaxs, ymaxs = list(zip(*bbs))
except ValueError:
return None # No bounding box items existed. So no bounding box.
xmin = min(xmins)
xmax = max(xmaxs)
ymin = min(ymins)
ymax = max(ymaxs)
return xmin, ymin, xmax, ymax

def reify(self):
"""
Realizes the transform to the shape properties.
Expand Down Expand Up @@ -5299,21 +5311,21 @@ def segments(self, transformed=True):
ry = self.ry
if rx == ry == 0:
segments = (Move(None, (x, y)),
Line((x, y), (x + width, y)),
Line((x + width, y), (x + width, y + height)),
Line((x + width, y + height), (x, y + height)),
Close((x, y + height), (x, y)))
Line((x, y), (x + width, y)),
Line((x + width, y), (x + width, y + height)),
Line((x + width, y + height), (x, y + height)),
Close((x, y + height), (x, y)))
else:
segments = (Move(None, (x + rx, y)),
Line((x + rx, y), (x + width - rx, y)),
Arc((x + width - rx, y), (x + width, y + ry), rx=rx, ry=ry),
Line((x + width, y + ry), (x + width, y + height - ry)),
Arc((x + width, y + height - ry), (x + width - rx, y + height), rx=rx, ry=ry),
Line((x + width - rx, y + height), (x + rx, y + height)),
Arc((x + rx, y + height), (x, y + height - ry), rx=rx, ry=ry),
Line((x, y + height - ry), (x, y + ry)),
Arc((x, y + ry), (x + rx, y), rx=rx, ry=ry),
Close((x + rx, y), (x + rx, y)))
Line((x + rx, y), (x + width - rx, y)),
Arc((x + width - rx, y), (x + width, y + ry), rx=rx, ry=ry),
Line((x + width, y + ry), (x + width, y + height - ry)),
Arc((x + width, y + height - ry), (x + width - rx, y + height), rx=rx, ry=ry),
Line((x + width - rx, y + height), (x + rx, y + height)),
Arc((x + rx, y + height), (x, y + height - ry), rx=rx, ry=ry),
Line((x, y + height - ry), (x, y + ry)),
Arc((x, y + ry), (x + rx, y), rx=rx, ry=ry),
Close((x + rx, y), (x + rx, y)))
if not transformed or self.transform.is_identity():
return segments
else:
Expand Down Expand Up @@ -5969,8 +5981,8 @@ def segments(self, transformed=True):
current = points[i]
segments.append(Line(last, current))
last = current
if isinstance(self,Polygon):
segments.append(Close(last,points[0]))
if isinstance(self, Polygon):
segments.append(Close(last, points[0]))
return segments

def reify(self):
Expand Down Expand Up @@ -6216,6 +6228,11 @@ def reverse(self):
class SVGText(GraphicObject, Transformable):
"""
SVG Text are defined in SVG 2.0 Chapter 11
No methods are implemented to perform a text to path conversion.
However, if such a method exists the assumption is that the results will be
placed in the path attribute, and functions like bbox() will check if such
a value exists.
"""

def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -6252,6 +6269,7 @@ def __init__(self, *args, **kwargs):
self.y = Length(values.get(SVG_ATTR_Y, self.y)).value()
self.dx = Length(values.get(SVG_ATTR_DX, self.dx)).value()
self.dy = Length(values.get(SVG_ATTR_DY, self.dy)).value()
self.path = None

def _set_values_by_dict(self, values):
if SVG_TAG_TEXT in values:
Expand Down Expand Up @@ -6295,6 +6313,14 @@ def render(self, width=None, height=None, relative_length=None, **kwargs):
self.dy = self.dy.value(relative_length=height, **kwargs)
return self

def bbox(self, transformed=True):
"""
Get the bounding box for the given text object.
"""
if self.path is not None:
return (self.path * self.transform).bbox(transformed=True)
return self.x, self.y, self.x, self.y


class SVGDesc:
"""
Expand Down Expand Up @@ -6410,6 +6436,34 @@ def set_values_by_image(self):
viewbox_transform = self.viewbox.transform()
self.transform = Matrix(viewbox_transform) * self.transform

def bbox(self, transformed=True):
"""
Get the bounding box for the given image object
"""
if self.image_width is None or self.image_height is None:
p = Point(0, 0)
p *= self.transform
return p[0], p[1], p[0], p[1]
width = self.image_width
height = self.image_height
if transformed:
p = (Point(0, 0) * self.transform,
Point(width, 0) * self.transform,
Point(width, height) * self.transform,
Point(0, height) * self.transform)
else:
p = (Point(0, 0),
Point(width, 0),
Point(width, height),
Point(0, height))
x_vals = list(s[0] for s in p)
y_vals = list(s[1] for s in p)
min_x = min(x_vals)
min_y = min(y_vals)
max_x = max(x_vals)
max_y = max(y_vals)
return min_x, min_y, max_x, max_y


class Viewbox:

Expand Down Expand Up @@ -6549,7 +6603,7 @@ def viewbox_transform(e_x, e_y, e_width, e_height, vb_x, vb_y, vb_width, vb_heig
# Let align be the align value of preserveAspectRatio, or 'xMidYMid' if preserveAspectRatio is not defined.
# Let meetOrSlice be the meetOrSlice value of preserveAspectRatio, or 'meet' if preserveAspectRatio is not defined
# or if meetOrSlice is missing from this value.
if e_x is None or e_y is None or e_width is None or e_height is None or\
if e_x is None or e_y is None or e_width is None or e_height is None or \
vb_x is None or vb_y is None or vb_width is None or vb_height is None:
return ''
if aspect is not None:
Expand Down
44 changes: 43 additions & 1 deletion test/test_shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,14 +304,56 @@ def test_shapes_repr(self):
s = Path(fill='red')
self.assertEqual(repr(s), "Path(fill='#ff0000')")

def test_shape_bbox(self):
s = Rect() * 'scale(20)'
self.assertEqual(s.bbox(False), (0, 0, 1, 1))
self.assertEqual(s.bbox(True), (0, 0, 20, 20))
self.assertNotEqual(s.bbox(False), (0, 0, 20, 20))
self.assertNotEqual(s.bbox(True), (0, 0, 1, 1))
s = Circle() * 'scale(20)'
self.assertEqual(s.bbox(False), (-1, -1, 1, 1))
self.assertEqual(s.bbox(True), (-20, -20, 20, 20))
self.assertNotEqual(s.bbox(False), (-20, -20, 20, 20))
self.assertNotEqual(s.bbox(True), (-1, -1, 1, 1))
s = Ellipse() * 'scale(20)'
self.assertEqual(s.bbox(False), (-1, -1, 1, 1))
self.assertEqual(s.bbox(True), (-20, -20, 20, 20))
self.assertNotEqual(s.bbox(False), (-20, -20, 20, 20))
self.assertNotEqual(s.bbox(True), (-1, -1, 1, 1))
s = Polygon() * 'scale(20)'
self.assertEqual(s.bbox(False), None)
self.assertEqual(s.bbox(True), None)
self.assertNotEqual(s.bbox(False), (0, 0, 0, 0))
self.assertNotEqual(s.bbox(True), (0, 0, 0, 0))
s = Polyline() * 'scale(20)'
self.assertEqual(s.bbox(False), None)
self.assertEqual(s.bbox(True), None)
self.assertNotEqual(s.bbox(False), (0, 0, 0, 0))
self.assertNotEqual(s.bbox(True), (0, 0, 0, 0))
s = Polygon("0,0 0,1 1,1 1,0 0,0") * 'scale(20)'
self.assertEqual(s.bbox(False), (0, 0, 1, 1))
self.assertEqual(s.bbox(True), (0, 0, 20, 20))
self.assertNotEqual(s.bbox(False), (0, 0, 20, 20))
self.assertNotEqual(s.bbox(True), (0, 0, 1, 1))
s = Polyline("0,0 0,1 1,1 1,0 0,0") * 'scale(20)'
self.assertEqual(s.bbox(False), (0, 0, 1, 1))
self.assertEqual(s.bbox(True), (0, 0, 20, 20))
self.assertNotEqual(s.bbox(False), (0, 0, 20, 20))
self.assertNotEqual(s.bbox(True), (0, 0, 1, 1))
s = SimpleLine(0, 0, 1, 1) * 'scale(20)'
self.assertEqual(s.bbox(False), (0, 0, 1, 1))
self.assertEqual(s.bbox(True), (0, 0, 20, 20))
self.assertNotEqual(s.bbox(False), (0, 0, 20, 20))
self.assertNotEqual(s.bbox(True), (0, 0, 1, 1))

def test_rect_rot_equal_rect_path_rotate(self):
r = Rect(10, 10, 8, 4)
a = r.d()
b = Path(a).d()
self.assertEqual(a, b)
a = (Path(r.d()) * "rotate(0.5turns)").d()
b = (r * "rotate(0.5turns)").d()
self.assertEqual(a,b)
self.assertEqual(a, b)

def test_rect_reify(self):
"""Reifying a rotated rect."""
Expand Down

0 comments on commit 909eaf5

Please sign in to comment.