From dcf8203a426550fbe74f00c49d7c5208720f1781 Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Fri, 7 Apr 2017 02:03:24 +0200 Subject: [PATCH 1/5] Fixed #16 Groups are now taken into account during parsing and transform attributes of groups are applied to the paths before they are returned. --- svgpathtools/svg2paths.py | 128 ++++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 27 deletions(-) diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index df519a5..6d1ded3 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -5,9 +5,12 @@ from __future__ import division, absolute_import, print_function from xml.dom.minidom import parse from os import path as os_path, getcwd +from shutil import copyfile +import numpy as np # Internal dependencies from .parser import parse_path +from .path import Path, bpoints2bezier def polyline2pathd(polyline_d): @@ -71,51 +74,122 @@ def svg2paths(svg_file_location, svg-attributes will be extracted and returned :return: list of Path objects, list of path attribute dictionaries, and (optionally) a dictionary of svg-attributes + """ if os_path.dirname(svg_file_location) == '': svg_file_location = os_path.join(getcwd(), svg_file_location) + # if pathless_svg: + # copyfile(svg_file_location, pathless_svg) + # doc = parse(pathless_svg) + # else: doc = parse(svg_file_location) + # Parse a list of paths def dom2dict(element): """Converts DOM elements to dictionaries of attributes.""" keys = list(element.attributes.keys()) values = [val.value for val in list(element.attributes.values())] return dict(list(zip(keys, values))) - # Use minidom to extract path strings from input SVG - paths = [dom2dict(el) for el in doc.getElementsByTagName('path')] - d_strings = [el['d'] for el in paths] - attribute_dictionary_list = paths - - # Use minidom to extract polyline strings from input SVG, convert to - # path strings, add to list - if convert_polylines_to_paths: - plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')] - d_strings += [polyline2pathd(pl['points']) for pl in plins] - attribute_dictionary_list += plins - - # Use minidom to extract polygon strings from input SVG, convert to - # path strings, add to list - if convert_polygons_to_paths: - pgons = [dom2dict(el) for el in doc.getElementsByTagName('polygon')] - d_strings += [polygon2pathd(pg['points']) for pg in pgons] - attribute_dictionary_list += pgons - - if convert_lines_to_paths: - lines = [dom2dict(el) for el in doc.getElementsByTagName('line')] - d_strings += [('M' + l['x1'] + ' ' + l['y1'] + - 'L' + l['x2'] + ' ' + l['y2']) for l in lines] - attribute_dictionary_list += lines - + def parseTrafo(trafoStr): + """Returns six matrix elements for a matrix transformation for any valid SVG transformation string.""" + #print(trafoStr) + valueStr = trafoStr.split('(')[1].split(')')[0] + values = list(map(float, valueStr.split(','))) + if 'translate' in trafoStr: + x = values[0] + y = values[1] if (len(values) > 1) else 0. + return [1.,0.,0.,1.,x,y] + elif 'scale' in trafoStr: + x = values[0] + y = values[1] if (len(values) > 1) else 0. + return [x,0.,0.,y,0.,0.] + elif 'rotate' in trafoStr: + a = values[0] + x = values[1] if (len(values) > 1) else 0. + y = values[2] if (len(values) > 2) else 0. + A = np.dot([cos(a),sin(a),-sin(a),cos(a),0.,0.,0.,0.,1.].reshape((3,3)),[1.,0.,0.,1.,-x,-y,0.,0.,1.].reshape((3,3))) + A = list(np.dot([1.,0.,0.,1.,x,y,0.,0.,1.].reshape((3,3)),A).reshape((9,))[:6]) + return A + elif 'skewX' in trafoStr: + a = values[0] + return [1.,0.,tan(a),1.,0.,0.] + elif 'skewY' in trafoStr: + a = values[0] + return [1.,tan(a),0.,1.,0.,0.] + else: + while len(values) < 6: + values += [0.] + return values + + def parseNode(node): + """Recursively iterate over nodes. Parse the groups individually to apply group transformations.""" + # Get everything in this tag + #ret_list, attribute_dictionary_list = [parseNode(child) for child in node.childNodes] + data = [parseNode(child) for child in node.childNodes] + if len(data) == 0: + ret_list = [] + attribute_dictionary_list = [] + else: + # Flatten the lists + ret_list = [] + attribute_dictionary_list = [] + for item in data: + if type(item) == tuple: + if len(item[0]) > 0: + ret_list += item[0] + attribute_dictionary_list += item[1] + + if node.nodeName == 'g': + # Group found + # Analyse group properties + group = dom2dict(node) + if 'transform' in group.keys(): + trafo = group['transform'] + + # Convert all transformations into a matrix operation + A = parseTrafo(trafo) + A = np.array([A[::2],A[1::2],[0.,0.,1.]]) + + # Apply transformation to all elements of the paths + xy = lambda z: np.array([z.real, z.imag, 1.]) + z = lambda xy: xy[0] + 1j*xy[1] + + ret_list = [Path(*[bpoints2bezier([z(np.dot(A,xy(pt))) + for pt in seg.bpoints()]) + for seg in path]) + for path in ret_list] + return ret_list, attribute_dictionary_list + elif node.nodeName == 'path': + # Path found; parsing it + path = dom2dict(node) + d_string = path['d'] + return [parse_path(d_string)]+ret_list, [path]+attribute_dictionary_list + elif convert_polylines_to_paths and node.nodeName == 'polyline': + attrs = dom2dict(node) + path = parse_path(polyline2pathd(node['points'])) + return [path]+ret_list, [attrs]+attribute_dictionary_list + elif convert_polygons_to_paths and node.nodeName == 'polygon': + attrs = dom2dict(node) + path = parse_path(polygon2pathd(node['points'])) + return [path]+ret_list, [attrs]+attribute_dictionary_list + elif convert_lines_to_paths and node.nodeName == 'line': + line = dom2dict(node) + d_string = ('M' + line['x1'] + ' ' + line['y1'] + + 'L' + line['x2'] + ' ' + line['y2']) + path = parse_path(d_string) + return [path]+ret_list, [line]+attribute_dictionary_list + else: + return ret_list, attribute_dictionary_list + + path_list, attribute_dictionary_list = parseNode(doc) if return_svg_attributes: svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0]) doc.unlink() - path_list = [parse_path(d) for d in d_strings] return path_list, attribute_dictionary_list, svg_attributes else: doc.unlink() - path_list = [parse_path(d) for d in d_strings] return path_list, attribute_dictionary_list From 8542afb77cd25744723f4eee0ce10c8d0b27af34 Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Sun, 9 Apr 2017 10:53:04 +0200 Subject: [PATCH 2/5] Made code follow PEP8 --- svgpathtools/svg2paths.py | 67 ++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index 6d1ded3..93f47fb 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -5,7 +5,6 @@ from __future__ import division, absolute_import, print_function from xml.dom.minidom import parse from os import path as os_path, getcwd -from shutil import copyfile import numpy as np # Internal dependencies @@ -92,54 +91,53 @@ def dom2dict(element): values = [val.value for val in list(element.attributes.values())] return dict(list(zip(keys, values))) - def parseTrafo(trafoStr): + def parse_trafo(trafo_str): """Returns six matrix elements for a matrix transformation for any valid SVG transformation string.""" - #print(trafoStr) - valueStr = trafoStr.split('(')[1].split(')')[0] - values = list(map(float, valueStr.split(','))) - if 'translate' in trafoStr: + value_str = trafo_str.split('(')[1].split(')')[0] + values = list(map(float, value_str.split(','))) + if 'translate' in trafo_str: x = values[0] y = values[1] if (len(values) > 1) else 0. - return [1.,0.,0.,1.,x,y] - elif 'scale' in trafoStr: + return [1., 0., 0., 1., x, y] + elif 'scale' in trafo_str: x = values[0] y = values[1] if (len(values) > 1) else 0. - return [x,0.,0.,y,0.,0.] - elif 'rotate' in trafoStr: + return [x, 0., 0., y, 0., 0.] + elif 'rotate' in trafo_str: a = values[0] x = values[1] if (len(values) > 1) else 0. y = values[2] if (len(values) > 2) else 0. - A = np.dot([cos(a),sin(a),-sin(a),cos(a),0.,0.,0.,0.,1.].reshape((3,3)),[1.,0.,0.,1.,-x,-y,0.,0.,1.].reshape((3,3))) - A = list(np.dot([1.,0.,0.,1.,x,y,0.,0.,1.].reshape((3,3)),A).reshape((9,))[:6]) - return A - elif 'skewX' in trafoStr: + am = np.dot(np.array([np.cos(a), np.sin(a), -np.sin(a), np.cos(a), 0., 0., 0., 0., 1.]).reshape((3, 3)), + np.array([1., 0., 0., 1., -x, -y, 0., 0., 1.]).reshape((3, 3))) + am = list(np.dot(np.array([1., 0., 0., 1., x, y, 0., 0., 1.]).reshape((3, 3)), am).reshape((9, ))[:6]) + return am + elif 'skewX' in trafo_str: a = values[0] - return [1.,0.,tan(a),1.,0.,0.] - elif 'skewY' in trafoStr: + return [1., 0., np.tan(a), 1., 0., 0.] + elif 'skewY' in trafo_str: a = values[0] - return [1.,tan(a),0.,1.,0.,0.] + return [1., np.tan(a), 0., 1., 0., 0.] else: while len(values) < 6: - values += [0.] + values += [0.] return values - def parseNode(node): + def parse_node(node): """Recursively iterate over nodes. Parse the groups individually to apply group transformations.""" # Get everything in this tag - #ret_list, attribute_dictionary_list = [parseNode(child) for child in node.childNodes] - data = [parseNode(child) for child in node.childNodes] + data = [parse_node(child) for child in node.childNodes] if len(data) == 0: ret_list = [] - attribute_dictionary_list = [] + attribute_dictionary_list_int = [] else: # Flatten the lists ret_list = [] - attribute_dictionary_list = [] + attribute_dictionary_list_int = [] for item in data: if type(item) == tuple: if len(item[0]) > 0: ret_list += item[0] - attribute_dictionary_list += item[1] + attribute_dictionary_list_int += item[1] if node.nodeName == 'g': # Group found @@ -149,18 +147,21 @@ def parseNode(node): trafo = group['transform'] # Convert all transformations into a matrix operation - A = parseTrafo(trafo) - A = np.array([A[::2],A[1::2],[0.,0.,1.]]) + am = parse_trafo(trafo) + am = np.array([am[::2], am[1::2], [0., 0., 1.]]) # Apply transformation to all elements of the paths - xy = lambda z: np.array([z.real, z.imag, 1.]) - z = lambda xy: xy[0] + 1j*xy[1] + def xy(p): + return np.array([p.real, p.imag, 1.]) + + def z(coords): + return coords[0] + 1j*coords[1] - ret_list = [Path(*[bpoints2bezier([z(np.dot(A,xy(pt))) - for pt in seg.bpoints()]) - for seg in path]) + ret_list = [Path(*[bpoints2bezier([z(np.dot(am, xy(pt))) + for pt in seg.bpoints()]) + for seg in path]) for path in ret_list] - return ret_list, attribute_dictionary_list + return ret_list, attribute_dictionary_list_int elif node.nodeName == 'path': # Path found; parsing it path = dom2dict(node) @@ -183,7 +184,7 @@ def parseNode(node): else: return ret_list, attribute_dictionary_list - path_list, attribute_dictionary_list = parseNode(doc) + path_list, attribute_dictionary_list = parse_node(doc) if return_svg_attributes: svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0]) doc.unlink() From 3a2cd2c7a0992da4d59e3dac5b8504dc55dee9b2 Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Sun, 9 Apr 2017 16:13:15 +0200 Subject: [PATCH 3/5] Unit test added for transformations. Transformation bugs fixed. --- svgpathtools/svg2paths.py | 25 +++++------ test/groups.svg | 81 +++++++++++++++++++++++++++++++++++ test/test_svg2paths_groups.py | 14 ++++++ 3 files changed, 108 insertions(+), 12 deletions(-) create mode 100644 test/groups.svg create mode 100644 test/test_svg2paths_groups.py diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index 93f47fb..e0602b1 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -104,18 +104,19 @@ def parse_trafo(trafo_str): y = values[1] if (len(values) > 1) else 0. return [x, 0., 0., y, 0., 0.] elif 'rotate' in trafo_str: - a = values[0] + a = values[0]*np.pi/180. x = values[1] if (len(values) > 1) else 0. y = values[2] if (len(values) > 2) else 0. - am = np.dot(np.array([np.cos(a), np.sin(a), -np.sin(a), np.cos(a), 0., 0., 0., 0., 1.]).reshape((3, 3)), - np.array([1., 0., 0., 1., -x, -y, 0., 0., 1.]).reshape((3, 3))) - am = list(np.dot(np.array([1., 0., 0., 1., x, y, 0., 0., 1.]).reshape((3, 3)), am).reshape((9, ))[:6]) + am = np.dot(np.array([np.cos(a), -np.sin(a), 0., np.sin(a), np.cos(a), 0., 0., 0., 1.]).reshape((3, 3)), + np.array([1., 0., -x, 0., 1., -y, 0., 0., 1.]).reshape((3, 3))) + am = list(np.dot(np.array([1., 0., x, 0., 1., y, 0., 0., 1.]).reshape((3, 3)), am).reshape((9, ))[:6]) + am = am[::3]+am[1::3]+am[2::3] return am elif 'skewX' in trafo_str: - a = values[0] + a = values[0]*np.pi/180. return [1., 0., np.tan(a), 1., 0., 0.] elif 'skewY' in trafo_str: - a = values[0] + a = values[0]*np.pi/180. return [1., np.tan(a), 0., 1., 0., 0.] else: while len(values) < 6: @@ -166,23 +167,23 @@ def z(coords): # Path found; parsing it path = dom2dict(node) d_string = path['d'] - return [parse_path(d_string)]+ret_list, [path]+attribute_dictionary_list + return [parse_path(d_string)]+ret_list, [path]+attribute_dictionary_list_int elif convert_polylines_to_paths and node.nodeName == 'polyline': attrs = dom2dict(node) path = parse_path(polyline2pathd(node['points'])) - return [path]+ret_list, [attrs]+attribute_dictionary_list + return [path]+ret_list, [attrs]+attribute_dictionary_list_int elif convert_polygons_to_paths and node.nodeName == 'polygon': attrs = dom2dict(node) - path = parse_path(polygon2pathd(node['points'])) - return [path]+ret_list, [attrs]+attribute_dictionary_list + path = parse_path(polygon2pathd(attrs['points'])) + return [path]+ret_list, [attrs]+attribute_dictionary_list_int elif convert_lines_to_paths and node.nodeName == 'line': line = dom2dict(node) d_string = ('M' + line['x1'] + ' ' + line['y1'] + 'L' + line['x2'] + ' ' + line['y2']) path = parse_path(d_string) - return [path]+ret_list, [line]+attribute_dictionary_list + return [path]+ret_list, [line]+attribute_dictionary_list_int else: - return ret_list, attribute_dictionary_list + return ret_list, attribute_dictionary_list_int path_list, attribute_dictionary_list = parse_node(doc) if return_svg_attributes: diff --git a/test/groups.svg b/test/groups.svg new file mode 100644 index 0000000..1b31c7a --- /dev/null +++ b/test/groups.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_svg2paths_groups.py b/test/test_svg2paths_groups.py new file mode 100644 index 0000000..c548bac --- /dev/null +++ b/test/test_svg2paths_groups.py @@ -0,0 +1,14 @@ +from __future__ import division, absolute_import, print_function +from unittest import TestCase +from svgpathtools import * +from os.path import join, dirname + +class TestSvg2pathsGroups(TestCase): + def test_svg2paths(self): + paths, _ = svg2paths(join(dirname(__file__), 'groups.svg')) + + # the paths should form crosses after being transformed + self.assertTrue((len(paths) % 2) == 0) + + for i in range(len(paths)//2): + self.assertTrue(len(paths[i * 2].intersect(paths[i * 2 + 1])) > 0, 'Path '+str(i * 2)+' does not intersect path '+str(i * 2 + 1)+'!') \ No newline at end of file From fb49d5b752f26f1cfdf11f0c9b5ab6c53e92bd1e Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Mon, 10 Apr 2017 09:19:14 +0200 Subject: [PATCH 4/5] List of transformations in transform attribute are now parsed correctly. --- svgpathtools/svg2paths.py | 70 ++++++++++++++++++++--------------- test/groups.svg | 13 ++++++- test/test_svg2paths_groups.py | 1 + 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index e0602b1..fc3a42b 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -93,35 +93,47 @@ def dom2dict(element): def parse_trafo(trafo_str): """Returns six matrix elements for a matrix transformation for any valid SVG transformation string.""" - value_str = trafo_str.split('(')[1].split(')')[0] - values = list(map(float, value_str.split(','))) - if 'translate' in trafo_str: - x = values[0] - y = values[1] if (len(values) > 1) else 0. - return [1., 0., 0., 1., x, y] - elif 'scale' in trafo_str: - x = values[0] - y = values[1] if (len(values) > 1) else 0. - return [x, 0., 0., y, 0., 0.] - elif 'rotate' in trafo_str: - a = values[0]*np.pi/180. - x = values[1] if (len(values) > 1) else 0. - y = values[2] if (len(values) > 2) else 0. - am = np.dot(np.array([np.cos(a), -np.sin(a), 0., np.sin(a), np.cos(a), 0., 0., 0., 1.]).reshape((3, 3)), - np.array([1., 0., -x, 0., 1., -y, 0., 0., 1.]).reshape((3, 3))) - am = list(np.dot(np.array([1., 0., x, 0., 1., y, 0., 0., 1.]).reshape((3, 3)), am).reshape((9, ))[:6]) - am = am[::3]+am[1::3]+am[2::3] - return am - elif 'skewX' in trafo_str: - a = values[0]*np.pi/180. - return [1., 0., np.tan(a), 1., 0., 0.] - elif 'skewY' in trafo_str: - a = values[0]*np.pi/180. - return [1., np.tan(a), 0., 1., 0., 0.] - else: - while len(values) < 6: - values += [0.] - return values + trafos = trafo_str.split(')')[:-1] + trafo_matrix = np.array([1., 0., 0., 0., 1., 0., 0., 0., 1.]).reshape((3, 3)) # Start with neutral matrix + + for trafo_sub_str in trafos: + trafo_sub_str = trafo_sub_str.lstrip(', ') + value_str = trafo_sub_str.split('(')[1] + values = list(map(float, value_str.split(','))) + if 'translate' in trafo_sub_str: + x = values[0] + y = values[1] if (len(values) > 1) else 0. + trafo_matrix = np.dot(trafo_matrix, + np.array([1., 0., x, 0., 1., y, 0., 0., 1.]).reshape((3, 3))) + elif 'scale' in trafo_sub_str: + x = values[0] + y = values[1] if (len(values) > 1) else 0. + trafo_matrix = np.dot(trafo_matrix, + np.array([x, 0., 0., 0., y, 0., 0., 0., 1.]).reshape((3, 3))) + elif 'rotate' in trafo_sub_str: + a = values[0]*np.pi/180. + x = values[1] if (len(values) > 1) else 0. + y = values[2] if (len(values) > 2) else 0. + am = np.dot(np.array([np.cos(a), -np.sin(a), 0., np.sin(a), np.cos(a), 0., 0., 0., 1.]).reshape((3, 3)), + np.array([1., 0., -x, 0., 1., -y, 0., 0., 1.]).reshape((3, 3))) + am = np.dot(np.array([1., 0., x, 0., 1., y, 0., 0., 1.]).reshape((3, 3)), am) + trafo_matrix = np.dot(trafo_matrix, am) + elif 'skewX' in trafo_sub_str: + a = values[0]*np.pi/180. + trafo_matrix = np.dot(trafo_matrix, + np.array([1., np.tan(a), 0., 0., 1., 0., 0., 0., 1.]).reshape((3, 3))) + elif 'skewY' in trafo_sub_str: + a = values[0]*np.pi/180. + trafo_matrix = np.dot(trafo_matrix, + np.array([1., 0., 0., np.tan(a), 1., 0., 0., 0., 1.]).reshape((3, 3))) + else: # Assume matrix transformation + while len(values) < 6: + values += [0.] + trafo_matrix = np.dot(trafo_matrix, + np.array([values[::2], values[1::2], [0., 0., 1.]])) + + trafo_list = list(trafo_matrix.reshape((9,))[:6]) + return trafo_list[::3]+trafo_list[1::3]+trafo_list[2::3] def parse_node(node): """Recursively iterate over nodes. Parse the groups individually to apply group transformations.""" diff --git a/test/groups.svg b/test/groups.svg index 1b31c7a..2d35114 100644 --- a/test/groups.svg +++ b/test/groups.svg @@ -1,5 +1,5 @@ - + + + + + + diff --git a/test/test_svg2paths_groups.py b/test/test_svg2paths_groups.py index c548bac..acbdbda 100644 --- a/test/test_svg2paths_groups.py +++ b/test/test_svg2paths_groups.py @@ -11,4 +11,5 @@ def test_svg2paths(self): self.assertTrue((len(paths) % 2) == 0) for i in range(len(paths)//2): + print(i * 2) self.assertTrue(len(paths[i * 2].intersect(paths[i * 2 + 1])) > 0, 'Path '+str(i * 2)+' does not intersect path '+str(i * 2 + 1)+'!') \ No newline at end of file From 77cab1e819c366644be6cb356db509606e9afae4 Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Mon, 24 Apr 2017 20:30:10 +0200 Subject: [PATCH 5/5] Limited line length to 79 characters. Also updated to current version of master branch in svgpathtools. --- svgpathtools/svg2paths.py | 155 +++++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 52 deletions(-) diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index fc3a42b..ca0a479 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -29,6 +29,33 @@ def polyline2pathd(polyline_d): return d +def ellipse2pathd(ellipse): + """converts the parameters from an ellipse or a circle to a string for a + Path object d-attribute""" + + cx = ellipse.get('cx', None) + cy = ellipse.get('cy', None) + rx = ellipse.get('rx', None) + ry = ellipse.get('ry', None) + r = ellipse.get('r', None) + + if r is not None: + rx = ry = float(r) + else: + rx = float(rx) + ry = float(ry) + + cx = float(cx) + cy = float(cy) + + d = '' + d += 'M' + str(cx - rx) + ',' + str(cy) + d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0' + d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0' + + return d + + def polygon2pathd(polyline_d): """converts the string from a polygon points-attribute to a string for a Path object d-attribute. @@ -43,10 +70,11 @@ def polygon2pathd(polyline_d): d = 'M' + points[0].replace(',', ' ') for p in points[1:]: d += 'L' + p.replace(',', ' ') - + # The `parse_path` call ignores redundant 'z' (closure) commands # e.g. `parse_path('M0 0L100 100Z') == parse_path('M0 0L100 100L0 0Z')` - # This check ensures that an n-point polygon is converted to an n-Line path. + # This check ensures that an n-point polygon is converted to an n-Line + # path. if reduntantly_closed: d += 'L' + points[0].replace(',', ' ') @@ -54,37 +82,37 @@ def polygon2pathd(polyline_d): def svg2paths(svg_file_location, + return_svg_attributes=False, convert_lines_to_paths=True, convert_polylines_to_paths=True, convert_polygons_to_paths=True, - return_svg_attributes=False): - """ + convert_ellipses_to_paths=True): + """Converts an SVG into a list of Path objects and attribute dictionaries. Converts an SVG file into a list of Path objects and a list of dictionaries containing their attributes. This currently supports - SVG Path, Line, Polyline, and Polygon elements. - :param svg_file_location: the location of the svg file - :param convert_lines_to_paths: Set to False to disclude SVG-Line objects - (converted to Paths) - :param convert_polylines_to_paths: Set to False to disclude SVG-Polyline - objects (converted to Paths) - :param convert_polygons_to_paths: Set to False to disclude SVG-Polygon - objects (converted to Paths) - :param return_svg_attributes: Set to True and a dictionary of - svg-attributes will be extracted and returned - :return: list of Path objects, list of path attribute dictionaries, and - (optionally) a dictionary of svg-attributes - + SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements. + Args: + svg_file_location (string): the location of the svg file + convert_lines_to_paths (bool): Set to False to exclude SVG-Line objects + (converted to Paths) + convert_polylines_to_paths (bool): Set to False to exclude SVG-Polyline + objects (converted to Paths) + convert_polygons_to_paths (bool): Set to False to exclude SVG-Polygon + objects (converted to Paths) + return_svg_attributes (bool): Set to True and a dictionary of + svg-attributes will be extracted and returned + convert_ellipses_to_paths (bool): Set to False to exclude SVG-Ellipse + objects (converted to Paths). Circles are treated as ellipses. + Returns: + list: The list of Path objects. + list: The list of corresponding path attribute dictionaries. + dict (optional): A dictionary of svg-attributes (see `svg2paths2()`). """ if os_path.dirname(svg_file_location) == '': svg_file_location = os_path.join(getcwd(), svg_file_location) - # if pathless_svg: - # copyfile(svg_file_location, pathless_svg) - # doc = parse(pathless_svg) - # else: doc = parse(svg_file_location) - # Parse a list of paths def dom2dict(element): """Converts DOM elements to dictionaries of attributes.""" keys = list(element.attributes.keys()) @@ -92,9 +120,11 @@ def dom2dict(element): return dict(list(zip(keys, values))) def parse_trafo(trafo_str): - """Returns six matrix elements for a matrix transformation for any valid SVG transformation string.""" + """Returns six matrix elements for a matrix transformation for any + valid SVG transformation string.""" trafos = trafo_str.split(')')[:-1] - trafo_matrix = np.array([1., 0., 0., 0., 1., 0., 0., 0., 1.]).reshape((3, 3)) # Start with neutral matrix + trafo_matrix = np.array([1., 0., 0., 0., 1., 0., 0., 0., 1.]).reshape( + (3, 3)) # Start with neutral matrix for trafo_sub_str in trafos: trafo_sub_str = trafo_sub_str.lstrip(', ') @@ -103,40 +133,53 @@ def parse_trafo(trafo_str): if 'translate' in trafo_sub_str: x = values[0] y = values[1] if (len(values) > 1) else 0. - trafo_matrix = np.dot(trafo_matrix, - np.array([1., 0., x, 0., 1., y, 0., 0., 1.]).reshape((3, 3))) + trafo_matrix = np.dot(trafo_matrix, np.array( + [1., 0., x, 0., 1., y, 0., 0., 1.]).reshape((3, 3))) elif 'scale' in trafo_sub_str: x = values[0] y = values[1] if (len(values) > 1) else 0. trafo_matrix = np.dot(trafo_matrix, - np.array([x, 0., 0., 0., y, 0., 0., 0., 1.]).reshape((3, 3))) + np.array([x, 0., 0., 0., y, 0., 0., 0., + 1.]).reshape((3, 3))) elif 'rotate' in trafo_sub_str: - a = values[0]*np.pi/180. + a = values[0] * np.pi / 180. x = values[1] if (len(values) > 1) else 0. y = values[2] if (len(values) > 2) else 0. - am = np.dot(np.array([np.cos(a), -np.sin(a), 0., np.sin(a), np.cos(a), 0., 0., 0., 1.]).reshape((3, 3)), - np.array([1., 0., -x, 0., 1., -y, 0., 0., 1.]).reshape((3, 3))) - am = np.dot(np.array([1., 0., x, 0., 1., y, 0., 0., 1.]).reshape((3, 3)), am) + am = np.dot(np.array( + [np.cos(a), -np.sin(a), 0., np.sin(a), np.cos(a), 0., 0., + 0., 1.]).reshape((3, 3)), + np.array( + [1., 0., -x, 0., 1., -y, 0., 0., 1.]).reshape( + (3, 3))) + am = np.dot( + np.array([1., 0., x, 0., 1., y, 0., 0., 1.]).reshape( + (3, 3)), am) trafo_matrix = np.dot(trafo_matrix, am) elif 'skewX' in trafo_sub_str: - a = values[0]*np.pi/180. + a = values[0] * np.pi / 180. trafo_matrix = np.dot(trafo_matrix, - np.array([1., np.tan(a), 0., 0., 1., 0., 0., 0., 1.]).reshape((3, 3))) + np.array( + [1., np.tan(a), 0., 0., 1., 0., 0., + 0., 1.]).reshape((3, 3))) elif 'skewY' in trafo_sub_str: - a = values[0]*np.pi/180. + a = values[0] * np.pi / 180. trafo_matrix = np.dot(trafo_matrix, - np.array([1., 0., 0., np.tan(a), 1., 0., 0., 0., 1.]).reshape((3, 3))) - else: # Assume matrix transformation + np.array( + [1., 0., 0., np.tan(a), 1., 0., 0., + 0., 1.]).reshape((3, 3))) + else: # Assume matrix transformation while len(values) < 6: values += [0.] trafo_matrix = np.dot(trafo_matrix, - np.array([values[::2], values[1::2], [0., 0., 1.]])) + np.array([values[::2], values[1::2], + [0., 0., 1.]])) trafo_list = list(trafo_matrix.reshape((9,))[:6]) - return trafo_list[::3]+trafo_list[1::3]+trafo_list[2::3] + return trafo_list[::3] + trafo_list[1::3] + trafo_list[2::3] def parse_node(node): - """Recursively iterate over nodes. Parse the groups individually to apply group transformations.""" + """Recursively iterate over nodes. Parse the groups individually to + apply group transformations.""" # Get everything in this tag data = [parse_node(child) for child in node.childNodes] if len(data) == 0: @@ -151,49 +194,55 @@ def parse_node(node): if len(item[0]) > 0: ret_list += item[0] attribute_dictionary_list_int += item[1] - + if node.nodeName == 'g': # Group found # Analyse group properties group = dom2dict(node) if 'transform' in group.keys(): trafo = group['transform'] - + # Convert all transformations into a matrix operation am = parse_trafo(trafo) am = np.array([am[::2], am[1::2], [0., 0., 1.]]) - + # Apply transformation to all elements of the paths def xy(p): return np.array([p.real, p.imag, 1.]) def z(coords): - return coords[0] + 1j*coords[1] - + return coords[0] + 1j * coords[1] + ret_list = [Path(*[bpoints2bezier([z(np.dot(am, xy(pt))) - for pt in seg.bpoints()]) - for seg in path]) + for pt in seg.bpoints()]) + for seg in path]) for path in ret_list] return ret_list, attribute_dictionary_list_int elif node.nodeName == 'path': # Path found; parsing it path = dom2dict(node) d_string = path['d'] - return [parse_path(d_string)]+ret_list, [path]+attribute_dictionary_list_int + return [parse_path(d_string)] + ret_list, [ + path] + attribute_dictionary_list_int elif convert_polylines_to_paths and node.nodeName == 'polyline': attrs = dom2dict(node) path = parse_path(polyline2pathd(node['points'])) - return [path]+ret_list, [attrs]+attribute_dictionary_list_int + return [path] + ret_list, [attrs] + attribute_dictionary_list_int elif convert_polygons_to_paths and node.nodeName == 'polygon': attrs = dom2dict(node) path = parse_path(polygon2pathd(attrs['points'])) - return [path]+ret_list, [attrs]+attribute_dictionary_list_int + return [path] + ret_list, [attrs] + attribute_dictionary_list_int elif convert_lines_to_paths and node.nodeName == 'line': line = dom2dict(node) d_string = ('M' + line['x1'] + ' ' + line['y1'] + 'L' + line['x2'] + ' ' + line['y2']) path = parse_path(d_string) - return [path]+ret_list, [line]+attribute_dictionary_list_int + return [path] + ret_list, [line] + attribute_dictionary_list_int + elif convert_ellipses_to_paths and ( + node.nodeName == 'ellipse' or node.nodeName == 'circle'): + attrs = dom2dict(node) + path = parse_path(ellipse2pathd(attrs)) + return [path] + ret_list, [attrs] + attribute_dictionary_list_int else: return ret_list, attribute_dictionary_list_int @@ -208,15 +257,17 @@ def z(coords): def svg2paths2(svg_file_location, + return_svg_attributes=True, convert_lines_to_paths=True, convert_polylines_to_paths=True, convert_polygons_to_paths=True, - return_svg_attributes=True): + convert_ellipses_to_paths=True): """Convenience function; identical to svg2paths() except that return_svg_attributes=True by default. See svg2paths() docstring for more info.""" return svg2paths(svg_file_location=svg_file_location, + return_svg_attributes=return_svg_attributes, convert_lines_to_paths=convert_lines_to_paths, convert_polylines_to_paths=convert_polylines_to_paths, convert_polygons_to_paths=convert_polygons_to_paths, - return_svg_attributes=return_svg_attributes) + convert_ellipses_to_paths=convert_ellipses_to_paths)