From f2897f6b792a0c03ee7a291d7ae7f4973122887b Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Mon, 7 May 2018 18:29:19 -0700 Subject: [PATCH 01/18] Make the Document class available when importing the library --- svgpathtools/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/svgpathtools/__init__.py b/svgpathtools/__init__.py index 60d2561..207b33a 100644 --- a/svgpathtools/__init__.py +++ b/svgpathtools/__init__.py @@ -12,6 +12,7 @@ from .paths2svg import disvg, wsvg from .polytools import polyroots, polyroots01, rational_limit, real, imag from .misctools import hex2rgb, rgb2hex from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks +from .document import Document try: from .svg2paths import svg2paths, svg2paths2 From 9370d3f5a277463322fbc16669c571baa6df1505 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Mon, 7 May 2018 18:30:15 -0700 Subject: [PATCH 02/18] Add a method to parse transform strings --- svgpathtools/parser.py | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index 9e7c22b..24fc4cd 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -5,6 +5,8 @@ Note: This file was taken (nearly) as is from the svg.path module (v 2.0).""" # External dependencies from __future__ import division, absolute_import, print_function import re +import numpy as np +import warnings # Internal dependencies from .path import Path, Line, QuadraticBezier, CubicBezier, Arc @@ -197,3 +199,83 @@ def parse_path(pathdef, current_pos=0j, tree_element=None): current_pos = end return segments + + +def _check_num_parsed_values(values, allowed): + if not any( num == len(values) for num in allowed): + if len(allowed) > 1: + warnings.warn('Expected one of the following number of values {0}, found {1}: {2}' + .format(allowed, len(values), values)) + elif allowed != 1: + warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed, len(values), values)) + else: + warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values)) + + +def _parse_transform_substr(transform_substr): + value_str = transform_substr.split('(')[1] + values = list(map(float, value_str.split(','))) + transform = np.identity(3) + if 'matrix' in transform_substr: + _check_num_parsed_values(values, 6) + + transform[0:2, 0:3] = np.matrix([values[0:3], values[3:6]]) + + elif 'translate' in transform_substr: + _check_num_parsed_values(values, [1, 2]) + + transform[0, 2] = values[0] + if len(values) > 1: + transform[1, 2] = values[1] + + elif 'scale' in transform_substr: + _check_num_parsed_values(values, [1, 2]) + + x_scale = values[0] + if len(values) > 1: + y_scale = values[1] + else: + y_scale = x_scale # y_scale is assumed to equal x_scale if only one value is provided + transform[0, 0] = x_scale + transform[1, 1] = y_scale + + elif 'rotate' in transform_substr: + _check_num_parsed_values(values, [1, 3]) + + angle = values[0] + if len(values) == 3: + x_offset = values[1] + y_offset = values[2] + else: + x_offset = 0 + y_offset = 0 + T_offset = np.identity(3) + T_offset[0:2, 2] = np.matrix([[x_offset], [y_offset]]) + T_rotate = np.identity(3) + T_rotate[0:2, 0:2] = np.matrix([np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]) + + transform = T_offset * T_rotate * (-T_offset) + + elif 'skewX' in transform_substr: + _check_num_parsed_values(values, 1) + transform[0, 1] = np.tan(values[0]) + + elif 'skewY' in transform_substr: + _check_num_parsed_values(values, 1) + transform[1, 0] = np.tan(values[0]) + + return transform + + +def parse_transform(transform_str): + """Converts a valid SVG tranformation string into a 3x3 matrix. + If the string is empty or null, this returns a 3x3 identity matrix""" + if not transform_str: + return np.identity(3) + elif not isinstance(transform_str, basestring): + raise TypeError('Must provide a string to parse') + + total_transform = np.identity(3) + transform_substrs = transform_str.split(')') + for substr in transform_substrs: + total_transform *= _parse_transform_substr(substr) From 17d283abe0b42a2dcc60c4dc4131dbd9688b54f4 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Mon, 7 May 2018 18:31:06 -0700 Subject: [PATCH 03/18] Iterate on the implementation of the Document class --- svgpathtools/document.py | 94 +++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index 8065aa6..21413b4 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -52,12 +52,15 @@ A Big Problem: from __future__ import division, absolute_import, print_function import os import xml.etree.cElementTree as etree +import xml.etree.ElementTree.Element as Element +import xml.etree.ElementTree.SubElement as SubElement # Internal dependencies from .parser import parse_path from .svg2paths import (ellipse2pathd, line2pathd, polyline2pathd, polygon2pathd, rect2pathd) from .misctools import open_in_browser +from .path import * # THESE MUST BE WRAPPED TO OUPUT ElementTree.element objects CONVERSIONS = {'circle': ellipse2pathd, @@ -68,13 +71,21 @@ CONVERSIONS = {'circle': ellipse2pathd, 'rect': rect2pathd} +def flatten_group_transforms(group): + """Returns a 3x3 matrix which can transform points on a path from a group frame to the root frame""" + if not isinstance(group, Element): + raise TypeError('Must provide an xml.etree.Element object') + + + + class Document: def __init__(self, filename, conversions=False, transform_paths=True): """(EXPERIMENTAL) A container for a DOM-style document. The `Document` class provides a simple interface to modify and analyze the path elements in a DOM-style document. The DOM-style document is - parsed into an ElementTree object (stored in the `tree` attribute and + parsed into an ElementTree object (stored in the `tree` attribute) and all SVG-Path (and, optionally, Path-like) elements are extracted into a list of svgpathtools Path objects. For more information on "Path-like" objects, see the below explanation of the `conversions` argument. @@ -110,7 +121,6 @@ class Document: self._prefix = '' # etree.register_namespace('', prefix) - self.paths = self._get_paths(conversions) def get_elements_by_tag(self, tag): """Returns a generator of all elements with the given tag. @@ -120,23 +130,6 @@ class Document: """ return self.tree.iter(tag=self._prefix + tag) - def _get_paths(self, conversions): - paths = [] - - # Get d-strings for SVG-Path elements - paths += [el.attrib for el in self.get_elements_by_tag('path')] - d_strings = [el['d'] for el in paths] - attribute_dictionary_list = paths - - # Convert path-like elements to d-strings and attribute dicts - if conversions: - for tag, fcn in conversions.items(): - attributes = [l.attrib for l in self.get_elements_by_tag(tag)] - d_strings += [fcn(d) for d in attributes] - - path_list = [parse_path(d) for d in d_strings] - return path_list - def convert_pathlike_elements_to_paths(self, conversions=CONVERSIONS): raise NotImplementedError @@ -148,14 +141,63 @@ class Document: """To help with backwards compatibility.""" return [p.tree_element.attrib for p in self.paths] - def add(self, path, attribs={}, parent=None): + def add_path(self, path, attribs={}, group=None): """Add a new path to the SVG.""" - if parent is None: - parent = self.tree.getroot() - # just get root - # then add new path - # then record element_tree object in path - raise NotImplementedError + + # If we are 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 + elif all(isinstance(elem, basestring) 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') + + # TODO: If the user passes in an xml.etree.Element object, should we check to make sure that it actually + # belongs to this Document object? + + if isinstance(path, Path): + path_svg = path.d() + elif is_path_segment(path): + path_svg = Path(path).d() + elif isinstance(path, basestring): + # Assume this is a valid d-string TODO: Should we sanity check the input string? + path_svg = path + + return SubElement(group, 'path', {'d': path_svg}) + + def get_or_add_group(self, nested_names): + """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. + + 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() + # Drill down through the names until we find the desired group + while nested_names: + prev_group = group + next_name = nested_names.pop(0) + for elem in group.iter(): + if elem.get('id') == next_name: + group = elem + + if prev_group is group: + # 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 + + return group def add_group(self, group_attribs={}, parent=None): """Add an empty group element to the SVG.""" From 8a4801bcdeabfe653df7101bbf79c330b5cf3657 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Tue, 8 May 2018 11:55:37 -0700 Subject: [PATCH 04/18] Tweaks to transform parsing implementation --- svgpathtools/parser.py | 28 +++++++++++++--------------- svgpathtools/svg2paths.py | 1 + 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index 24fc4cd..e32ea50 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -213,10 +213,10 @@ def _check_num_parsed_values(values, allowed): def _parse_transform_substr(transform_substr): - value_str = transform_substr.split('(')[1] + type_str, value_str = transform_substr.split('(') values = list(map(float, value_str.split(','))) transform = np.identity(3) - if 'matrix' in transform_substr: + if 'matrix' in type_str: _check_num_parsed_values(values, 6) transform[0:2, 0:3] = np.matrix([values[0:3], values[3:6]]) @@ -232,25 +232,20 @@ def _parse_transform_substr(transform_substr): _check_num_parsed_values(values, [1, 2]) x_scale = values[0] - if len(values) > 1: - y_scale = values[1] - else: - y_scale = x_scale # y_scale is assumed to equal x_scale if only one value is provided + y_scale = values[1] if (len(values) > 1) else x_scale transform[0, 0] = x_scale transform[1, 1] = y_scale elif 'rotate' in transform_substr: _check_num_parsed_values(values, [1, 3]) - angle = values[0] + angle = values[0] * np.pi / 180.0 if len(values) == 3: - x_offset = values[1] - y_offset = values[2] + offset = values[1:3] else: - x_offset = 0 - y_offset = 0 + offset = (0, 0) T_offset = np.identity(3) - T_offset[0:2, 2] = np.matrix([[x_offset], [y_offset]]) + T_offset[0:2, 2] = np.matrix([[offset[0]], [offset[1]]]) T_rotate = np.identity(3) T_rotate[0:2, 0:2] = np.matrix([np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]) @@ -258,17 +253,20 @@ def _parse_transform_substr(transform_substr): elif 'skewX' in transform_substr: _check_num_parsed_values(values, 1) - transform[0, 1] = np.tan(values[0]) + transform[0, 1] = np.tan(values[0] * np.pi / 180.0) elif 'skewY' in transform_substr: _check_num_parsed_values(values, 1) - transform[1, 0] = np.tan(values[0]) + transform[1, 0] = np.tan(values[0] * np.pi / 180.0) + else: + # Return an identity matrix if the type of transform is unknown, and warn the user + warnings.warn('Unknown SVG transform type: {0})'.format(type_str)) return transform def parse_transform(transform_str): - """Converts a valid SVG tranformation string into a 3x3 matrix. + """Converts a valid SVG transformation string into a 3x3 matrix. If the string is empty or null, this returns a 3x3 identity matrix""" if not transform_str: return np.identity(3) diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index 8793f68..1b55337 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -114,6 +114,7 @@ CONVERSIONS = {'circle': ellipse2pathd, 'polygon': polygon2pathd, 'rect': rect2pathd} + def svg2paths(svg_file_location, return_svg_attributes=False, conversions=CONVERSIONS, return_tree=False): """Converts SVG to list of Path objects and attribute dictionaries. From 70f6a782883f14028b00ca6b2cfbc8d57469757f Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Tue, 8 May 2018 13:22:19 -0700 Subject: [PATCH 05/18] Implementing a depth-first flattening of groups --- svgpathtools/document.py | 46 ++++++++++++++++++++++++++++++++++----- svgpathtools/svg2paths.py | 2 ++ 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index 21413b4..67baa6d 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -51,19 +51,22 @@ A Big Problem: # External dependencies from __future__ import division, absolute_import, print_function import os +import collections import xml.etree.cElementTree as etree import xml.etree.ElementTree.Element as Element import xml.etree.ElementTree.SubElement as SubElement # Internal dependencies from .parser import parse_path -from .svg2paths import (ellipse2pathd, line2pathd, polyline2pathd, +from .parser import parse_transform +from .svg2paths import (path2pathd, ellipse2pathd, line2pathd, polyline2pathd, polygon2pathd, rect2pathd) from .misctools import open_in_browser from .path import * -# THESE MUST BE WRAPPED TO OUPUT ElementTree.element objects -CONVERSIONS = {'circle': ellipse2pathd, +# THESE MUST BE WRAPPED TO OUTPUT ElementTree.element objects +CONVERSIONS = {'path': path2pathd, + 'circle': ellipse2pathd, 'ellipse': ellipse2pathd, 'line': line2pathd, 'polyline': polyline2pathd, @@ -71,11 +74,44 @@ CONVERSIONS = {'circle': ellipse2pathd, 'rect': rect2pathd} -def flatten_group_transforms(group): - """Returns a 3x3 matrix which can transform points on a path from a group frame to the root frame""" +def flatten_paths(group, return_attribs = False, group_filter = lambda x: True, path_filter = lambda x: True, + path_conversions = CONVERSIONS): + """Returns the paths inside a group (recursively), expressing the paths in the root coordinates + + @param group is an Element""" if not isinstance(group, Element): raise TypeError('Must provide an xml.etree.Element object') + # Stop right away if the group_selector rejects this group + if not group_filter(group): + return [] + + def get_relevant_children(parent): + return filter(group_filter, parent.findall('g')) + + # 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 the group element, the second entry is its transform, the third is its + # list of child elements, and the fourth is the index of the next child to traverse for that element. + StackElement = collections.namedtuple('StackElement', ['group', 'transform', 'children', 'next_child_index']) + + def new_stack_element(element): + return StackElement(element, parse_transform(element.get('transform')), get_relevant_children(element), 0) + + stack = [new_stack_element(group)] + + paths = [] + if return_attribs: path_attribs = [] + + while stack: + top = stack[-1] + + for key in path_conversions: + for path_elem in filter(path_filter, top.group.iterfind(key)): + pass # TODO: Finish this + + + + diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index 1b55337..eab6790 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -9,6 +9,8 @@ import xml.etree.cElementTree as etree # Internal dependencies from .parser import parse_path +def path2pathd(path): + return path.get('d', None) def ellipse2pathd(ellipse): """converts the parameters from an ellipse or a circle to a string From 332e959f529aa3af9d76b34d2fa134cff5211c7c Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Tue, 8 May 2018 16:52:36 -0700 Subject: [PATCH 06/18] Finish implementation of flatten_paths --- svgpathtools/document.py | 105 +++++++++++++++++++++++++------------- svgpathtools/parser.py | 2 +- svgpathtools/path.py | 26 ++++++++++ svgpathtools/svg2paths.py | 2 +- 4 files changed, 98 insertions(+), 37 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index 67baa6d..e3f8c1a 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -74,11 +74,17 @@ CONVERSIONS = {'path': path2pathd, 'rect': rect2pathd} -def flatten_paths(group, return_attribs = False, group_filter = lambda x: True, path_filter = lambda x: True, - path_conversions = CONVERSIONS): - """Returns the paths inside a group (recursively), expressing the paths in the root coordinates +def flatten_paths(group, return_attribs=False, + group_filter=lambda x: True, + path_filter=lambda x: True, + path_conversions=CONVERSIONS): + """Returns the paths inside a group (recursively), expressing the paths in the root coordinates. - @param group is an Element""" + 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). + """ if not isinstance(group, Element): raise TypeError('Must provide an xml.etree.Element object') @@ -86,57 +92,57 @@ def flatten_paths(group, return_attribs = False, group_filter = lambda x: True, if not group_filter(group): return [] - def get_relevant_children(parent): - return filter(group_filter, parent.findall('g')) - # 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 the group element, the second entry is its transform, the third is its - # list of child elements, and the fourth is the index of the next child to traverse for that element. - StackElement = collections.namedtuple('StackElement', ['group', 'transform', 'children', 'next_child_index']) + # 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): - return StackElement(element, parse_transform(element.get('transform')), get_relevant_children(element), 0) + def new_stack_element(element, last_tf): + return StackElement(element, last_tf * parse_transform(element.get('transform'))) - stack = [new_stack_element(group)] + def get_relevant_children(parent, last_tf): + children = [] + for elem in filter(group_filter, parent.iterfind('g')): + children.append(new_stack_element(elem, last_tf)) + return children + + stack = [new_stack_element(group, np.identity(3))] paths = [] - if return_attribs: path_attribs = [] + path_attribs = [] while stack: - top = stack[-1] + top = stack.pop() - for key in path_conversions: + # 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: for path_elem in filter(path_filter, top.group.iterfind(key)): - pass # TODO: Finish this - - - + paths.append(transform(parse_path(converter(path_elem)), top.transform)) + if return_attribs: + path_attribs.append(path_elem.attrib) + stack.extend(get_relevant_children(top.group, top.transform)) + if return_attribs: + return paths, path_attribs + else: + return paths class Document: - def __init__(self, filename, conversions=False, transform_paths=True): - """(EXPERIMENTAL) A container for a DOM-style document. + def __init__(self, filename): + """A container for a DOM-style SVG document. The `Document` class provides a simple interface to modify and analyze the path elements in a DOM-style document. The DOM-style document is - parsed into an ElementTree object (stored in the `tree` attribute) and - all SVG-Path (and, optionally, Path-like) elements are extracted into a - list of svgpathtools Path objects. For more information on "Path-like" - objects, see the below explanation of the `conversions` argument. + parsed into an ElementTree object (stored in the `tree` attribute). + + This class provides functions for extracting SVG data into Path objects. + The Path output objects will be transformed based on their parent groups. Args: - merge_transforms (object): filename (str): The filename of the DOM-style object. - conversions (bool or dict): If true, automatically converts - circle, ellipse, line, polyline, polygon, and rect elements - into path elements. These changes are saved in the ElementTree - object. For custom conversions, a dictionary can be passed in instead whose - keys are the element tags that are to be converted and whose values - are the corresponding conversion functions. Conversion - functions should both take in and return an ElementTree.element - object. """ # remember location of original svg file @@ -157,6 +163,35 @@ class Document: self._prefix = '' # etree.register_namespace('', prefix) + def flatten_paths(self, return_attribs=False, + group_filter=lambda x: True, + path_filter=lambda x: True, + path_conversions=CONVERSIONS): + paths = [] + path_attribs = [] + + # We don't need to worry about transforming any paths that lack a group. + # We can just append them to the list of paths and grab their attributes. + for key, converter in path_conversions: + for path_elem in filter(path_filter, self.tree.getroot().iterfind(key)): + paths.append(parse_path(converter(path_elem))) + if return_attribs: + path_attribs.append(path_elem.attrib) + + for group_elem in filter(group_filter, self.tree.getroot().iterfind('g')): + if return_attribs: + new_paths, new_attribs = flatten_paths(group_elem, return_attribs, + group_filter, path_filter, path_conversions) + path_attribs.extend(new_attribs) + else: + new_paths = flatten_paths(group_elem, return_attribs, + group_filter, path_filter, path_conversions) + paths.extend(new_paths) + + if return_attribs: + return new_paths, new_attribs + else: + return new_paths def get_elements_by_tag(self, tag): """Returns a generator of all elements with the given tag. diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index e32ea50..e9a7f94 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -202,7 +202,7 @@ def parse_path(pathdef, current_pos=0j, tree_element=None): def _check_num_parsed_values(values, allowed): - if not any( num == len(values) for num in allowed): + if not any(num == len(values) for num in allowed): if len(allowed) > 1: warnings.warn('Expected one of the following number of values {0}, found {1}: {2}' .format(allowed, len(values), values)) diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 13ec198..0c70a90 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -207,6 +207,32 @@ def translate(curve, z0): "QuadraticBezier, CubicBezier, or Arc object.") +def transform(curve, tf): + """Transforms the curve by the homogeneous transformation matrix tf""" + def to_point(p): + return np.matrix([[p.real], [p.imag], [1.0]]) + + def to_vector(v): + return np.matrix([[v.real], [v.imag], [0.0]]) + + def to_complex(z): + return z[0] + 1j * z[1] + + if isinstance(curve, Path): + return Path(*[transform(segment, tf) for segment in curve]) + elif is_bezier_segment(curve): + return bpoints2bezier([to_complex(tf*to_point(p)) for p in curve.bpoints()]) + elif isinstance(curve, Arc): + new_start = to_complex(tf * to_point(curve.start)) + new_end = to_complex(tf * to_point(curve.end)) + new_radius = to_complex(tf * to_vector(curve.radius)) + return Arc(new_start, radius=new_radius, rotation=curve.rotation, + large_arc=curve.large_arc, sweep=curve.sweep, end=new_end) + else: + raise TypeError("Input `curve` should be a Path, Line, " + "QuadraticBezier, CubicBezier, or Arc object.") + + def bezier_unit_tangent(seg, t): """Returns the unit tangent of the segment at t. diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index eab6790..cc8374c 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -10,7 +10,7 @@ import xml.etree.cElementTree as etree from .parser import parse_path def path2pathd(path): - return path.get('d', None) + return path.get('d', '') def ellipse2pathd(ellipse): """converts the parameters from an ellipse or a circle to a string From 3512f869680c3aaf3b12c03355374e2c0be87f4e Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Wed, 9 May 2018 17:39:54 -0700 Subject: [PATCH 07/18] Beginning to write tests for groups --- test/groups.svg | 143 ++++++++++++++++++++++++++++++++++++++++++++ test/test_groups.py | 8 +++ 2 files changed, 151 insertions(+) create mode 100644 test/groups.svg create mode 100644 test/test_groups.py diff --git a/test/groups.svg b/test/groups.svg new file mode 100644 index 0000000..6b3cc8e --- /dev/null +++ b/test/groups.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/test_groups.py b/test/test_groups.py new file mode 100644 index 0000000..7c89ed3 --- /dev/null +++ b/test/test_groups.py @@ -0,0 +1,8 @@ +from __future__ import division, absolute_import, print_function +import unittest +from svgpathtools import * + +class TestGroups(unittest.TestCase): + + def test_group_transform(self): + pass \ No newline at end of file From f5a7fe77d132d6a30775538c25d852e6e3bc9de1 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Wed, 9 May 2018 17:40:49 -0700 Subject: [PATCH 08/18] Refactoring flatten_paths() into flatten_all_paths() --- svgpathtools/document.py | 56 +++++++++++----------------------------- svgpathtools/parser.py | 2 +- 2 files changed, 16 insertions(+), 42 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index e3f8c1a..7b1f7b3 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -74,10 +74,11 @@ CONVERSIONS = {'path': path2pathd, 'rect': rect2pathd} -def flatten_paths(group, return_attribs=False, - group_filter=lambda x: True, - path_filter=lambda x: True, - path_conversions=CONVERSIONS): +def flatten_all_paths( + group, + group_filter=lambda x: True, + path_filter=lambda x: True, + path_conversions=CONVERSIONS): """Returns the paths inside a group (recursively), expressing the paths in the root coordinates. Args: @@ -108,8 +109,8 @@ def flatten_paths(group, return_attribs=False, stack = [new_stack_element(group, np.identity(3))] + FlattenedPath = collections.namedtuple('FlattenedPath', ['path', 'attributes', 'transform']) paths = [] - path_attribs = [] while stack: top = stack.pop() @@ -118,16 +119,13 @@ def flatten_paths(group, return_attribs=False, # the path_filter accepts it. for key, converter in path_conversions: for path_elem in filter(path_filter, top.group.iterfind(key)): - paths.append(transform(parse_path(converter(path_elem)), top.transform)) - if return_attribs: - path_attribs.append(path_elem.attrib) + path_tf = top.transform * parse_transform(path_elem.get('transform')) + path = transform(parse_path(converter(path_elem)), path_tf) + paths.append(FlattenedPath(path, path_elem.attrib, path_tf)) stack.extend(get_relevant_children(top.group, top.transform)) - if return_attribs: - return paths, path_attribs - else: - return paths + return paths class Document: @@ -163,35 +161,11 @@ class Document: self._prefix = '' # etree.register_namespace('', prefix) - def flatten_paths(self, return_attribs=False, - group_filter=lambda x: True, - path_filter=lambda x: True, - path_conversions=CONVERSIONS): - paths = [] - path_attribs = [] - - # We don't need to worry about transforming any paths that lack a group. - # We can just append them to the list of paths and grab their attributes. - for key, converter in path_conversions: - for path_elem in filter(path_filter, self.tree.getroot().iterfind(key)): - paths.append(parse_path(converter(path_elem))) - if return_attribs: - path_attribs.append(path_elem.attrib) - - for group_elem in filter(group_filter, self.tree.getroot().iterfind('g')): - if return_attribs: - new_paths, new_attribs = flatten_paths(group_elem, return_attribs, - group_filter, path_filter, path_conversions) - path_attribs.extend(new_attribs) - else: - new_paths = flatten_paths(group_elem, return_attribs, - group_filter, path_filter, path_conversions) - paths.extend(new_paths) - - if return_attribs: - return new_paths, new_attribs - else: - return new_paths + def flatten_all_paths(self, + group_filter=lambda x: True, + path_filter=lambda x: True, + path_conversions=CONVERSIONS): + return flatten_all_paths(self.tree.getroot(), group_filter, path_filter, path_conversions) def get_elements_by_tag(self, tag): """Returns a generator of all elements with the given tag. diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index e9a7f94..70f0358 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -219,7 +219,7 @@ def _parse_transform_substr(transform_substr): if 'matrix' in type_str: _check_num_parsed_values(values, 6) - transform[0:2, 0:3] = np.matrix([values[0:3], values[3:6]]) + transform[0:2, 0:3] = np.matrix([values[0:6:2], values[1:6:2]]) elif 'translate' in transform_substr: _check_num_parsed_values(values, [1, 2]) From be675f1b1cf65509ac4cc98df6f639dee3a5f72e Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 10 May 2018 12:55:19 -0700 Subject: [PATCH 09/18] Clean up implementation of document classes --- svgpathtools/document.py | 113 ++++++++++++++++++++++++++++++--------- 1 file changed, 89 insertions(+), 24 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index 7b1f7b3..1dbaf18 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -55,6 +55,7 @@ import collections import xml.etree.cElementTree as etree import xml.etree.ElementTree.Element as Element import xml.etree.ElementTree.SubElement as SubElement +import warnings # Internal dependencies from .parser import parse_path @@ -79,7 +80,11 @@ def flatten_all_paths( group_filter=lambda x: True, path_filter=lambda x: True, path_conversions=CONVERSIONS): - """Returns the paths inside a group (recursively), expressing the paths in the root coordinates. + """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. Args: group is an Element @@ -128,6 +133,38 @@ def flatten_all_paths( return paths +def flatten_group( + group_to_flatten, + root, + recursive=True, + group_filter=lambda x: True, + path_filter=lambda x: True, + path_conversions=CONVERSIONS): + """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.""" + + 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. + return [] + + # We create a set of the unique IDs of each group object that we want to flatten. + # 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(): + desired_groups.add(id(group)) + else: + desired_groups.add(id(group_to_flatten)) + + 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) + + class Document: def __init__(self, filename): """A container for a DOM-style SVG document. @@ -167,6 +204,21 @@ class Document: path_conversions=CONVERSIONS): return flatten_all_paths(self.tree.getroot(), group_filter, path_filter, path_conversions) + def flatten_group(self, + group, + recursive=True, + group_filter=lambda x: True, + path_filter=lambda x: True, + path_conversions=CONVERSIONS): + if all(isinstance(s, basestring) for s in group): + # 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') + + return flatten_group(group, self.tree.getroot(), recursive, group_filter, path_filter, path_conversions) + def get_elements_by_tag(self, tag): """Returns a generator of all elements with the given tag. @@ -175,18 +227,15 @@ class Document: """ return self.tree.iter(tag=self._prefix + tag) - def convert_pathlike_elements_to_paths(self, conversions=CONVERSIONS): - raise NotImplementedError - def get_svg_attributes(self): """To help with backwards compatibility.""" return self.get_elements_by_tag('svg')[0].attrib def get_path_attributes(self): """To help with backwards compatibility.""" - return [p.tree_element.attrib for p in self.paths] + return [p.tree_element.attrib for p in self.tree.getroot().iter('path')] - def add_path(self, path, attribs={}, group=None): + 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 @@ -200,18 +249,32 @@ class Document: elif not isinstance(group, Element): raise TypeError('Must provide a list of strings or an xml.etree.Element object') - # TODO: If the user passes in an xml.etree.Element object, should we check to make sure that it actually - # belongs to this Document object? + 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') if isinstance(path, Path): path_svg = path.d() elif is_path_segment(path): path_svg = Path(path).d() elif isinstance(path, basestring): - # 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') - return SubElement(group, 'path', {'d': path_svg}) + if attribs is None: + attribs = {} + else: + attribs = attribs.copy() + + attribs['d'] = path_svg + + return SubElement(group, 'path', attribs) + + def contains_group(self, group): + return any(group is owned for owned in self.tree.iter()) def get_or_add_group(self, nested_names): """Get a group from the tree, or add a new one with the given name structure. @@ -228,9 +291,10 @@ class Document: while nested_names: prev_group = group next_name = nested_names.pop(0) - for elem in group.iter(): + for elem in group.iterfind('g'): if elem.get('id') == next_name: group = elem + break if prev_group is group: # The group we're looking for does not exist, so let's create the group structure @@ -244,31 +308,32 @@ class Document: return group - def add_group(self, group_attribs={}, parent=None): + def add_group(self, group_attribs=None, parent=None): """Add an empty group element to the SVG.""" if parent is None: parent = self.tree.getroot() - raise NotImplementedError + elif not self.contains_group(parent): + warnings.warn('The requested group does not belong to this Document') - def update_tree(self): - """Rewrite d-string's for each path in the `tree` attribute.""" - raise NotImplementedError + if group_attribs is None: + group_attribs = {} + else: + group_attribs = group_attribs.copy() - def save(self, filename, update=True): - """Write to svg to a file.""" - if update: - self.update_tree() + return SubElement(parent, 'g', group_attribs) + + def save(self, filename=None): + if filename is None: + filename = self.original_filename with open(filename, 'w') as output_svg: output_svg.write(etree.tostring(self.tree.getroot())) - def display(self, filename=None, update=True): + def display(self, filename=None): """Displays/opens the doc using the OS's default application.""" - if update: - self.update_tree() if filename is None: - raise NotImplementedError + filename = self.original_filename # write to a (by default temporary) file with open(filename, 'w') as output_svg: From a29b392234084ec4066ca19ada651a2d7faea91d Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 10 May 2018 16:45:41 -0700 Subject: [PATCH 10/18] Debugging xml namespace behavior -- needs improvement --- svgpathtools.egg-info/PKG-INFO | 4 +-- svgpathtools.egg-info/SOURCES.txt | 3 ++ svgpathtools/__init__.py | 2 +- svgpathtools/document.py | 52 +++++++++++++++++-------------- svgpathtools/parser.py | 45 +++++++++++++++++--------- test/groups.svg | 6 ++-- test/test_groups.py | 10 ++++-- test/test_parsing.py | 4 +++ 8 files changed, 78 insertions(+), 48 deletions(-) diff --git a/svgpathtools.egg-info/PKG-INFO b/svgpathtools.egg-info/PKG-INFO index 29ff89b..9409782 100644 --- a/svgpathtools.egg-info/PKG-INFO +++ b/svgpathtools.egg-info/PKG-INFO @@ -7,7 +7,6 @@ Author: Andy Port Author-email: AndyAPort@gmail.com License: MIT Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.2 -Description-Content-Type: UNKNOWN Description: svgpathtools ============ @@ -595,9 +594,8 @@ Description: of the 'parallel' offset curve.""" nls = [] for seg in path: - ct = 1 for k in range(steps): - t = k / steps + t = k / float(steps) offset_vector = offset_distance * seg.normal(t) nl = Line(seg.point(t), seg.point(t) + offset_vector) nls.append(nl) diff --git a/svgpathtools.egg-info/SOURCES.txt b/svgpathtools.egg-info/SOURCES.txt index c1dde0f..1eb4a50 100644 --- a/svgpathtools.egg-info/SOURCES.txt +++ b/svgpathtools.egg-info/SOURCES.txt @@ -15,6 +15,7 @@ test.svg vectorframes.svg svgpathtools/__init__.py svgpathtools/bezier.py +svgpathtools/document.py svgpathtools/misctools.py svgpathtools/parser.py svgpathtools/path.py @@ -29,11 +30,13 @@ svgpathtools.egg-info/requires.txt svgpathtools.egg-info/top_level.txt test/circle.svg test/ellipse.svg +test/groups.svg test/polygons.svg test/rects.svg test/test.svg test/test_bezier.py test/test_generation.py +test/test_groups.py test/test_parsing.py test/test_path.py test/test_polytools.py diff --git a/svgpathtools/__init__.py b/svgpathtools/__init__.py index 207b33a..e678bb2 100644 --- a/svgpathtools/__init__.py +++ b/svgpathtools/__init__.py @@ -17,4 +17,4 @@ from .document import Document try: from .svg2paths import svg2paths, svg2paths2 except ImportError: - pass \ No newline at end of file + pass diff --git a/svgpathtools/document.py b/svgpathtools/document.py index 1dbaf18..69ef34f 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -52,9 +52,8 @@ A Big Problem: from __future__ import division, absolute_import, print_function import os import collections -import xml.etree.cElementTree as etree -import xml.etree.ElementTree.Element as Element -import xml.etree.ElementTree.SubElement as SubElement +import xml.etree.ElementTree as etree +from xml.etree.ElementTree import Element, SubElement, register_namespace, _namespace_map import warnings # Internal dependencies @@ -65,6 +64,11 @@ from .svg2paths import (path2pathd, ellipse2pathd, line2pathd, polyline2pathd, from .misctools import open_in_browser from .path import * +# Let xml.etree.ElementTree know about the SVG namespace +print(' ------------------ about to register the svg namespace') +register_namespace('svg', 'http://www.w3.org/2000/svg') +print('namespace map: {0}'.format(_namespace_map)) + # THESE MUST BE WRAPPED TO OUTPUT ElementTree.element objects CONVERSIONS = {'path': path2pathd, 'circle': ellipse2pathd, @@ -74,12 +78,15 @@ CONVERSIONS = {'path': path2pathd, 'polygon': polygon2pathd, 'rect': rect2pathd} +ONLY_PATHS = {'path': path2pathd} + def flatten_all_paths( group, group_filter=lambda x: True, path_filter=lambda x: True, - path_conversions=CONVERSIONS): + path_conversions=CONVERSIONS, + search_xpath='{http://www.w3.org/2000/svg}g'): """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) @@ -92,7 +99,8 @@ def flatten_all_paths( that are not included in this dictionary will be ignored (including the `path` tag). """ if not isinstance(group, Element): - raise TypeError('Must provide an xml.etree.Element object') + raise TypeError('Must provide an xml.etree.Element object. Instead you provided {0} : compared to {1}' + .format(type(group), type(Element('some tag')))) # Stop right away if the group_selector rejects this group if not group_filter(group): @@ -108,7 +116,7 @@ def flatten_all_paths( def get_relevant_children(parent, last_tf): children = [] - for elem in filter(group_filter, parent.iterfind('g')): + for elem in filter(group_filter, parent.iterfind(search_xpath)): children.append(new_stack_element(elem, last_tf)) return children @@ -120,10 +128,13 @@ def flatten_all_paths( while stack: top = stack.pop() + print('popping group {0}'.format(top.group.attrib)) + print('has children: {0}'.format(list(elem.tag for elem in top.group.iter()))) + # 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: - for path_elem in filter(path_filter, top.group.iterfind(key)): + for key, converter in path_conversions.iteritems(): + for path_elem in filter(path_filter, top.group.iterfind('{http://www.w3.org/2000/svg}'+key)): path_tf = top.transform * parse_transform(path_elem.get('transform')) path = transform(parse_path(converter(path_elem)), path_tf) paths.append(FlattenedPath(path, path_elem.attrib, path_tf)) @@ -139,7 +150,8 @@ def flatten_group( recursive=True, group_filter=lambda x: True, path_filter=lambda x: True, - path_conversions=CONVERSIONS): + path_conversions=CONVERSIONS, + search_xpath='g'): """Flatten all the paths in a specific group. The paths will be flattened into the 'root' frame. Note that root needs to be @@ -150,7 +162,7 @@ def flatten_group( # 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 group object that we want to flatten. + # 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: @@ -162,7 +174,7 @@ 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) + return flatten_all_paths(root, desired_group_filter, path_filter, path_conversions, search_xpath) class Document: @@ -190,14 +202,6 @@ class Document: self.tree = etree.parse(filename) self.root = self.tree.getroot() - # get URI namespace (only necessary in OS X?) - root_tag = self.tree.getroot().tag - if root_tag[0] == "{": - self._prefix = root_tag[:root_tag.find('}') + 1] - else: - self._prefix = '' - # etree.register_namespace('', prefix) - def flatten_all_paths(self, group_filter=lambda x: True, path_filter=lambda x: True, @@ -215,7 +219,7 @@ class Document: 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') + '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) @@ -247,7 +251,8 @@ class Document: 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') + 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 @@ -262,7 +267,8 @@ class Document: # 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') + 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 = {} @@ -313,7 +319,7 @@ class Document: if parent is None: parent = self.tree.getroot() elif not self.contains_group(parent): - warnings.warn('The requested group does not belong to this Document') + warnings.warn('The requested group {0} does not belong to this Document'.format(parent)) if group_attribs is None: group_attribs = {} diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index 70f0358..f01aee5 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -206,30 +206,38 @@ def _check_num_parsed_values(values, allowed): if len(allowed) > 1: warnings.warn('Expected one of the following number of values {0}, found {1}: {2}' .format(allowed, len(values), values)) - elif allowed != 1: - warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed, len(values), values)) + elif allowed[0] != 1: + warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed[0], len(values), values)) else: warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values)) + return False + return True def _parse_transform_substr(transform_substr): + type_str, value_str = transform_substr.split('(') - values = list(map(float, value_str.split(','))) + value_str = value_str.replace(',', ' ') + values = list(map(float, filter(None, value_str.split(' ')))) + transform = np.identity(3) if 'matrix' in type_str: - _check_num_parsed_values(values, 6) + if not _check_num_parsed_values(values, [6]): + return transform transform[0:2, 0:3] = np.matrix([values[0:6:2], values[1:6:2]]) elif 'translate' in transform_substr: - _check_num_parsed_values(values, [1, 2]) + if not _check_num_parsed_values(values, [1, 2]): + return transform transform[0, 2] = values[0] if len(values) > 1: transform[1, 2] = values[1] elif 'scale' in transform_substr: - _check_num_parsed_values(values, [1, 2]) + if not _check_num_parsed_values(values, [1, 2]): + return transform x_scale = values[0] y_scale = values[1] if (len(values) > 1) else x_scale @@ -237,26 +245,31 @@ def _parse_transform_substr(transform_substr): transform[1, 1] = y_scale elif 'rotate' in transform_substr: - _check_num_parsed_values(values, [1, 3]) + if not _check_num_parsed_values(values, [1, 3]): + return transform angle = values[0] * np.pi / 180.0 if len(values) == 3: offset = values[1:3] else: offset = (0, 0) - T_offset = np.identity(3) - T_offset[0:2, 2] = np.matrix([[offset[0]], [offset[1]]]) - T_rotate = np.identity(3) - T_rotate[0:2, 0:2] = np.matrix([np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]) + tf_offset = np.identity(3) + tf_offset[0:2, 2:3] = np.matrix([[offset[0]], [offset[1]]]) + tf_rotate = np.identity(3) + tf_rotate[0:2, 0:2] = np.matrix([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]) - transform = T_offset * T_rotate * (-T_offset) + transform = tf_offset * tf_rotate * (-tf_offset) elif 'skewX' in transform_substr: - _check_num_parsed_values(values, 1) + if not _check_num_parsed_values(values, [1]): + return transform + transform[0, 1] = np.tan(values[0] * np.pi / 180.0) elif 'skewY' in transform_substr: - _check_num_parsed_values(values, 1) + if not _check_num_parsed_values(values, [1]): + return transform + transform[1, 0] = np.tan(values[0] * np.pi / 180.0) else: # Return an identity matrix if the type of transform is unknown, and warn the user @@ -274,6 +287,8 @@ def parse_transform(transform_str): raise TypeError('Must provide a string to parse') total_transform = np.identity(3) - transform_substrs = transform_str.split(')') + transform_substrs = transform_str.split(')')[:-1] # Skip the last element, because it should be empty for substr in transform_substrs: total_transform *= _parse_transform_substr(substr) + + return total_transform diff --git a/test/groups.svg b/test/groups.svg index 6b3cc8e..f5492d1 100644 --- a/test/groups.svg +++ b/test/groups.svg @@ -5,13 +5,11 @@ viewBox="0 0 365 365" height="100%" width="100%" - xmlns="http://www.w3.org/2000/svg" - xmlns:ev="http://www.w3.org/2001/xml-events" - xmlns:xlink="http://www.w3.org/1999/xlink"> + xmlns="http://www.w3.org/2000/svg"> + transform="matrix(1.5 0.0 0.0 0.5 -40.0 20.0)"> Date: Thu, 10 May 2018 17:05:47 -0700 Subject: [PATCH 11/18] Improve the way the svg namespace is handled --- svgpathtools/document.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index 69ef34f..0e17a95 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -65,9 +65,8 @@ from .misctools import open_in_browser from .path import * # Let xml.etree.ElementTree know about the SVG namespace -print(' ------------------ about to register the svg namespace') +SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'} register_namespace('svg', 'http://www.w3.org/2000/svg') -print('namespace map: {0}'.format(_namespace_map)) # THESE MUST BE WRAPPED TO OUTPUT ElementTree.element objects CONVERSIONS = {'path': path2pathd, @@ -78,7 +77,9 @@ CONVERSIONS = {'path': path2pathd, 'polygon': polygon2pathd, 'rect': rect2pathd} -ONLY_PATHS = {'path': path2pathd} +CONVERT_ONLY_PATHS = {'path': path2pathd} + +SVG_GROUP_TAG = 'svg:g' def flatten_all_paths( @@ -86,7 +87,7 @@ def flatten_all_paths( group_filter=lambda x: True, path_filter=lambda x: True, path_conversions=CONVERSIONS, - search_xpath='{http://www.w3.org/2000/svg}g'): + 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) @@ -96,11 +97,11 @@ def flatten_all_paths( 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). + 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} : compared to {1}' - .format(type(group), type(Element('some tag')))) + 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): @@ -116,7 +117,7 @@ def flatten_all_paths( def get_relevant_children(parent, last_tf): children = [] - for elem in filter(group_filter, parent.iterfind(search_xpath)): + for elem in filter(group_filter, parent.iterfind(group_search_xpath, SVG_NAMESPACE)): children.append(new_stack_element(elem, last_tf)) return children @@ -128,13 +129,10 @@ def flatten_all_paths( while stack: top = stack.pop() - print('popping group {0}'.format(top.group.attrib)) - print('has children: {0}'.format(list(elem.tag for elem in top.group.iter()))) - # 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.iteritems(): - for path_elem in filter(path_filter, top.group.iterfind('{http://www.w3.org/2000/svg}'+key)): + for path_elem in filter(path_filter, top.group.iterfind('svg:'+key, SVG_NAMESPACE)): path_tf = top.transform * parse_transform(path_elem.get('transform')) path = transform(parse_path(converter(path_elem)), path_tf) paths.append(FlattenedPath(path, path_elem.attrib, path_tf)) @@ -151,7 +149,7 @@ def flatten_group( group_filter=lambda x: True, path_filter=lambda x: True, path_conversions=CONVERSIONS, - search_xpath='g'): + 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 @@ -174,7 +172,7 @@ 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, search_xpath) + return flatten_all_paths(root, desired_group_filter, path_filter, path_conversions, group_search_xpath) class Document: @@ -297,7 +295,7 @@ class Document: while nested_names: prev_group = group next_name = nested_names.pop(0) - for elem in group.iterfind('g'): + for elem in group.iterfind('svg:g', SVG_NAMESPACE): if elem.get('id') == next_name: group = elem break From 1c8ca10f73e242a4c31bfdab3152c2ebddb01968 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 10 May 2018 17:06:16 -0700 Subject: [PATCH 12/18] Print out some paths to see that they're sane --- test/test_groups.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_groups.py b/test/test_groups.py index c281511..143dbf6 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -10,5 +10,10 @@ class TestGroups(unittest.TestCase): doc = Document(join(dirname(__file__), 'groups.svg')) result = doc.flatten_all_paths() - print('\nnumber of paths: '+str(len(result))) + print('\nNumber of paths: '+str(len(result))) self.assertGreater(len(result), 0) + + for svg_path in result: + print(svg_path.path) + + # TODO: Test that the paths were transformed as expected From 29eb1e9364177e32f69296d5be1efae62bec3e3e Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 24 May 2018 19:36:22 +0800 Subject: [PATCH 13/18] Fix multiplication of numpy matrices -- need to use .dot() instead of operator* --- svgpathtools/document.py | 4 ++-- svgpathtools/parser.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index 0e17a95..b10ed6d 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -113,7 +113,7 @@ def flatten_all_paths( StackElement = collections.namedtuple('StackElement', ['group', 'transform']) def new_stack_element(element, last_tf): - return StackElement(element, last_tf * parse_transform(element.get('transform'))) + return StackElement(element, last_tf.dot(parse_transform(element.get('transform')))) def get_relevant_children(parent, last_tf): children = [] @@ -133,7 +133,7 @@ def flatten_all_paths( # the path_filter accepts it. for key, converter in path_conversions.iteritems(): for path_elem in filter(path_filter, top.group.iterfind('svg:'+key, SVG_NAMESPACE)): - path_tf = top.transform * parse_transform(path_elem.get('transform')) + 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.attrib, path_tf)) diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index f01aee5..07fb0e1 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -257,8 +257,10 @@ def _parse_transform_substr(transform_substr): tf_offset[0:2, 2:3] = np.matrix([[offset[0]], [offset[1]]]) tf_rotate = np.identity(3) tf_rotate[0:2, 0:2] = np.matrix([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]]) + tf_offset_neg = np.identity(3) + tf_offset_neg[0:2, 2:3] = np.matrix([[-offset[0]], [-offset[1]]]) - transform = tf_offset * tf_rotate * (-tf_offset) + transform = tf_offset.dot(tf_rotate).dot(tf_offset_neg) elif 'skewX' in transform_substr: if not _check_num_parsed_values(values, [1]): @@ -289,6 +291,6 @@ def parse_transform(transform_str): total_transform = np.identity(3) transform_substrs = transform_str.split(')')[:-1] # Skip the last element, because it should be empty for substr in transform_substrs: - total_transform *= _parse_transform_substr(substr) + total_transform = total_transform.dot(_parse_transform_substr(substr)) return total_transform From b54796984609fb4f1b99074af0a94f225bf4f505 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 24 May 2018 19:40:41 +0800 Subject: [PATCH 14/18] Create a unit test for parsing SVG groups --- test/groups.svg | 36 ++++++++++----- test/test_groups.py | 105 +++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 124 insertions(+), 17 deletions(-) diff --git a/test/groups.svg b/test/groups.svg index f5492d1..804a6b8 100644 --- a/test/groups.svg +++ b/test/groups.svg @@ -5,7 +5,8 @@ viewBox="0 0 365 365" height="100%" width="100%" - xmlns="http://www.w3.org/2000/svg"> + xmlns="http://www.w3.org/2000/svg" + xmlns:test="some://testuri"> + stroke-width="3" + test:name="path00"/> + stroke-width="3" + test:name="path01"/> + stroke-width="3" + test:name="path02"/> @@ -46,7 +50,8 @@ d="M 150,200 l -50,25" fill="black" stroke="black" - stroke-width="3"/> + stroke-width="3" + test:name="path03"/> @@ -58,7 +63,8 @@ d="M 150,200 l -50,25" fill="black" stroke="black" - stroke-width="3"/> + stroke-width="3" + test:name="path04"/> @@ -70,7 +76,8 @@ d="M 150,200 l -50,25" fill="black" stroke="black" - stroke-width="3"/> + stroke-width="3" + test:name="path05"/> @@ -84,7 +91,8 @@ d="M 122,320 l -50,0" fill="black" stroke="black" - stroke-width="3"/> + stroke-width="3" + test:name="path06"/> @@ -96,7 +104,8 @@ d="M 183,183 l 0,30" fill="black" stroke="black" - stroke-width="3"/> + stroke-width="3" + test:name="path07"/> @@ -108,7 +117,8 @@ d="M 183,183 l 0,30" fill="black" stroke="black" - stroke-width="3"/> + stroke-width="3" + test:name="path08"/> @@ -120,7 +130,8 @@ d="M 183,183 l 40,40" fill="black" stroke="black" - stroke-width="3"/> + stroke-width="3" + test:name="path09"/> @@ -132,7 +143,8 @@ d="M 183,183 l 40,40" fill="black" stroke="black" - stroke-width="3"/> + stroke-width="3" + test:name="path10"/> diff --git a/test/test_groups.py b/test/test_groups.py index 143dbf6..1f0e09c 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -2,18 +2,113 @@ from __future__ import division, absolute_import, print_function import unittest from svgpathtools import * from os.path import join, dirname +import numpy as np + + +def get_desired_path(name, paths): + return next(p for p in paths if p.attributes['{some://testuri}name'] == name) + + +def column_vector(values): + input = [] + for value in values: + input.append([value]) + return np.matrix(input) class TestGroups(unittest.TestCase): + def check_values(self, v, z): + self.assertAlmostEqual(v[0], z.real) + self.assertAlmostEqual(v[1], z.imag) + + def check_path(self, tf, v_s_vals, v_e_relative_vals, name, paths): + v_s_vals.append(1.0) + v_e_relative_vals.append(0.0) + v_s = column_vector(v_s_vals) + v_e = v_s + column_vector(v_e_relative_vals) + + actual = get_desired_path(name, paths) + + self.check_values(tf.dot(v_s), actual.path.start) + self.check_values(tf.dot(v_e), actual.path.end) + def test_group_flatten(self): doc = Document(join(dirname(__file__), 'groups.svg')) result = doc.flatten_all_paths() - print('\nNumber of paths: '+str(len(result))) - self.assertGreater(len(result), 0) + self.assertEqual(11, len(result)) - for svg_path in result: - print(svg_path.path) + tf_matrix_group = np.matrix([[1.5, 0.0, -40.0], [0.0, 0.5, 20.0], [0.0, 0.0, 1.0]]) - # TODO: Test that the paths were transformed as expected + self.check_path(tf_matrix_group, + [183, 183], [0.0, -50], + 'path00', result) + + tf_scale_group = np.matrix([[1.25, 0.0, 0.0], [0.0, 1.25, 0.0], [0.0, 0.0, 1.0]]) + + self.check_path(tf_matrix_group.dot(tf_scale_group), + [122, 320], [-50.0, 0.0], + 'path01', result) + + self.check_path(tf_matrix_group.dot(tf_scale_group), + [150, 200], [-50, 25], + 'path02', result) + + self.check_path(tf_matrix_group.dot(tf_scale_group), + [150, 200], [-50, 25], + 'path03', result) + + tf_nested_translate_group = np.matrix([[1, 0, 20], [0, 1, 0], [0, 0, 1]]) + + self.check_path(tf_matrix_group.dot(tf_scale_group).dot(tf_nested_translate_group), + [150, 200], [-50, 25], + 'path04', result) + + tf_nested_translate_xy_group = np.matrix([[1, 0, 20], [0, 1, 30], [0, 0, 1]]) + + self.check_path(tf_matrix_group.dot(tf_scale_group).dot(tf_nested_translate_xy_group), + [150, 200], [-50, 25], + 'path05', result) + + tf_scale_xy_group = np.matrix([[0.5, 0, 0], [0, 1.5, 0.0], [0, 0, 1]]) + + self.check_path(tf_matrix_group.dot(tf_scale_xy_group), + [122, 320], [-50, 0], + 'path06', result) + + a_07 = 20.0*np.pi/180.0 + tf_rotate_group = np.matrix([[np.cos(a_07), -np.sin(a_07), 0], + [np.sin(a_07), np.cos(a_07), 0], + [0, 0, 1]]) + + self.check_path(tf_matrix_group.dot(tf_rotate_group), + [183, 183], [0, 30], + 'path07', result) + + a_08 = 45.0*np.pi/180.0 + tf_rotate_xy_group_R = np.matrix([[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.matrix([[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_path(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.matrix([[1, np.tan(a_09), 0], [0, 1, 0], [0, 0, 1]]) + + self.check_path(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.matrix([[1, 0, 0], [np.tan(a_10), 1, 0], [0, 0, 1]]) + + self.check_path(tf_matrix_group.dot(tf_skew_y_group), + [183, 183], [40, 40], + 'path10', result) + + # TODO: Add a test where a path element has a transform attribute From a6ceec4f0da8b0534984bcf59a7ef7ff84f41c4a Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Fri, 25 May 2018 16:42:36 +0800 Subject: [PATCH 15/18] Return a reference to an element instead of a copied dictionary of attributes --- svgpathtools/document.py | 4 ++-- test/test_groups.py | 34 +++++++++++++++++++++------------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index b10ed6d..e87f13e 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -123,7 +123,7 @@ def flatten_all_paths( stack = [new_stack_element(group, np.identity(3))] - FlattenedPath = collections.namedtuple('FlattenedPath', ['path', 'attributes', 'transform']) + FlattenedPath = collections.namedtuple('FlattenedPath', ['path', 'element', 'transform']) paths = [] while stack: @@ -135,7 +135,7 @@ def flatten_all_paths( 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.attrib, path_tf)) + paths.append(FlattenedPath(path, path_elem, path_tf)) stack.extend(get_relevant_children(top.group, top.transform)) diff --git a/test/test_groups.py b/test/test_groups.py index 1f0e09c..672e0fa 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -6,7 +6,7 @@ import numpy as np def get_desired_path(name, paths): - return next(p for p in paths if p.attributes['{some://testuri}name'] == name) + return next(p for p in paths if p.element.get('{some://testuri}name') == name) def column_vector(values): @@ -19,10 +19,17 @@ def column_vector(values): 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 self.assertAlmostEqual(v[0], z.real) self.assertAlmostEqual(v[1], z.imag) - def check_path(self, tf, v_s_vals, v_e_relative_vals, name, paths): + def check_line(self, tf, v_s_vals, v_e_relative_vals, name, paths): + # 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) + # * paths is the output of doc.flatten_all_paths() v_s_vals.append(1.0) v_e_relative_vals.append(0.0) v_s = column_vector(v_s_vals) @@ -34,6 +41,7 @@ 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. doc = Document(join(dirname(__file__), 'groups.svg')) result = doc.flatten_all_paths() @@ -41,39 +49,39 @@ class TestGroups(unittest.TestCase): tf_matrix_group = np.matrix([[1.5, 0.0, -40.0], [0.0, 0.5, 20.0], [0.0, 0.0, 1.0]]) - self.check_path(tf_matrix_group, + self.check_line(tf_matrix_group, [183, 183], [0.0, -50], 'path00', result) tf_scale_group = np.matrix([[1.25, 0.0, 0.0], [0.0, 1.25, 0.0], [0.0, 0.0, 1.0]]) - self.check_path(tf_matrix_group.dot(tf_scale_group), + self.check_line(tf_matrix_group.dot(tf_scale_group), [122, 320], [-50.0, 0.0], 'path01', result) - self.check_path(tf_matrix_group.dot(tf_scale_group), + self.check_line(tf_matrix_group.dot(tf_scale_group), [150, 200], [-50, 25], 'path02', result) - self.check_path(tf_matrix_group.dot(tf_scale_group), + self.check_line(tf_matrix_group.dot(tf_scale_group), [150, 200], [-50, 25], 'path03', result) tf_nested_translate_group = np.matrix([[1, 0, 20], [0, 1, 0], [0, 0, 1]]) - self.check_path(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.matrix([[1, 0, 20], [0, 1, 30], [0, 0, 1]]) - self.check_path(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.matrix([[0.5, 0, 0], [0, 1.5, 0.0], [0, 0, 1]]) - self.check_path(tf_matrix_group.dot(tf_scale_xy_group), + self.check_line(tf_matrix_group.dot(tf_scale_xy_group), [122, 320], [-50, 0], 'path06', result) @@ -82,7 +90,7 @@ class TestGroups(unittest.TestCase): [np.sin(a_07), np.cos(a_07), 0], [0, 0, 1]]) - self.check_path(tf_matrix_group.dot(tf_rotate_group), + self.check_line(tf_matrix_group.dot(tf_rotate_group), [183, 183], [0, 30], 'path07', result) @@ -93,21 +101,21 @@ class TestGroups(unittest.TestCase): tf_rotate_xy_group_T = np.matrix([[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_path(tf_matrix_group.dot(tf_rotate_xy_group), + 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.matrix([[1, np.tan(a_09), 0], [0, 1, 0], [0, 0, 1]]) - self.check_path(tf_matrix_group.dot(tf_skew_x_group), + 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.matrix([[1, 0, 0], [np.tan(a_10), 1, 0], [0, 0, 1]]) - self.check_path(tf_matrix_group.dot(tf_skew_y_group), + self.check_line(tf_matrix_group.dot(tf_skew_y_group), [183, 183], [40, 40], 'path10', result) From ff96a37d16c415a14f42908b8ff05e3855cbd233 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Fri, 25 May 2018 16:50:40 +0800 Subject: [PATCH 16/18] Add a test for elements that contain a 'transform' attribute --- test/groups.svg | 8 ++++++++ test/test_groups.py | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/test/groups.svg b/test/groups.svg index 804a6b8..1787617 100644 --- a/test/groups.svg +++ b/test/groups.svg @@ -146,6 +146,14 @@ stroke-width="3" test:name="path10"/> + + diff --git a/test/test_groups.py b/test/test_groups.py index 672e0fa..c3a1aef 100644 --- a/test/test_groups.py +++ b/test/test_groups.py @@ -42,10 +42,13 @@ class TestGroups(unittest.TestCase): 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. doc = Document(join(dirname(__file__), 'groups.svg')) result = doc.flatten_all_paths() - self.assertEqual(11, len(result)) + self.assertEqual(12, len(result)) tf_matrix_group = np.matrix([[1.5, 0.0, -40.0], [0.0, 0.5, 20.0], [0.0, 0.0, 1.0]]) @@ -119,4 +122,14 @@ class TestGroups(unittest.TestCase): [183, 183], [40, 40], 'path10', result) - # TODO: Add a test where a path element has a transform attribute + # 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.matrix([[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.matrix([[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), + [180, 20], [-70, 80], + 'path11', result) From 79ab1e6a4396dc78fb675a1ce0cf1508f9962ceb Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Tue, 29 May 2018 12:42:25 +0800 Subject: [PATCH 17/18] Remove accidental paranthesis --- svgpathtools/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index 07fb0e1..a4d710f 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -275,7 +275,7 @@ def _parse_transform_substr(transform_substr): transform[1, 0] = np.tan(values[0] * np.pi / 180.0) else: # Return an identity matrix if the type of transform is unknown, and warn the user - warnings.warn('Unknown SVG transform type: {0})'.format(type_str)) + warnings.warn('Unknown SVG transform type: {0}'.format(type_str)) return transform From 0fdaad0284f537a15ac9f008042ee0fd465b2c3a Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Tue, 29 May 2018 12:54:20 +0800 Subject: [PATCH 18/18] Remove unnecessary import --- svgpathtools/svg2paths.py | 1 - 1 file changed, 1 deletion(-) diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index fcfe87c..e80645f 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -6,7 +6,6 @@ from __future__ import division, absolute_import, print_function from xml.dom.minidom import parse from os import path as os_path, getcwd import re -import xml.etree.cElementTree as etree # Internal dependencies from .parser import parse_path