diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 41989de..599e269 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -2238,4 +2238,4 @@ class Path(MutableSequence): def translated(self, z0): """Returns a copy of self shifted by the complex quantity `z0` such that self.translated(z0).point(t) = self.point(t) + z0 for any t.""" - return translate(self, z0) + return translate(self, z0) \ No newline at end of file diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index 8f62932..acc51a4 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -5,9 +5,38 @@ The main tool being the svg2paths() function.""" from __future__ import division, absolute_import, print_function from xml.dom.minidom import parse from os import path as os_path, getcwd +import numpy as np # Internal dependencies from .parser import parse_path +from .path import Path, bpoints2bezier + + +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 ellipse2pathd(ellipse): @@ -71,7 +100,8 @@ def polygon2pathd(polyline_d): # 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(',', ' ') @@ -102,7 +132,7 @@ def svg2paths(svg_file_location, convert_polylines_to_paths=True, convert_polygons_to_paths=True, convert_rectangles_to_paths=True): - """Converts an SVG into a list of Path objects and attribute dictionaries. + """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 @@ -144,30 +174,139 @@ def svg2paths(svg_file_location, 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 + def parse_trafo(trafo_str): + """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 - # 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 + 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.]])) - # 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 + trafo_list = list(trafo_matrix.reshape((9,))[:6]) + return trafo_list[::3] + trafo_list[1::3] + trafo_list[2::3] - 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 parse_node(node): + """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: + ret_list = [] + attribute_dictionary_list_int = [] + else: + # Flatten the lists + ret_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_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] + + 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_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 + 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 + 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 + 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 + elif convert_ellipses_to_paths and node.nodeName == 'ellipse': + attrs = dom2dict(node) + path = parse_path(ellipse2pathd(attrs)) + return [path] + ret_list, [attrs] + attribute_dictionary_list_int + elif convert_circles_to_paths and node.nodeName == 'circle': + attrs = dom2dict(node) + path = parse_path(ellipse2pathd(attrs)) + return [path] + ret_list, [attrs] + attribute_dictionary_list_int + elif convert_rectangles_to_paths and node.nodeName == 'rect': + attrs = dom2dict(node) + path = parse_path(rect2pathd(attrs)) + return [path] + ret_list, [attrs] + attribute_dictionary_list_int + else: + return ret_list, attribute_dictionary_list_int if convert_ellipses_to_paths: ellipses = [dom2dict(el) for el in doc.getElementsByTagName('ellipse')] @@ -187,11 +326,9 @@ def svg2paths(svg_file_location, 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 diff --git a/test/groups.svg b/test/groups.svg new file mode 100644 index 0000000..cde31d8 --- /dev/null +++ b/test/groups.svg @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/test_svg2paths_groups.py b/test/test_svg2paths_groups.py new file mode 100644 index 0000000..acbdbda --- /dev/null +++ b/test/test_svg2paths_groups.py @@ -0,0 +1,15 @@ +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): + 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