From 7ebc56a831357379ff22216bec07e2c12e8c5bc6 Mon Sep 17 00:00:00 2001 From: Andy Port Date: Tue, 21 Aug 2018 20:54:02 -0700 Subject: [PATCH] aesthetic cleanup --- svgpathtools/document.py | 161 ++++++++++++++++++++++++--------------- svgpathtools/path.py | 99 ++++++++++++------------ test/test_groups.py | 108 +++++++++++++++++--------- test/test_parsing.py | 118 +++++++++++++++++----------- 4 files changed, 299 insertions(+), 187 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index de32554..cd40761 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -20,7 +20,8 @@ Example: >> path = result.path >> # Do something with the transformed Path object. >> element = result.element - >> # Inspect the raw SVG element. This gives access to the path's attributes + >> # Inspect the raw SVG element. This gives access to the + >> # path's attributes >> transform = result.transform >> # Use the transform that was applied to the path. >> foo(doc.tree) # do stuff using ElementTree's functionality @@ -38,14 +39,14 @@ from __future__ import division, absolute_import, print_function import os import collections import xml.etree.ElementTree as etree -from xml.etree.ElementTree import Element, SubElement, register_namespace, _namespace_map +from xml.etree.ElementTree import Element, SubElement, register_namespace import warnings # Internal dependencies from .parser import parse_path from .parser import parse_transform -from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd, polyline2pathd, - polygon2pathd, rect2pathd) +from .svg_to_paths import (path2pathd, ellipse2pathd, line2pathd, + polyline2pathd, polygon2pathd, rect2pathd) from .misctools import open_in_browser from .path import * @@ -73,58 +74,71 @@ CONVERT_ONLY_PATHS = {'path': path2pathd} SVG_GROUP_TAG = 'svg:g' -def flatten_all_paths( - group, - group_filter=lambda x: True, - path_filter=lambda x: True, - path_conversions=CONVERSIONS, - group_search_xpath=SVG_GROUP_TAG): - """Returns the paths inside a group (recursively), expressing the paths in the base coordinates. +def flatten_all_paths(group, group_filter=lambda x: True, + path_filter=lambda x: True, path_conversions=CONVERSIONS, + group_search_xpath=SVG_GROUP_TAG): + """Returns the paths inside a group (recursively), expressing the + paths in the base coordinates. - Note that if the group being passed in is nested inside some parent group(s), we cannot take the parent group(s) - into account, because xml.etree.Element has no pointer to its parent. You should use Document.flatten_group(group) - to flatten a specific nested group into the root coordinates. + Note that if the group being passed in is nested inside some parent + group(s), we cannot take the parent group(s) into account, because + xml.etree.Element has no pointer to its parent. You should use + Document.flatten_group(group) to flatten a specific nested group into + the root coordinates. Args: group is an Element - path_conversions (dict): A dictionary to convert from an SVG element to a path data string. Any element tags - that are not included in this dictionary will be ignored (including the `path` tag). - To only convert explicit path elements, pass in path_conversions=CONVERT_ONLY_PATHS. + path_conversions (dict): + A dictionary to convert from an SVG element to a path data + string. Any element tags that are not included in this + dictionary will be ignored (including the `path` tag). To + only convert explicit path elements, pass in + `path_conversions=CONVERT_ONLY_PATHS`. """ if not isinstance(group, Element): - raise TypeError('Must provide an xml.etree.Element object. Instead you provided {0}'.format(type(group))) + raise TypeError('Must provide an xml.etree.Element object. ' + 'Instead you provided {0}'.format(type(group))) # Stop right away if the group_selector rejects this group if not group_filter(group): return [] - # To handle the transforms efficiently, we'll traverse the tree of groups depth-first using a stack of tuples. - # The first entry in the tuple is a group element and the second entry is its transform. As we pop each entry in - # the stack, we will add all its child group elements to the stack. - StackElement = collections.namedtuple('StackElement', ['group', 'transform']) + # To handle the transforms efficiently, we'll traverse the tree of + # groups depth-first using a stack of tuples. + # The first entry in the tuple is a group element and the second + # entry is its transform. As we pop each entry in the stack, we + # will add all its child group elements to the stack. + StackElement = collections.namedtuple('StackElement', + ['group', 'transform']) def new_stack_element(element, last_tf): - return StackElement(element, last_tf.dot(parse_transform(element.get('transform')))) + return StackElement(element, last_tf.dot( + parse_transform(element.get('transform')))) def get_relevant_children(parent, last_tf): children = [] - for elem in filter(group_filter, parent.iterfind(group_search_xpath, SVG_NAMESPACE)): + for elem in filter(group_filter, + parent.iterfind(group_search_xpath, SVG_NAMESPACE)): children.append(new_stack_element(elem, last_tf)) return children stack = [new_stack_element(group, np.identity(3))] - FlattenedPath = collections.namedtuple('FlattenedPath', ['path', 'element', 'transform']) + FlattenedPath = collections.namedtuple('FlattenedPath', + ['path', 'element', 'transform']) paths = [] while stack: top = stack.pop() - # For each element type that we know how to convert into path data, parse the element after confirming that - # the path_filter accepts it. + # For each element type that we know how to convert into path + # data, parse the element after confirming that the path_filter + # accepts it. for key, converter in path_conversions.items(): - for path_elem in filter(path_filter, top.group.iterfind('svg:'+key, SVG_NAMESPACE)): - path_tf = top.transform.dot(parse_transform(path_elem.get('transform'))) + for path_elem in filter(path_filter, top.group.iterfind( + 'svg:'+key, SVG_NAMESPACE)): + path_tf = top.transform.dot( + parse_transform(path_elem.get('transform'))) path = transform(parse_path(converter(path_elem)), path_tf) paths.append(FlattenedPath(path, path_elem, path_tf)) @@ -143,16 +157,20 @@ def flatten_group( group_search_xpath=SVG_GROUP_TAG): """Flatten all the paths in a specific group. - The paths will be flattened into the 'root' frame. Note that root needs to be - an ancestor of the group that is being flattened. Otherwise, no paths will be returned.""" + The paths will be flattened into the 'root' frame. Note that root + needs to be an ancestor of the group that is being flattened. + Otherwise, no paths will be returned.""" if not any(group_to_flatten is descendant for descendant in root.iter()): - warnings.warn('The requested group_to_flatten is not a descendant of root') - # We will shortcut here, because it is impossible for any paths to be returned anyhow. + warnings.warn('The requested group_to_flatten is not a ' + 'descendant of root') + # We will shortcut here, because it is impossible for any paths + # to be returned anyhow. return [] - # We create a set of the unique IDs of each element that we wish to flatten, if those elements are groups. - # Any groups outside of this set will be skipped while we flatten the paths. + # We create a set of the unique IDs of each element that we wish to + # flatten, if those elements are groups. Any groups outside of this + # set will be skipped while we flatten the paths. desired_groups = set() if recursive: for group in group_to_flatten.iter(): @@ -163,7 +181,8 @@ def flatten_group( def desired_group_filter(x): return (id(x) in desired_groups) and group_filter(x) - return flatten_all_paths(root, desired_group_filter, path_filter, path_conversions, group_search_xpath) + return flatten_all_paths(root, desired_group_filter, path_filter, + path_conversions, group_search_xpath) class Document: @@ -199,8 +218,10 @@ class Document: group_filter=lambda x: True, path_filter=lambda x: True, path_conversions=CONVERSIONS): - """Forward the tree of this document into the more general flatten_all_paths function and return the result.""" - return flatten_all_paths(self.tree.getroot(), group_filter, path_filter, path_conversions) + """Forward the tree of this document into the more general + flatten_all_paths function and return the result.""" + return flatten_all_paths(self.tree.getroot(), group_filter, + path_filter, path_conversions) def flatten_group(self, group, @@ -209,33 +230,40 @@ class Document: path_filter=lambda x: True, path_conversions=CONVERSIONS): if all(isinstance(s, str) for s in group): - # If we're given a list of strings, assume it represents a nested sequence + # If we're given a list of strings, assume it represents a + # nested sequence group = self.get_or_add_group(group) elif not isinstance(group, Element): - raise TypeError('Must provide a list of strings that represent a nested group name, ' - 'or provide an xml.etree.Element object. Instead you provided {0}'.format(group)) + raise TypeError( + 'Must provide a list of strings that represent a nested ' + 'group name, or provide an xml.etree.Element object. ' + 'Instead you provided {0}'.format(group)) - return flatten_group(group, self.tree.getroot(), recursive, group_filter, path_filter, path_conversions) + return flatten_group(group, self.tree.getroot(), recursive, + group_filter, path_filter, path_conversions) def add_path(self, path, attribs=None, group=None): """Add a new path to the SVG.""" - # If we are not given a parent, assume that the path does not have a group + # If not given a parent, assume that the path does not have a group if group is None: group = self.tree.getroot() - # If we are given a list of strings (one or more), assume it represents a sequence of nested group names + # If given a list of strings (one or more), assume it represents + # a sequence of nested group names elif all(isinstance(elem, str) for elem in group): group = self.get_or_add_group(group) elif not isinstance(group, Element): - raise TypeError('Must provide a list of strings or an xml.etree.Element object. ' - 'Instead you provided {0}'.format(group)) + raise TypeError( + 'Must provide a list of strings or an xml.etree.Element ' + 'object. Instead you provided {0}'.format(group)) else: # Make sure that the group belongs to this Document object if not self.contains_group(group): - warnings.warn('The requested group does not belong to this Document') + warnings.warn('The requested group does not belong to ' + 'this Document') # TODO: It might be better to use duck-typing here with a try-except if isinstance(path, Path): @@ -243,11 +271,13 @@ class Document: elif is_path_segment(path): path_svg = Path(path).d() elif isinstance(path, str): - # Assume this is a valid d-string. TODO: Should we sanity check the input string? + # Assume this is a valid d-string. + # TODO: Should we sanity check the input string? path_svg = path else: - raise TypeError('Must provide a Path, a path segment type, or a valid SVG path d-string. ' - 'Instead you provided {0}'.format(path)) + raise TypeError( + 'Must provide a Path, a path segment type, or a valid ' + 'SVG path d-string. Instead you provided {0}'.format(path)) if attribs is None: attribs = {} @@ -262,16 +292,20 @@ class Document: return any(group is owned for owned in self.tree.iter()) def get_or_add_group(self, nested_names, name_attr='id'): - """Get a group from the tree, or add a new one with the given name structure. + """Get a group from the tree, or add a new one with the given + name structure. - *nested_names* is a list of strings which represent group names. Each group name will be nested inside of the - previous group name. + `nested_names` is a list of strings which represent group names. + Each group name will be nested inside of the previous group name. - *name_attr* is the group attribute that is being used to represent the group's name. Default is 'id', but some - SVGs may contain custom name labels, like 'inkscape:label'. + `name_attr` is the group attribute that is being used to + represent the group's name. Default is 'id', but some SVGs may + contain custom name labels, like 'inkscape:label'. - Returns the requested group. If the requested group did not exist, this function will create it, as well as all - parent groups that it requires. All created groups will be left with blank attributes. + Returns the requested group. If the requested group did not + exist, this function will create it, as well as all parent + groups that it requires. All created groups will be left with + blank attributes. """ group = self.tree.getroot() @@ -285,14 +319,15 @@ class Document: break if prev_group is group: - # The group we're looking for does not exist, so let's create the group structure + # The group we're looking for does not exist, so let's + # create the group structure nested_names.insert(0, next_name) while nested_names: next_name = nested_names.pop(0) group = self.add_group({'id': next_name}, group) - # Now nested_names will be empty, so the topmost while-loop will end - + # Now nested_names will be empty, so the topmost + # while-loop will end return group def add_group(self, group_attribs=None, parent=None): @@ -300,14 +335,16 @@ class Document: if parent is None: parent = self.tree.getroot() elif not self.contains_group(parent): - warnings.warn('The requested group {0} does not belong to this Document'.format(parent)) + warnings.warn('The requested group {0} does not belong to ' + 'this Document'.format(parent)) if group_attribs is None: group_attribs = {} else: group_attribs = group_attribs.copy() - return SubElement(parent, '{{{0}}}g'.format(SVG_NAMESPACE['svg']), group_attribs) + return SubElement(parent, '{{{0}}}g'.format( + SVG_NAMESPACE['svg']), group_attribs) def save(self, filename=None): if filename is None: diff --git a/svgpathtools/path.py b/svgpathtools/path.py index a4e7531..b39c5cd 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -269,7 +269,8 @@ def transform(curve, tf): if isinstance(curve, Path): return Path(*[transform(segment, tf) for segment in curve]) elif is_bezier_segment(curve): - return bpoints2bezier([to_complex(tf.dot(to_point(p))) for p in curve.bpoints()]) + return bpoints2bezier([to_complex(tf.dot(to_point(p))) + for p in curve.bpoints()]) elif isinstance(curve, Arc): new_start = to_complex(tf.dot(to_point(curve.start))) new_end = to_complex(tf.dot(to_point(curve.end))) @@ -458,12 +459,15 @@ def inv_arclength(curve, s, s_tol=ILENGTH_S_TOL, maxits=ILENGTH_MAXITS, return 1 if isinstance(curve, Path): - seg_lengths = [seg.length(error=error, min_depth=min_depth) for seg in curve] + seg_lengths = [seg.length(error=error, min_depth=min_depth) + for seg in curve] lsum = 0 # Find which segment the point we search for is located on for k, len_k in enumerate(seg_lengths): if lsum <= s <= lsum + len_k: - t = inv_arclength(curve[k], s - lsum, s_tol=s_tol, maxits=maxits, error=error, min_depth=min_depth) + t = inv_arclength(curve[k], s - lsum, s_tol=s_tol, + maxits=maxits, error=error, + min_depth=min_depth) return curve.t2T(k, t) lsum += len_k return 1 @@ -1694,7 +1698,6 @@ class Arc(object): xmin = max(xtrema) return min(xtrema), max(xtrema), min(ytrema), max(ytrema) - def split(self, t): """returns two segments, whose union is this segment and which join at self.point(t).""" @@ -1716,48 +1719,46 @@ class Arc(object): and maximize, respectively, the distance, d = |self.point(t)-origin|.""" - u1orig = self.u1transform(origin) - if abs(u1orig) == 1: # origin lies on ellipse - t = self.phase2t(phase(u1orig)) - d_min = 0 - - # Transform to a coordinate system where the ellipse is centered - # at the origin and its axes are horizontal/vertical - zeta0 = self.centeriso(origin) - a, b = self.radius.real, self.radius.imag - x0, y0 = zeta0.real, zeta0.imag - - # Find t s.t. z'(t) - a2mb2 = (a**2 - b**2) - if u1orig.imag: # x != x0 - - coeffs = [a2mb2**2, - 2*a2mb2*b**2*y0, - (-a**4 + (2*a**2 - b**2 + y0**2)*b**2 + x0**2)*b**2, - -2*a2mb2*b**4*y0, - -b**6*y0**2] - ys = polyroots(coeffs, realroots=True, - condition=lambda r: -b <= r <= b) - xs = (a*sqrt(1 - y**2/b**2) for y in ys) - - - - ts = [self.phase2t(phase(self.u1transform(self.icenteriso( - complex(x, y))))) for x, y in zip(xs, ys)] - - else: # This case is very similar, see notes and assume instead y0!=y - b2ma2 = (b**2 - a**2) - coeffs = [b2ma2**2, - 2*b2ma2*a**2*x0, - (-b**4 + (2*b**2 - a**2 + x0**2)*a**2 + y0**2)*a**2, - -2*b2ma2*a**4*x0, - -a**6*x0**2] - xs = polyroots(coeffs, realroots=True, - condition=lambda r: -a <= r <= a) - ys = (b*sqrt(1 - x**2/a**2) for x in xs) - - ts = [self.phase2t(phase(self.u1transform(self.icenteriso( - complex(x, y))))) for x, y in zip(xs, ys)] + # u1orig = self.u1transform(origin) + # if abs(u1orig) == 1: # origin lies on ellipse + # t = self.phase2t(phase(u1orig)) + # d_min = 0 + # + # # Transform to a coordinate system where the ellipse is centered + # # at the origin and its axes are horizontal/vertical + # zeta0 = self.centeriso(origin) + # a, b = self.radius.real, self.radius.imag + # x0, y0 = zeta0.real, zeta0.imag + # + # # Find t s.t. z'(t) + # a2mb2 = (a**2 - b**2) + # if u1orig.imag: # x != x0 + # + # coeffs = [a2mb2**2, + # 2*a2mb2*b**2*y0, + # (-a**4 + (2*a**2 - b**2 + y0**2)*b**2 + x0**2)*b**2, + # -2*a2mb2*b**4*y0, + # -b**6*y0**2] + # ys = polyroots(coeffs, realroots=True, + # condition=lambda r: -b <= r <= b) + # xs = (a*sqrt(1 - y**2/b**2) for y in ys) + # + # ts = [self.phase2t(phase(self.u1transform(self.icenteriso( + # complex(x, y))))) for x, y in zip(xs, ys)] + # + # else: # This case is very similar, see notes and assume instead y0!=y + # b2ma2 = (b**2 - a**2) + # coeffs = [b2ma2**2, + # 2*b2ma2*a**2*x0, + # (-b**4 + (2*b**2 - a**2 + x0**2)*a**2 + y0**2)*a**2, + # -2*b2ma2*a**4*x0, + # -a**6*x0**2] + # xs = polyroots(coeffs, realroots=True, + # condition=lambda r: -a <= r <= a) + # ys = (b*sqrt(1 - x**2/a**2) for x in xs) + # + # ts = [self.phase2t(phase(self.u1transform(self.icenteriso( + # complex(x, y))))) for x, y in zip(xs, ys)] raise _NotImplemented4ArcException @@ -2156,7 +2157,8 @@ class Path(MutableSequence): (seg_idx - 1) % len(self._segments)] if not seg.joins_smoothly_with(previous_seg_in_path): return float('inf') - elif np.isclose(t, 1) and (seg_idx != len(self) - 1 or self.end==self.start): + elif np.isclose(t, 1) and (seg_idx != len(self) - 1 or + self.end == self.start): next_seg_in_path = self._segments[ (seg_idx + 1) % len(self._segments)] if not next_seg_in_path.joins_smoothly_with(seg): @@ -2223,7 +2225,8 @@ class Path(MutableSequence): # redundant intersection. This code block checks for and removes said # redundancies. if intersection_list: - pts = [seg1.point(_t1) for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]] + pts = [seg1.point(_t1) + for _T1, _seg1, _t1 in list(zip(*intersection_list))[0]] indices2remove = [] for ind1 in range(len(pts)): for ind2 in range(ind1 + 1, len(pts)): diff --git a/test/test_groups.py b/test/test_groups.py index 2250114..f493524 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -6,13 +6,15 @@ import numpy as np def get_desired_path(name, paths): - return next(p for p in paths if p.element.get('{some://testuri}name') == name) + return next(p for p in paths + if p.element.get('{some://testuri}name') == name) class TestGroups(unittest.TestCase): def check_values(self, v, z): - # Check that the components of 2D vector v match the components of complex number z + # Check that the components of 2D vector v match the components + # of complex number z self.assertAlmostEqual(v[0], z.real) self.assertAlmostEqual(v[1], z.imag) @@ -20,8 +22,10 @@ class TestGroups(unittest.TestCase): # Check that the endpoints of the line have been correctly transformed. # * tf is the transform that should have been applied. # * v_s_vals is a 2D list of the values of the line's start point - # * v_e_relative_vals is a 2D list of the values of the line's end point relative to the start point - # * name is the path name (value of the test:name attribute in the SVG document) + # * v_e_relative_vals is a 2D list of the values of the line's + # end point relative to the start point + # * name is the path name (value of the test:name attribute in + # the SVG document) # * paths is the output of doc.flatten_all_paths() v_s_vals.append(1.0) v_e_relative_vals.append(0.0) @@ -34,22 +38,30 @@ class TestGroups(unittest.TestCase): self.check_values(tf.dot(v_e), actual.path.end) def test_group_flatten(self): - # Test the Document.flatten_all_paths() function against the groups.svg test file. - # There are 12 paths in that file, with various levels of being nested inside of group transforms. - # The check_line function is used to reduce the boilerplate, since all the tests are very similar. - # This test covers each of the different types of transforms that are specified by the SVG standard. + # Test the Document.flatten_all_paths() function against the + # groups.svg test file. + # There are 12 paths in that file, with various levels of being + # nested inside of group transforms. + # The check_line function is used to reduce the boilerplate, + # since all the tests are very similar. + # This test covers each of the different types of transforms + # that are specified by the SVG standard. doc = Document(join(dirname(__file__), 'groups.svg')) result = doc.flatten_all_paths() self.assertEqual(12, len(result)) - tf_matrix_group = np.array([[1.5, 0.0, -40.0], [0.0, 0.5, 20.0], [0.0, 0.0, 1.0]]) + tf_matrix_group = np.array([[1.5, 0.0, -40.0], + [0.0, 0.5, 20.0], + [0.0, 0.0, 1.0]]) self.check_line(tf_matrix_group, [183, 183], [0.0, -50], 'path00', result) - tf_scale_group = np.array([[1.25, 0.0, 0.0], [0.0, 1.25, 0.0], [0.0, 0.0, 1.0]]) + tf_scale_group = np.array([[1.25, 0.0, 0.0], + [0.0, 1.25, 0.0], + [0.0, 0.0, 1.0]]) self.check_line(tf_matrix_group.dot(tf_scale_group), [122, 320], [-50.0, 0.0], @@ -63,19 +75,27 @@ class TestGroups(unittest.TestCase): [150, 200], [-50, 25], 'path03', result) - tf_nested_translate_group = np.array([[1, 0, 20], [0, 1, 0], [0, 0, 1]]) + tf_nested_translate_group = np.array([[1, 0, 20], + [0, 1, 0], + [0, 0, 1]]) - self.check_line(tf_matrix_group.dot(tf_scale_group).dot(tf_nested_translate_group), + self.check_line(tf_matrix_group.dot(tf_scale_group + ).dot(tf_nested_translate_group), [150, 200], [-50, 25], 'path04', result) - tf_nested_translate_xy_group = np.array([[1, 0, 20], [0, 1, 30], [0, 0, 1]]) + tf_nested_translate_xy_group = np.array([[1, 0, 20], + [0, 1, 30], + [0, 0, 1]]) - self.check_line(tf_matrix_group.dot(tf_scale_group).dot(tf_nested_translate_xy_group), + self.check_line(tf_matrix_group.dot(tf_scale_group + ).dot(tf_nested_translate_xy_group), [150, 200], [-50, 25], 'path05', result) - tf_scale_xy_group = np.array([[0.5, 0, 0], [0, 1.5, 0.0], [0, 0, 1]]) + tf_scale_xy_group = np.array([[0.5, 0, 0], + [0, 1.5, 0.0], + [0, 0, 1]]) self.check_line(tf_matrix_group.dot(tf_scale_xy_group), [122, 320], [-50, 0], @@ -92,35 +112,46 @@ class TestGroups(unittest.TestCase): a_08 = 45.0*np.pi/180.0 tf_rotate_xy_group_R = np.array([[np.cos(a_08), -np.sin(a_08), 0], - [np.sin(a_08), np.cos(a_08), 0], - [0, 0, 1]]) - tf_rotate_xy_group_T = np.array([[1, 0, 183], [0, 1, 183], [0, 0, 1]]) - tf_rotate_xy_group = tf_rotate_xy_group_T.dot(tf_rotate_xy_group_R).dot(np.linalg.inv(tf_rotate_xy_group_T)) + [np.sin(a_08), np.cos(a_08), 0], + [0, 0, 1]]) + tf_rotate_xy_group_T = np.array([[1, 0, 183], + [0, 1, 183], + [0, 0, 1]]) + tf_rotate_xy_group = tf_rotate_xy_group_T.dot( + tf_rotate_xy_group_R).dot( + np.linalg.inv(tf_rotate_xy_group_T)) self.check_line(tf_matrix_group.dot(tf_rotate_xy_group), [183, 183], [0, 30], 'path08', result) a_09 = 5.0*np.pi/180.0 - tf_skew_x_group = np.array([[1, np.tan(a_09), 0], [0, 1, 0], [0, 0, 1]]) + tf_skew_x_group = np.array([[1, np.tan(a_09), 0], + [0, 1, 0], + [0, 0, 1]]) self.check_line(tf_matrix_group.dot(tf_skew_x_group), [183, 183], [40, 40], 'path09', result) a_10 = 5.0*np.pi/180.0 - tf_skew_y_group = np.array([[1, 0, 0], [np.tan(a_10), 1, 0], [0, 0, 1]]) + tf_skew_y_group = np.array([[1, 0, 0], + [np.tan(a_10), 1, 0], + [0, 0, 1]]) self.check_line(tf_matrix_group.dot(tf_skew_y_group), [183, 183], [40, 40], 'path10', result) - # This last test is for handling transforms that are defined as attributes of a element. + # This last test is for handling transforms that are defined as + # attributes of a element. a_11 = -40*np.pi/180.0 tf_path11_R = np.array([[np.cos(a_11), -np.sin(a_11), 0], [np.sin(a_11), np.cos(a_11), 0], [0, 0, 1]]) - tf_path11_T = np.array([[1, 0, 100], [0, 1, 100], [0, 0, 1]]) + tf_path11_T = np.array([[1, 0, 100], + [0, 1, 100], + [0, 0, 1]]) tf_path11 = tf_path11_T.dot(tf_path11_R).dot(np.linalg.inv(tf_path11_T)) self.check_line(tf_matrix_group.dot(tf_skew_y_group).dot(tf_path11), @@ -129,13 +160,13 @@ class TestGroups(unittest.TestCase): def check_group_count(self, doc, expected_count): count = 0 - for group in doc.tree.getroot().iter('{{{0}}}g'.format(SVG_NAMESPACE['svg'])): + for _ in doc.tree.getroot().iter('{{{0}}}g'.format(SVG_NAMESPACE['svg'])): count += 1 self.assertEqual(expected_count, count) def test_add_group(self): - # Test the Document.add_group() function and related Document functions. + # Test `Document.add_group()` function and related Document functions. doc = Document(None) self.check_group_count(doc, 0) @@ -161,21 +192,30 @@ class TestGroups(unittest.TestCase): # Test that we can retrieve each new group from the document self.assertEqual(base_group, doc.get_or_add_group(['base_group'])) - self.assertEqual(child_group, doc.get_or_add_group(['base_group', 'child_group'])) - self.assertEqual(grandchild_group, doc.get_or_add_group(['base_group', 'child_group', 'grandchild_group'])) - self.assertEqual(sibling_group, doc.get_or_add_group(['base_group', 'sibling_group'])) + self.assertEqual(child_group, doc.get_or_add_group( + ['base_group', 'child_group'])) + self.assertEqual(grandchild_group, doc.get_or_add_group( + ['base_group', 'child_group', 'grandchild_group'])) + self.assertEqual(sibling_group, doc.get_or_add_group( + ['base_group', 'sibling_group'])) # Create a new nested group - new_child = doc.get_or_add_group(['base_group', 'new_parent', 'new_child']) + new_child = doc.get_or_add_group( + ['base_group', 'new_parent', 'new_child']) self.check_group_count(doc, 6) - self.assertEqual(new_child, doc.get_or_add_group(['base_group', 'new_parent', 'new_child'])) + self.assertEqual(new_child, doc.get_or_add_group( + ['base_group', 'new_parent', 'new_child'])) - new_leaf = doc.get_or_add_group(['base_group', 'new_parent', 'new_child', 'new_leaf']) - self.assertEqual(new_leaf, doc.get_or_add_group(['base_group', 'new_parent', 'new_child', 'new_leaf'])) + new_leaf = doc.get_or_add_group( + ['base_group', 'new_parent', 'new_child', 'new_leaf']) + self.assertEqual(new_leaf, doc.get_or_add_group([ + 'base_group', 'new_parent', 'new_child', 'new_leaf'])) self.check_group_count(doc, 7) - path_d = 'M 206.07112,858.41289 L 206.07112,-2.02031 C -50.738,-81.14814 -20.36402,-105.87055 ' \ - '52.52793,-101.01525 L 103.03556,0.0 L 0.0,111.11678' + path_d = ('M 206.07112,858.41289 L 206.07112,-2.02031 ' + 'C -50.738,-81.14814 -20.36402,-105.87055 52.52793,-101.01525 ' + 'L 103.03556,0.0 ' + 'L 0.0,111.11678') svg_path = doc.add_path(path_d, group=new_leaf) self.assertEqual(path_d, svg_path.get('d')) diff --git a/test/test_parsing.py b/test/test_parsing.py index fbadf07..84648e8 100644 --- a/test/test_parsing.py +++ b/test/test_parsing.py @@ -1,5 +1,4 @@ # Note: This file was taken mostly as is from the svg.path module (v 2.0) -#------------------------------------------------------------------------------ from __future__ import division, absolute_import, print_function import unittest from svgpathtools import * @@ -12,7 +11,8 @@ def construct_rotation_tf(a, x, y): tf_offset = np.identity(3) tf_offset[0:2, 2:3] = np.array([[x], [y]]) tf_rotate = np.identity(3) - tf_rotate[0:2, 0:2] = np.array([[np.cos(a), -np.sin(a)], [np.sin(a), np.cos(a)]]) + tf_rotate[0:2, 0:2] = np.array([[np.cos(a), -np.sin(a)], + [np.sin(a), np.cos(a)]]) tf_offset_neg = np.identity(3) tf_offset_neg[0:2, 2:3] = np.array([[-x], [-y]]) @@ -31,11 +31,10 @@ class TestParser(unittest.TestCase): # for Z command behavior when there is multiple subpaths path1 = parse_path('M 0 0 L 50 20 M 100 100 L 300 100 L 200 300 z') - self.assertEqual(path1, Path( - Line(0 + 0j, 50 + 20j), - Line(100 + 100j, 300 + 100j), - Line(300 + 100j, 200 + 300j), - Line(200 + 300j, 100 + 100j))) + self.assertEqual(path1, Path(Line(0 + 0j, 50 + 20j), + Line(100 + 100j, 300 + 100j), + Line(300 + 100j, 200 + 300j), + Line(200 + 300j, 100 + 100j))) path1 = parse_path('M 100 100 L 200 200') path2 = parse_path('M100 100L200 200') @@ -47,46 +46,68 @@ class TestParser(unittest.TestCase): path1 = parse_path("""M100,200 C100,100 250,100 250,200 S400,300 400,200""") - self.assertEqual(path1, - Path(CubicBezier(100 + 200j, 100 + 100j, 250 + 100j, 250 + 200j), - CubicBezier(250 + 200j, 250 + 300j, 400 + 300j, 400 + 200j))) + self.assertEqual(path1, Path(CubicBezier(100 + 200j, + 100 + 100j, + 250 + 100j, + 250 + 200j), + CubicBezier(250 + 200j, + 250 + 300j, + 400 + 300j, + 400 + 200j))) path1 = parse_path('M100,200 C100,100 400,100 400,200') - self.assertEqual(path1, - Path(CubicBezier(100 + 200j, 100 + 100j, 400 + 100j, 400 + 200j))) + self.assertEqual(path1, Path(CubicBezier(100 + 200j, + 100 + 100j, + 400 + 100j, + 400 + 200j))) path1 = parse_path('M100,500 C25,400 475,400 400,500') - self.assertEqual(path1, - Path(CubicBezier(100 + 500j, 25 + 400j, 475 + 400j, 400 + 500j))) + self.assertEqual(path1, Path(CubicBezier(100 + 500j, + 25 + 400j, + 475 + 400j, + 400 + 500j))) path1 = parse_path('M100,800 C175,700 325,700 400,800') - self.assertEqual(path1, - Path(CubicBezier(100 + 800j, 175 + 700j, 325 + 700j, 400 + 800j))) + self.assertEqual(path1, Path(CubicBezier(100 + 800j, + 175 + 700j, + 325 + 700j, + 400 + 800j))) path1 = parse_path('M600,200 C675,100 975,100 900,200') - self.assertEqual(path1, - Path(CubicBezier(600 + 200j, 675 + 100j, 975 + 100j, 900 + 200j))) + self.assertEqual(path1, Path(CubicBezier(600 + 200j, + 675 + 100j, + 975 + 100j, + 900 + 200j))) path1 = parse_path('M600,500 C600,350 900,650 900,500') - self.assertEqual(path1, - Path(CubicBezier(600 + 500j, 600 + 350j, 900 + 650j, 900 + 500j))) + self.assertEqual(path1, Path(CubicBezier(600 + 500j, + 600 + 350j, + 900 + 650j, + 900 + 500j))) path1 = parse_path("""M600,800 C625,700 725,700 750,800 S875,900 900,800""") - self.assertEqual(path1, - Path(CubicBezier(600 + 800j, 625 + 700j, 725 + 700j, 750 + 800j), - CubicBezier(750 + 800j, 775 + 900j, 875 + 900j, 900 + 800j))) + self.assertEqual(path1, Path(CubicBezier(600 + 800j, + 625 + 700j, + 725 + 700j, + 750 + 800j), + CubicBezier(750 + 800j, + 775 + 900j, + 875 + 900j, + 900 + 800j))) path1 = parse_path('M200,300 Q400,50 600,300 T1000,300') - self.assertEqual(path1, - Path(QuadraticBezier(200 + 300j, 400 + 50j, 600 + 300j), - QuadraticBezier(600 + 300j, 800 + 550j, 1000 + 300j))) + self.assertEqual(path1, Path(QuadraticBezier(200 + 300j, + 400 + 50j, + 600 + 300j), + QuadraticBezier(600 + 300j, + 800 + 550j, + 1000 + 300j))) path1 = parse_path('M300,200 h-150 a150,150 0 1,0 150,-150 z') - self.assertEqual(path1, - Path(Line(300 + 200j, 150 + 200j), - Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j), - Line(300 + 50j, 300 + 200j))) + self.assertEqual(path1, Path(Line(300 + 200j, 150 + 200j), + Arc(150 + 200j, 150 + 150j, 0, 1, 0, 300 + 50j), + Line(300 + 50j, 300 + 200j))) path1 = parse_path('M275,175 v-150 a150,150 0 0,0 -150,150 z') self.assertEqual(path1, @@ -115,26 +136,32 @@ class TestParser(unittest.TestCase): # Relative moveto: path1 = parse_path('M 0 0 L 50 20 m 50 80 L 300 100 L 200 300 z') - self.assertEqual(path1, Path( - Line(0 + 0j, 50 + 20j), - Line(100 + 100j, 300 + 100j), - Line(300 + 100j, 200 + 300j), - Line(200 + 300j, 100 + 100j))) + self.assertEqual(path1, Path(Line(0 + 0j, 50 + 20j), + Line(100 + 100j, 300 + 100j), + Line(300 + 100j, 200 + 300j), + Line(200 + 300j, 100 + 100j))) # Initial smooth and relative CubicBezier path1 = parse_path("""M100,200 s 150,-100 150,0""") self.assertEqual(path1, - Path(CubicBezier(100 + 200j, 100 + 200j, 250 + 100j, 250 + 200j))) + Path(CubicBezier(100 + 200j, + 100 + 200j, + 250 + 100j, + 250 + 200j))) # Initial smooth and relative QuadraticBezier path1 = parse_path("""M100,200 t 150,0""") self.assertEqual(path1, - Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j))) + Path(QuadraticBezier(100 + 200j, + 100 + 200j, + 250 + 200j))) # Relative QuadraticBezier path1 = parse_path("""M100,200 q 0,0 150,0""") self.assertEqual(path1, - Path(QuadraticBezier(100 + 200j, 100 + 200j, 250 + 200j))) + Path(QuadraticBezier(100 + 200j, + 100 + 200j, + 250 + 200j))) def test_negative(self): """You don't need spaces before a minus-sign""" @@ -144,20 +171,24 @@ class TestParser(unittest.TestCase): def test_numbers(self): """Exponents and other number format cases""" - # It can be e or E, the plus is optional, and a minimum of +/-3.4e38 must be supported. + # It can be e or E, the plus is optional, and a minimum of + # +/-3.4e38 must be supported. path1 = parse_path('M-3.4e38 3.4E+38L-3.4E-38,3.4e-38') path2 = Path(Line(-3.4e+38 + 3.4e+38j, -3.4e-38 + 3.4e-38j)) self.assertEqual(path1, path2) def test_errors(self): - self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200') + self.assertRaises(ValueError, parse_path, + 'M 100 100 L 200 200 Z 100 200') def test_transform(self): - tf_matrix = svgpathtools.parser.parse_transform('matrix(1.0 2.0 3.0 4.0 5.0 6.0)') + tf_matrix = svgpathtools.parser.parse_transform( + 'matrix(1.0 2.0 3.0 4.0 5.0 6.0)') expected_tf_matrix = np.identity(3) - expected_tf_matrix[0:2, 0:3] = np.array([[1.0, 3.0, 5.0], [2.0, 4.0, 6.0]]) + expected_tf_matrix[0:2, 0:3] = np.array([[1.0, 3.0, 5.0], + [2.0, 4.0, 6.0]]) self.assertTrue(np.array_equal(expected_tf_matrix, tf_matrix)) # Try a test with no y specified @@ -170,7 +201,8 @@ class TestParser(unittest.TestCase): # Now specify y expected_tf_translate[1, 2] = 45.5 - tf_translate = svgpathtools.parser.parse_transform('translate(-36 45.5)') + tf_translate = svgpathtools.parser.parse_transform( + 'translate(-36 45.5)') self.assertTrue(np.array_equal(expected_tf_translate, tf_translate)) # Try a test with no y specified