From dcf8203a426550fbe74f00c49d7c5208720f1781 Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Fri, 7 Apr 2017 02:03:24 +0200 Subject: [PATCH 01/10] 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 | 124 ++++++++++++++++++++++++++++++-------- 1 file changed, 99 insertions(+), 25 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 @@ 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 +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 + 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 - # 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 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 02/10] 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 @@ 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 -from shutil import copyfile import numpy as np # Internal dependencies @@ -92,54 +91,53 @@ def svg2paths(svg_file_location, 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 svg2paths(svg_file_location, 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 svg2paths(svg_file_location, 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 03/10] 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 svg2paths(svg_file_location, 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 svg2paths(svg_file_location, # 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 04/10] 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 svg2paths(svg_file_location, 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 @@ class TestSvg2pathsGroups(TestCase): 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 05/10] 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 svg2paths(svg_file_location, 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 svg2paths(svg_file_location, 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 svg2paths(svg_file_location, 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 svg2paths(svg_file_location, 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) From ecdade1be3872183489e2dc10c06f22a1fa848c5 Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Wed, 17 May 2017 12:26:38 +0200 Subject: [PATCH 06/10] PEP8 and fix in unittest --- build/lib/svgpathtools/path.py | 8 +- build/lib/svgpathtools/svg2paths.py | 168 +++- svgpathtools.egg-info/PKG-INFO | 1320 ++++++++++++++------------- svgpathtools.egg-info/SOURCES.txt | 9 +- test/groups.svg | 2 +- 5 files changed, 808 insertions(+), 699 deletions(-) diff --git a/build/lib/svgpathtools/path.py b/build/lib/svgpathtools/path.py index ec23b71..7afd58e 100644 --- a/build/lib/svgpathtools/path.py +++ b/build/lib/svgpathtools/path.py @@ -2136,7 +2136,13 @@ class Path(MutableSequence): def cropped(self, T0, T1): """returns a cropped copy of the path.""" + assert 0 <= T0 <= 1 and 0 <= T1<= 1 assert T0 != T1 + assert not (T0 == 1 and T1 == 0) + + if T0 == 1 and 0 < T1 < 1 and self.isclosed(): + return self.cropped(0, T1) + if T1 == 1: seg1 = self[-1] t_seg1 = 1 @@ -2171,7 +2177,7 @@ class Path(MutableSequence): # T1 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 += [polyline2pathd(pg['points']) + 'z' 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.]) - # if pathless_svg: - # with open(pathless_svg, "wb") as f: - # doc.writexml(f) + 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 + else: + return ret_list, attribute_dictionary_list_int + path_list, attribute_dictionary_list = parse_node(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 diff --git a/svgpathtools.egg-info/PKG-INFO b/svgpathtools.egg-info/PKG-INFO index edc988c..85717c6 100644 --- a/svgpathtools.egg-info/PKG-INFO +++ b/svgpathtools.egg-info/PKG-INFO @@ -1,657 +1,663 @@ -Metadata-Version: 1.1 -Name: svgpathtools -Version: 1.3.1 -Summary: A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves. -Home-page: https://github.com/mathandy/svgpathtools -Author: Andy Port -Author-email: AndyAPort@gmail.com -License: MIT -Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.1 -Description: svgpathtools - ============ - - svgpathtools is a collection of tools for manipulating and analyzing SVG - Path objects and Bézier curves. - - Features - -------- - - svgpathtools contains functions designed to **easily read, write and - display SVG files** as well as *a large selection of - geometrically-oriented tools* to **transform and analyze path - elements**. - - Additionally, the submodule *bezier.py* contains tools for for working - with general **nth order Bezier curves stored as n-tuples**. - - Some included tools: - - - **read**, **write**, and **display** SVG files containing Path (and - other) SVG elements - - convert Bézier path segments to **numpy.poly1d** (polynomial) objects - - convert polynomials (in standard form) to their Bézier form - - compute **tangent vectors** and (right-hand rule) **normal vectors** - - compute **curvature** - - break discontinuous paths into their **continuous subpaths**. - - efficiently compute **intersections** between paths and/or segments - - find a **bounding box** for a path or segment - - **reverse** segment/path orientation - - **crop** and **split** paths and segments - - **smooth** paths (i.e. smooth away kinks to make paths - differentiable) - - **transition maps** from path domain to segment domain and back (T2t - and t2T) - - compute **area** enclosed by a closed path - - compute **arc length** - - compute **inverse arc length** - - convert RGB color tuples to hexadecimal color strings and back - - Note on Python 3 - ---------------- - While I am hopeful that this package entirely works with Python 3, it was born from a larger project coded in Python 2 and has not been thoroughly tested in - Python 3. Please let me know if you find any incompatibilities. - - Prerequisites - ------------- - - - **numpy** - - **svgwrite** - - Setup - ----- - - If not already installed, you can **install the prerequisites** using - pip. - - .. code:: bash - - $ pip install numpy - - .. code:: bash - - $ pip install svgwrite - - Then **install svgpathtools**: - - .. code:: bash - - $ pip install svgpathtools - - Alternative Setup - ~~~~~~~~~~~~~~~~~ - - You can download the source from Github and install by using the command - (from inside the folder containing setup.py): - - .. code:: bash - - $ python setup.py install - - Credit where credit's due - ------------------------- - - Much of the core of this module was taken from `the svg.path (v2.0) - module `__. Interested svg.path - users should see the compatibility notes at bottom of this readme. - - Also, a big thanks to the author(s) of `A Primer on Bézier Curves `_, an outstanding resource for learning about Bézier curves and Bézier curve-related algorithms. - - Basic Usage - ----------- - - Classes - ~~~~~~~ - - The svgpathtools module is primarily structured around four path segment - classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``. - There is also a fifth class, ``Path``, whose objects are sequences of - (connected or disconnected\ `1 <#f1>`__\ ) path segment objects. - - - ``Line(start, end)`` - - - ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See - docstring for a detailed explanation of these parameters - - - ``QuadraticBezier(start, control, end)`` - - - ``CubicBezier(start, control1, control2, end)`` - - - ``Path(*segments)`` - - See the relevant docstrings in *path.py* or the `official SVG - specifications `__ for more - information on what each parameter means. - - 1 Warning: Some of the functionality in this library has not been tested - on discontinuous Path objects. A simple workaround is provided, however, - by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__ - - .. code:: python - - from __future__ import division, print_function - - .. code:: python - - # Coordinates are given as points in the complex plane - from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc - seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300) - seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350) - path = Path(seg1, seg2) # A path traversing the cubic and then the line - - # We could alternatively created this Path object using a d-string - from svgpathtools import parse_path - path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350') - - # Let's check that these two methods are equivalent - print(path) - print(path_alt) - print(path == path_alt) - - # On a related note, the Path.d() method returns a Path object's d-string - print(path.d()) - print(parse_path(path.d()) == path) - - - .. parsed-literal:: - - Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), - Line(start=(200+300j), end=(250+350j))) - Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), - Line(start=(200+300j), end=(250+350j))) - True - M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0 - True - - - The ``Path`` class is a mutable sequence, so it behaves much like a - list. So segments can **append**\ ed, **insert**\ ed, set by index, - **del**\ eted, **enumerate**\ d, **slice**\ d out, etc. - - .. code:: python - - # Let's append another to the end of it - path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j)) - print(path) - - # Let's replace the first segment with a Line object - path[0] = Line(200+100j, 200+300j) - print(path) - - # You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end) - print("path is continuous? ", path.iscontinuous()) - print("path is closed? ", path.isclosed()) - - # The curve the path follows is not, however, smooth (differentiable) - from svgpathtools import kinks, smoothed_path - print("path contains non-differentiable points? ", len(kinks(path)) > 0) - - # If we want, we can smooth these out (Experimental and only for line/cubic paths) - # Note: smoothing will always works (except on 180 degree turns), but you may want - # to play with the maxjointsize and tightness parameters to get pleasing results - # Note also: smoothing will increase the number of segments in a path - spath = smoothed_path(path) - print("spath contains non-differentiable points? ", len(kinks(spath)) > 0) - print(spath) - - # Let's take a quick look at the path and its smoothed relative - # The following commands will open two browser windows to display path and spaths - from svgpathtools import disvg - from time import sleep - disvg(path) - sleep(1) # needed when not giving the SVGs unique names (or not using timestamp) - disvg(spath) - print("Notice that path contains {} segments and spath contains {} segments." - "".format(len(path), len(spath))) - - - .. parsed-literal:: - - Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), - Line(start=(200+300j), end=(250+350j)), - CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) - Path(Line(start=(200+100j), end=(200+300j)), - Line(start=(200+300j), end=(250+350j)), - CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) - path is continuous? True - path is closed? True - path contains non-differentiable points? True - spath contains non-differentiable points? False - Path(Line(start=(200+101.5j), end=(200+298.5j)), - CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)), - Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)), - CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)), - CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)), - CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j))) - Notice that path contains 3 segments and spath contains 6 segments. - - - Reading SVGSs - ~~~~~~~~~~~~~ - - | The **svg2paths()** function converts an svgfile to a list of Path - objects and a separate list of dictionaries containing the attributes - of each said path. - | Note: Line, Polyline, Polygon, and Path SVG elements can all be - converted to Path objects using this function. - - .. code:: python - - # Read SVG into a list of path objects and list of dictionaries of attributes - from svgpathtools import svg2paths, wsvg - paths, attributes = svg2paths('test.svg') - - # Update: You can now also extract the svg-attributes by setting - # return_svg_attributes=True, or with the convenience function svg2paths2 - from svgpathtools import svg2paths2 - paths, attributes, svg_attributes = svg2paths2('test.svg') - - # Let's print out the first path object and the color it was in the SVG - # We'll see it is composed of two CubicBezier objects and, in the SVG file it - # came from, it was red - redpath = paths[0] - redpath_attribs = attributes[0] - print(redpath) - print(redpath_attribs['stroke']) - - - .. parsed-literal:: - - Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)), - CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j))) - red - - - Writing SVGSs (and some geometric functions and methods) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - The **wsvg()** function creates an SVG file from a list of path. This - function can do many things (see docstring in *paths2svg.py* for more - information) and is meant to be quick and easy to use. Note: Use the - convenience function **disvg()** (or set 'openinbrowser=True') to - automatically attempt to open the created svg file in your default SVG - viewer. - - .. code:: python - - # Let's make a new SVG that's identical to the first - wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg') - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output1.svg - :alt: output1.svg - - output1.svg - - There will be many more examples of writing and displaying path data - below. - - The .point() method and transitioning between path and path segment parameterizations - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - SVG Path elements and their segments have official parameterizations. - These parameterizations can be accessed using the ``Path.point()``, - ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, - and ``Arc.point()`` methods. All these parameterizations are defined - over the domain 0 <= t <= 1. - - | **Note:** In this document and in inline documentation and doctrings, - I use a capital ``T`` when referring to the parameterization of a Path - object and a lower case ``t`` when referring speaking about path - segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc - objects). - | Given a ``T`` value, the ``Path.T2t()`` method can be used to find the - corresponding segment index, ``k``, and segment parameter, ``t``, such - that ``path.point(T)=path[k].point(t)``. - | There is also a ``Path.t2T()`` method to solve the inverse problem. - - .. code:: python - - # Example: - - # Let's check that the first segment of redpath starts - # at the same point as redpath - firstseg = redpath[0] - print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start) - - # Let's check that the last segment of redpath ends on the same point as redpath - lastseg = redpath[-1] - print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end) - - # This next boolean should return False as redpath is composed multiple segments - print(redpath.point(0.5) == firstseg.point(0.5)) - - # If we want to figure out which segment of redpoint the - # point redpath.point(0.5) lands on, we can use the path.T2t() method - k, t = redpath.T2t(0.5) - print(redpath[k].point(t) == redpath.point(0.5)) - - - .. parsed-literal:: - - True - True - False - True - - - Tangent vectors and Bezier curves as numpy polynomial objects - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - | Another great way to work with the parameterizations for Line, - QuadraticBezier, and CubicBezier objects is to convert them to - ``numpy.poly1d`` objects. This is done easily using the - ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` - methods. - | There's also a ``polynomial2bezier()`` function in the pathtools.py - submodule to convert polynomials back to Bezier curves. - - **Note:** cubic Bezier curves are parameterized as - - .. math:: \mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3 - - where :math:`P_0`, :math:`P_1`, :math:`P_2`, and :math:`P_3` are the - control points ``start``, ``control1``, ``control2``, and ``end``, - respectively, that svgpathtools uses to define a CubicBezier object. The - ``CubicBezier.poly()`` method expands this polynomial to its standard - form - - .. math:: \mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3 - - where - - .. math:: - - \begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} = - \begin{bmatrix} - -1 & 3 & -3 & 1\\ - 3 & -6 & -3 & 0\\ - -3 & 3 & 0 & 0\\ - 1 & 0 & 0 & 0\\ - \end{bmatrix} - \begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix} - - QuadraticBezier.poly() and Line.poly() are defined similarly. - - .. code:: python - - # Example: - b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) - p = b.poly() - - # p(t) == b.point(t) - print(p(0.235) == b.point(0.235)) - - # What is p(t)? It's just the cubic b written in standard form. - bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints()) - print("The CubicBezier, b.point(x) = \n\n" + - bpretty + "\n\n" + - "can be rewritten in standard form as \n\n" + - str(p).replace('x','t')) - - - .. parsed-literal:: - - True - The CubicBezier, b.point(x) = - - (300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3 - - can be rewritten in standard form as - - 3 2 - (-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j) - - - To illustrate the awesomeness of being able to convert our Bezier curve - objects to numpy.poly1d objects and back, lets compute the unit tangent - vector of the above CubicBezier object, b, at t=0.5 in four different - ways. - - Tangent vectors (and more on polynomials) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - .. code:: python - - t = 0.5 - ### Method 1: the easy way - u1 = b.unit_tangent(t) - - ### Method 2: another easy way - # Note: This way will fail if it encounters a removable singularity. - u2 = b.derivative(t)/abs(b.derivative(t)) - - ### Method 2: a third easy way - # Note: This way will also fail if it encounters a removable singularity. - dp = p.deriv() - u3 = dp(t)/abs(dp(t)) - - ### Method 4: the removable-singularity-proof numpy.poly1d way - # Note: This is roughly how Method 1 works - from svgpathtools import real, imag, rational_limit - dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy - p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2 - # Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and, - # the limit_{t->t0}[f(t) / abs(f(t))] == - # sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2]) - from cmath import sqrt - u4 = sqrt(rational_limit(dp**2, p_mag2, t)) - - print("unit tangent check:", u1 == u2 == u3 == u4) - - # Let's do a visual check - mag = b.length()/4 # so it's not hard to see the tangent line - tangent_line = Line(b.point(t), b.point(t) + mag*u1) - disvg([b, tangent_line], 'bg', nodes=[b.point(t)]) - - - .. parsed-literal:: - - unit tangent check: True - - - Translations (shifts), reversing orientation, and normal vectors - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - .. code:: python - - # Speaking of tangents, let's add a normal vector to the picture - n = b.normal(t) - normal_line = Line(b.point(t), b.point(t) + mag*n) - disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)]) - - # and let's reverse the orientation of b! - # the tangent and normal lines should be sent to their opposites - br = b.reversed() - - # Let's also shift b_r over a bit to the right so we can view it next to b - # The simplest way to do this is br = br.translated(3*mag), but let's use - # the .bpoints() instead, which returns a Bezier's control points - br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] # - - tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t)) - normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t)) - wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r], - 'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg', - text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r]) - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/vectorframes.svg - :alt: vectorframes.svg - - vectorframes.svg - - Rotations and Translations - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - .. code:: python - - # Let's take a Line and an Arc and make some pictures - top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1) - midline = Line(-1.5, 1.5) - - # First let's make our ellipse whole - bottom_half = top_half.rotated(180) - decorated_ellipse = Path(top_half, bottom_half) - - # Now let's add the decorations - for k in range(12): - decorated_ellipse.append(midline.rotated(30*k)) - - # Let's move it over so we can see the original Line and Arc object next - # to the final product - decorated_ellipse = decorated_ellipse.translated(4+0j) - wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg') - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/decorated_ellipse.svg - :alt: decorated\_ellipse.svg - - decorated\_ellipse.svg - - arc length and inverse arc length - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Here we'll create an SVG that shows off the parametric and geometric - midpoints of the paths from ``test.svg``. We'll need to compute use the - ``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``, - ``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the - related inverse arc length methods ``.ilength()`` function to do this. - - .. code:: python - - # First we'll load the path data from the file test.svg - paths, attributes = svg2paths('test.svg') - - # Let's mark the parametric midpoint of each segment - # I say "parametric" midpoint because Bezier curves aren't - # parameterized by arclength - # If they're also the geometric midpoint, let's mark them - # purple and otherwise we'll mark the geometric midpoint green - min_depth = 5 - error = 1e-4 - dots = [] - ncols = [] - nradii = [] - for path in paths: - for seg in path: - parametric_mid = seg.point(0.5) - seg_length = seg.length() - if seg.length(0.5)/seg.length() == 1/2: - dots += [parametric_mid] - ncols += ['purple'] - nradii += [5] - else: - t_mid = seg.ilength(seg_length/2) - geo_mid = seg.point(t_mid) - dots += [parametric_mid, geo_mid] - ncols += ['red', 'green'] - nradii += [5] * 2 - - # In 'output2.svg' the paths will retain their original attributes - wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, - attributes=attributes, filename='output2.svg') - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output2.svg - :alt: output2.svg - - output2.svg - - Intersections between Bezier curves - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - .. code:: python - - # Let's find all intersections between redpath and the other - redpath = paths[0] - redpath_attribs = attributes[0] - intersections = [] - for path in paths[1:]: - for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path): - intersections.append(redpath.point(T1)) - - disvg(paths, filename='output_intersections.svg', attributes=attributes, - nodes = intersections, node_radii = [5]*len(intersections)) - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output_intersections.svg - :alt: output\_intersections.svg - - output\_intersections.svg - - An Advanced Application: Offsetting Paths - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Here we'll find the `offset - curve `__ for a few paths. - - .. code:: python - - from svgpathtools import parse_path, Line, Path, wsvg - def offset_curve(path, offset_distance, steps=1000): - """Takes in a Path object, `path`, and a distance, - `offset_distance`, and outputs an piecewise-linear approximation - of the 'parallel' offset curve.""" - nls = [] - for seg in path: - ct = 1 - for k in range(steps): - t = k / steps - offset_vector = offset_distance * seg.normal(t) - nl = Line(seg.point(t), seg.point(t) + offset_vector) - nls.append(nl) - connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)] - if path.isclosed(): - connect_the_dots.append(Line(nls[-1].end, nls[0].end)) - offset_path = Path(*connect_the_dots) - return offset_path - - # Examples: - path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ") - path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300) - path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j) - paths = [path1, path2, path3] - - offset_distances = [10*k for k in range(1,51)] - offset_paths = [] - for path in paths: - for distances in offset_distances: - offset_paths.append(offset_curve(path, distances)) - - # Note: This will take a few moments - wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg') - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/offset_curves.svg - :alt: offset\_curves.svg - - offset\_curves.svg - - Compatibility Notes for users of svg.path (v2.0) - ------------------------------------------------ - - - renamed Arc.arc attribute as Arc.large\_arc - - - Path.d() : For behavior similar\ `2 <#f2>`__\ to svg.path (v2.0), - set both useSandT and use\_closed\_attrib to be True. - - 2 The behavior would be identical, but the string formatting used in - this method has been changed to use default format (instead of the - General format, {:G}), for inceased precision. `↩ <#a2>`__ - - Licence - ------- - - This module is under a MIT License. - -Keywords: svg,svg path,svg.path,bezier,parse svg path,display svg -Platform: OS Independent -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 3 -Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based -Classifier: Topic :: Scientific/Engineering -Classifier: Topic :: Scientific/Engineering :: Image Recognition -Classifier: Topic :: Scientific/Engineering :: Information Analysis -Classifier: Topic :: Scientific/Engineering :: Mathematics -Classifier: Topic :: Scientific/Engineering :: Visualization -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Requires: numpy -Requires: svgwrite +Metadata-Version: 1.1 +Name: svgpathtools +Version: 1.3.2b0 +Summary: A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves. +Home-page: https://github.com/mathandy/svgpathtools +Author: Andy Port +Author-email: AndyAPort@gmail.com +License: MIT +Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.2beta +Description: + svgpathtools + ============ + + svgpathtools is a collection of tools for manipulating and analyzing SVG + Path objects and Bézier curves. + + Features + -------- + + svgpathtools contains functions designed to **easily read, write and + display SVG files** as well as *a large selection of + geometrically-oriented tools* to **transform and analyze path + elements**. + + Additionally, the submodule *bezier.py* contains tools for for working + with general **nth order Bezier curves stored as n-tuples**. + + Some included tools: + + - **read**, **write**, and **display** SVG files containing Path (and + other) SVG elements + - convert Bézier path segments to **numpy.poly1d** (polynomial) objects + - convert polynomials (in standard form) to their Bézier form + - compute **tangent vectors** and (right-hand rule) **normal vectors** + - compute **curvature** + - break discontinuous paths into their **continuous subpaths**. + - efficiently compute **intersections** between paths and/or segments + - find a **bounding box** for a path or segment + - **reverse** segment/path orientation + - **crop** and **split** paths and segments + - **smooth** paths (i.e. smooth away kinks to make paths + differentiable) + - **transition maps** from path domain to segment domain and back (T2t + and t2T) + - compute **area** enclosed by a closed path + - compute **arc length** + - compute **inverse arc length** + - convert RGB color tuples to hexadecimal color strings and back + + Prerequisites + ------------- + + - **numpy** + - **svgwrite** + + Setup + ----- + + If not already installed, you can **install the prerequisites** using + pip. + + .. code:: bash + + $ pip install numpy + + .. code:: bash + + $ pip install svgwrite + + Then **install svgpathtools**: + + .. code:: bash + + $ pip install svgpathtools + + Alternative Setup + ~~~~~~~~~~~~~~~~~ + + You can download the source from Github and install by using the command + (from inside the folder containing setup.py): + + .. code:: bash + + $ python setup.py install + + Credit where credit's due + ------------------------- + + Much of the core of this module was taken from `the svg.path (v2.0) + module `__. Interested svg.path + users should see the compatibility notes at bottom of this readme. + + Basic Usage + ----------- + + Classes + ~~~~~~~ + + The svgpathtools module is primarily structured around four path segment + classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``. + There is also a fifth class, ``Path``, whose objects are sequences of + (connected or disconnected\ `1 <#f1>`__\ ) path segment objects. + + - ``Line(start, end)`` + + - ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See + docstring for a detailed explanation of these parameters + + - ``QuadraticBezier(start, control, end)`` + + - ``CubicBezier(start, control1, control2, end)`` + + - ``Path(*segments)`` + + See the relevant docstrings in *path.py* or the `official SVG + specifications `__ for more + information on what each parameter means. + + 1 Warning: Some of the functionality in this library has not been tested + on discontinuous Path objects. A simple workaround is provided, however, + by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__ + + .. code:: ipython2 + + from __future__ import division, print_function + + .. code:: ipython2 + + # Coordinates are given as points in the complex plane + from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc + seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300) + seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350) + path = Path(seg1, seg2) # A path traversing the cubic and then the line + + # We could alternatively created this Path object using a d-string + from svgpathtools import parse_path + path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350') + + # Let's check that these two methods are equivalent + print(path) + print(path_alt) + print(path == path_alt) + + # On a related note, the Path.d() method returns a Path object's d-string + print(path.d()) + print(parse_path(path.d()) == path) + + + .. parsed-literal:: + + Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), + Line(start=(200+300j), end=(250+350j))) + Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), + Line(start=(200+300j), end=(250+350j))) + True + M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0 + True + + + The ``Path`` class is a mutable sequence, so it behaves much like a + list. So segments can **append**\ ed, **insert**\ ed, set by index, + **del**\ eted, **enumerate**\ d, **slice**\ d out, etc. + + .. code:: ipython2 + + # Let's append another to the end of it + path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j)) + print(path) + + # Let's replace the first segment with a Line object + path[0] = Line(200+100j, 200+300j) + print(path) + + # You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end) + print("path is continuous? ", path.iscontinuous()) + print("path is closed? ", path.isclosed()) + + # The curve the path follows is not, however, smooth (differentiable) + from svgpathtools import kinks, smoothed_path + print("path contains non-differentiable points? ", len(kinks(path)) > 0) + + # If we want, we can smooth these out (Experimental and only for line/cubic paths) + # Note: smoothing will always works (except on 180 degree turns), but you may want + # to play with the maxjointsize and tightness parameters to get pleasing results + # Note also: smoothing will increase the number of segments in a path + spath = smoothed_path(path) + print("spath contains non-differentiable points? ", len(kinks(spath)) > 0) + print(spath) + + # Let's take a quick look at the path and its smoothed relative + # The following commands will open two browser windows to display path and spaths + from svgpathtools import disvg + from time import sleep + disvg(path) + sleep(1) # needed when not giving the SVGs unique names (or not using timestamp) + disvg(spath) + print("Notice that path contains {} segments and spath contains {} segments." + "".format(len(path), len(spath))) + + + .. parsed-literal:: + + Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), + Line(start=(200+300j), end=(250+350j)), + CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) + Path(Line(start=(200+100j), end=(200+300j)), + Line(start=(200+300j), end=(250+350j)), + CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) + path is continuous? True + path is closed? True + path contains non-differentiable points? True + spath contains non-differentiable points? False + Path(Line(start=(200+101.5j), end=(200+298.5j)), + CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)), + Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)), + CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)), + CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)), + CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j))) + Notice that path contains 3 segments and spath contains 6 segments. + + + Reading SVGSs + ~~~~~~~~~~~~~ + + | The **svg2paths()** function converts an svgfile to a list of Path + objects and a separate list of dictionaries containing the attributes + of each said path. + | Note: Line, Polyline, Polygon, and Path SVG elements can all be + converted to Path objects using this function. + + .. code:: ipython2 + + # Read SVG into a list of path objects and list of dictionaries of attributes + from svgpathtools import svg2paths, wsvg + paths, attributes = svg2paths('test.svg') + + # Update: You can now also extract the svg-attributes by setting + # return_svg_attributes=True, or with the convenience function svg2paths2 + from svgpathtools import svg2paths2 + paths, attributes, svg_attributes = svg2paths2('test.svg') + + # Let's print out the first path object and the color it was in the SVG + # We'll see it is composed of two CubicBezier objects and, in the SVG file it + # came from, it was red + redpath = paths[0] + redpath_attribs = attributes[0] + print(redpath) + print(redpath_attribs['stroke']) + + + .. parsed-literal:: + + Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)), + CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j))) + red + + + Writing SVGSs (and some geometric functions and methods) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + The **wsvg()** function creates an SVG file from a list of path. This + function can do many things (see docstring in *paths2svg.py* for more + information) and is meant to be quick and easy to use. Note: Use the + convenience function **disvg()** (or set 'openinbrowser=True') to + automatically attempt to open the created svg file in your default SVG + viewer. + + .. code:: ipython2 + + # Let's make a new SVG that's identical to the first + wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg') + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output1.svg + :alt: output1.svg + + output1.svg + + There will be many more examples of writing and displaying path data + below. + + The .point() method and transitioning between path and path segment parameterizations + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + SVG Path elements and their segments have official parameterizations. + These parameterizations can be accessed using the ``Path.point()``, + ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, + and ``Arc.point()`` methods. All these parameterizations are defined + over the domain 0 <= t <= 1. + + | **Note:** In this document and in inline documentation and doctrings, + I use a capital ``T`` when referring to the parameterization of a Path + object and a lower case ``t`` when referring speaking about path + segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc + objects). + | Given a ``T`` value, the ``Path.T2t()`` method can be used to find the + corresponding segment index, ``k``, and segment parameter, ``t``, such + that ``path.point(T)=path[k].point(t)``. + | There is also a ``Path.t2T()`` method to solve the inverse problem. + + .. code:: ipython2 + + # Example: + + # Let's check that the first segment of redpath starts + # at the same point as redpath + firstseg = redpath[0] + print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start) + + # Let's check that the last segment of redpath ends on the same point as redpath + lastseg = redpath[-1] + print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end) + + # This next boolean should return False as redpath is composed multiple segments + print(redpath.point(0.5) == firstseg.point(0.5)) + + # If we want to figure out which segment of redpoint the + # point redpath.point(0.5) lands on, we can use the path.T2t() method + k, t = redpath.T2t(0.5) + print(redpath[k].point(t) == redpath.point(0.5)) + + + .. parsed-literal:: + + True + True + False + True + + + Bezier curves as NumPy polynomial objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + | Another great way to work with the parameterizations for ``Line``, + ``QuadraticBezier``, and ``CubicBezier`` objects is to convert them to + ``numpy.poly1d`` objects. This is done easily using the + ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` + methods. + | There's also a ``polynomial2bezier()`` function in the pathtools.py + submodule to convert polynomials back to Bezier curves. + + **Note:** cubic Bezier curves are parameterized as + + .. math:: \mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3 + + where :math:`P_0`, :math:`P_1`, :math:`P_2`, and :math:`P_3` are the + control points ``start``, ``control1``, ``control2``, and ``end``, + respectively, that svgpathtools uses to define a CubicBezier object. The + ``CubicBezier.poly()`` method expands this polynomial to its standard + form + + .. math:: \mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3 + + where + + .. math:: + + \begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} = + \begin{bmatrix} + -1 & 3 & -3 & 1\\ + 3 & -6 & -3 & 0\\ + -3 & 3 & 0 & 0\\ + 1 & 0 & 0 & 0\\ + \end{bmatrix} + \begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix} + + ``QuadraticBezier.poly()`` and ``Line.poly()`` are `defined + similarly `__. + + .. code:: ipython2 + + # Example: + b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) + p = b.poly() + + # p(t) == b.point(t) + print(p(0.235) == b.point(0.235)) + + # What is p(t)? It's just the cubic b written in standard form. + bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints()) + print("The CubicBezier, b.point(x) = \n\n" + + bpretty + "\n\n" + + "can be rewritten in standard form as \n\n" + + str(p).replace('x','t')) + + + .. parsed-literal:: + + True + The CubicBezier, b.point(x) = + + (300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3 + + can be rewritten in standard form as + + 3 2 + (-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j) + + + The ability to convert between Bezier objects to NumPy polynomial + objects is very useful. For starters, we can take turn a list of Bézier + segments into a NumPy array + + Numpy Array operations on Bézier path segments + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + `Example available + here `__ + + To further illustrate the power of being able to convert our Bezier + curve objects to numpy.poly1d objects and back, lets compute the unit + tangent vector of the above CubicBezier object, b, at t=0.5 in four + different ways. + + Tangent vectors (and more on NumPy polynomials) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: ipython2 + + t = 0.5 + ### Method 1: the easy way + u1 = b.unit_tangent(t) + + ### Method 2: another easy way + # Note: This way will fail if it encounters a removable singularity. + u2 = b.derivative(t)/abs(b.derivative(t)) + + ### Method 2: a third easy way + # Note: This way will also fail if it encounters a removable singularity. + dp = p.deriv() + u3 = dp(t)/abs(dp(t)) + + ### Method 4: the removable-singularity-proof numpy.poly1d way + # Note: This is roughly how Method 1 works + from svgpathtools import real, imag, rational_limit + dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy + p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2 + # Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and, + # the limit_{t->t0}[f(t) / abs(f(t))] == + # sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2]) + from cmath import sqrt + u4 = sqrt(rational_limit(dp**2, p_mag2, t)) + + print("unit tangent check:", u1 == u2 == u3 == u4) + + # Let's do a visual check + mag = b.length()/4 # so it's not hard to see the tangent line + tangent_line = Line(b.point(t), b.point(t) + mag*u1) + disvg([b, tangent_line], 'bg', nodes=[b.point(t)]) + + + .. parsed-literal:: + + unit tangent check: True + + + Translations (shifts), reversing orientation, and normal vectors + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: ipython2 + + # Speaking of tangents, let's add a normal vector to the picture + n = b.normal(t) + normal_line = Line(b.point(t), b.point(t) + mag*n) + disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)]) + + # and let's reverse the orientation of b! + # the tangent and normal lines should be sent to their opposites + br = b.reversed() + + # Let's also shift b_r over a bit to the right so we can view it next to b + # The simplest way to do this is br = br.translated(3*mag), but let's use + # the .bpoints() instead, which returns a Bezier's control points + br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] # + + tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t)) + normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t)) + wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r], + 'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg', + text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r]) + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/vectorframes.svg + :alt: vectorframes.svg + + vectorframes.svg + + Rotations and Translations + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: ipython2 + + # Let's take a Line and an Arc and make some pictures + top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1) + midline = Line(-1.5, 1.5) + + # First let's make our ellipse whole + bottom_half = top_half.rotated(180) + decorated_ellipse = Path(top_half, bottom_half) + + # Now let's add the decorations + for k in range(12): + decorated_ellipse.append(midline.rotated(30*k)) + + # Let's move it over so we can see the original Line and Arc object next + # to the final product + decorated_ellipse = decorated_ellipse.translated(4+0j) + wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg') + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/decorated_ellipse.svg + :alt: decorated\_ellipse.svg + + decorated\_ellipse.svg + + arc length and inverse arc length + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Here we'll create an SVG that shows off the parametric and geometric + midpoints of the paths from ``test.svg``. We'll need to compute use the + ``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``, + ``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the + related inverse arc length methods ``.ilength()`` function to do this. + + .. code:: ipython2 + + # First we'll load the path data from the file test.svg + paths, attributes = svg2paths('test.svg') + + # Let's mark the parametric midpoint of each segment + # I say "parametric" midpoint because Bezier curves aren't + # parameterized by arclength + # If they're also the geometric midpoint, let's mark them + # purple and otherwise we'll mark the geometric midpoint green + min_depth = 5 + error = 1e-4 + dots = [] + ncols = [] + nradii = [] + for path in paths: + for seg in path: + parametric_mid = seg.point(0.5) + seg_length = seg.length() + if seg.length(0.5)/seg.length() == 1/2: + dots += [parametric_mid] + ncols += ['purple'] + nradii += [5] + else: + t_mid = seg.ilength(seg_length/2) + geo_mid = seg.point(t_mid) + dots += [parametric_mid, geo_mid] + ncols += ['red', 'green'] + nradii += [5] * 2 + + # In 'output2.svg' the paths will retain their original attributes + wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, + attributes=attributes, filename='output2.svg') + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output2.svg + :alt: output2.svg + + output2.svg + + Intersections between Bezier curves + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: ipython2 + + # Let's find all intersections between redpath and the other + redpath = paths[0] + redpath_attribs = attributes[0] + intersections = [] + for path in paths[1:]: + for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path): + intersections.append(redpath.point(T1)) + + disvg(paths, filename='output_intersections.svg', attributes=attributes, + nodes = intersections, node_radii = [5]*len(intersections)) + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output_intersections.svg + :alt: output\_intersections.svg + + output\_intersections.svg + + An Advanced Application: Offsetting Paths + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Here we'll find the `offset + curve `__ for a few paths. + + .. code:: ipython2 + + from svgpathtools import parse_path, Line, Path, wsvg + def offset_curve(path, offset_distance, steps=1000): + """Takes in a Path object, `path`, and a distance, + `offset_distance`, and outputs an piecewise-linear approximation + of the 'parallel' offset curve.""" + nls = [] + for seg in path: + ct = 1 + for k in range(steps): + t = k / steps + offset_vector = offset_distance * seg.normal(t) + nl = Line(seg.point(t), seg.point(t) + offset_vector) + nls.append(nl) + connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)] + if path.isclosed(): + connect_the_dots.append(Line(nls[-1].end, nls[0].end)) + offset_path = Path(*connect_the_dots) + return offset_path + + # Examples: + path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ") + path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300) + path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j) + paths = [path1, path2, path3] + + offset_distances = [10*k for k in range(1,51)] + offset_paths = [] + for path in paths: + for distances in offset_distances: + offset_paths.append(offset_curve(path, distances)) + + # Note: This will take a few moments + wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg') + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/offset_curves.svg + :alt: offset\_curves.svg + + offset\_curves.svg + + Compatibility Notes for users of svg.path (v2.0) + ------------------------------------------------ + + - renamed Arc.arc attribute as Arc.large\_arc + + - Path.d() : For behavior similar\ `2 <#f2>`__\ to svg.path (v2.0), + set both useSandT and use\_closed\_attrib to be True. + + 2 The behavior would be identical, but the string formatting used in + this method has been changed to use default format (instead of the + General format, {:G}), for inceased precision. `↩ <#a2>`__ + + Licence + ------- + + This module is under a MIT License. + + +Keywords: svg,svg path,svg.path,bezier,parse svg path,display svg +Platform: OS Independent +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based +Classifier: Topic :: Scientific/Engineering +Classifier: Topic :: Scientific/Engineering :: Image Recognition +Classifier: Topic :: Scientific/Engineering :: Information Analysis +Classifier: Topic :: Scientific/Engineering :: Mathematics +Classifier: Topic :: Scientific/Engineering :: Visualization +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires: numpy +Requires: svgwrite diff --git a/svgpathtools.egg-info/SOURCES.txt b/svgpathtools.egg-info/SOURCES.txt index 99a5197..fcc0a2a 100644 --- a/svgpathtools.egg-info/SOURCES.txt +++ b/svgpathtools.egg-info/SOURCES.txt @@ -15,12 +15,10 @@ test.svg vectorframes.svg svgpathtools/__init__.py svgpathtools/bezier.py -svgpathtools/directional_field.py svgpathtools/misctools.py svgpathtools/parser.py svgpathtools/path.py svgpathtools/paths2svg.py -svgpathtools/pathtools.py svgpathtools/polytools.py svgpathtools/smoothing.py svgpathtools/svg2paths.py @@ -28,10 +26,13 @@ svgpathtools.egg-info/PKG-INFO svgpathtools.egg-info/SOURCES.txt svgpathtools.egg-info/dependency_links.txt svgpathtools.egg-info/top_level.txt +test/groups.svg +test/polygons.svg test/test.svg test/test_bezier.py test/test_generation.py test/test_parsing.py test/test_path.py -test/test_pathtools.py -test/test_polytools.py \ No newline at end of file +test/test_polytools.py +test/test_svg2paths.py +test/test_svg2paths_groups.py \ No newline at end of file diff --git a/test/groups.svg b/test/groups.svg index 2d35114..cde31d8 100644 --- a/test/groups.svg +++ b/test/groups.svg @@ -83,7 +83,7 @@ id="p14" style="stroke:#ff0000;stroke-width:2." d="m 1100.,100. 100.,0." /> - + Date: Wed, 17 May 2017 17:46:25 +0200 Subject: [PATCH 07/10] Made fork of svg2paths.py up to date. --- svgpathtools/svg2paths.py | 91 +++++++++++++++++++++++++++------------ 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index ca0a479..f37c4fe 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -12,23 +12,6 @@ from .parser import parse_path from .path import Path, bpoints2bezier -def polyline2pathd(polyline_d): - """converts the string from a polyline points-attribute to a string for a - Path object d-attribute""" - points = polyline_d.replace(', ', ',') - points = points.replace(' ,', ',') - points = points.split() - - closed = points[0] == points[-1] - - d = 'M' + points.pop(0).replace(',', ' ') - for p in points: - d += 'L' + p.replace(',', ' ') - if closed: - d += 'z' - return d - - def ellipse2pathd(ellipse): """converts the parameters from an ellipse or a circle to a string for a Path object d-attribute""" @@ -56,6 +39,23 @@ def ellipse2pathd(ellipse): return d + +def polyline2pathd(polyline_d): + """converts the string from a polyline points-attribute to a string for a + Path object d-attribute""" + points = polyline_d.replace(', ', ',') + points = points.replace(' ,', ',') + points = points.split() + + closed = points[0] == points[-1] + + d = 'M' + points.pop(0).replace(',', ' ') + for p in points: + d += 'L' + p.replace(',', ' ') + if closed: + d += 'z' + return d + def polygon2pathd(polyline_d): """converts the string from a polygon points-attribute to a string for a Path object d-attribute. @@ -81,28 +81,54 @@ def polygon2pathd(polyline_d): return d + 'z' +def rect2pathd(rect): + """Converts an SVG-rect element to a Path d-string. + + The rectangle will start at the (x,y) coordinate specified by the rectangle + object and proceed counter-clockwise.""" + x0, y0 = float(rect.get('x', 0)), float(rect.get('y', 0)) + w, h = float(rect["width"]), float(rect["height"]) + x1, y1 = x0 + w, y0 + x2, y2 = x0 + w, y0 + h + x3, y3 = x0, y0 + h + + d = ("M{} {} L {} {} L {} {} L {} {} z" + "".format(x0, y0, x1, y1, x2, y2, x3, y3)) + return d + def svg2paths(svg_file_location, return_svg_attributes=False, + convert_circles_to_paths=True, + convert_ellipses_to_paths=True, convert_lines_to_paths=True, convert_polylines_to_paths=True, convert_polygons_to_paths=True, - convert_ellipses_to_paths=True): + convert_rectangles_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, 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 + return_svg_attributes (bool): Set to True and a dictionary of + svg-attributes will be extracted and returned. See also the + `svg2paths2()` function. + convert_circles_to_paths: Set to False to exclude SVG-Circle + elements (converted to Paths). By default circles are included as + paths of two `Arc` objects. + convert_ellipses_to_paths (bool): Set to False to exclude SVG-Ellipse + elements (converted to Paths). By default ellipses are included as + paths of two `Arc` objects. + convert_lines_to_paths (bool): Set to False to exclude SVG-Line elements (converted to Paths) convert_polylines_to_paths (bool): Set to False to exclude SVG-Polyline - objects (converted to Paths) + elements (converted to Paths) convert_polygons_to_paths (bool): Set to False to exclude SVG-Polygon - objects (converted to Paths) + elements (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. + convert_rectangles_to_paths (bool): Set to False to exclude SVG-Rect + elements (converted to Paths). Returns: list: The list of Path objects. list: The list of corresponding path attribute dictionaries. @@ -238,10 +264,17 @@ def svg2paths(svg_file_location, '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' or node.nodeName == 'circle'): + 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 @@ -258,16 +291,20 @@ def svg2paths(svg_file_location, def svg2paths2(svg_file_location, return_svg_attributes=True, + convert_circles_to_paths=True, + convert_ellipses_to_paths=True, convert_lines_to_paths=True, convert_polylines_to_paths=True, convert_polygons_to_paths=True, - convert_ellipses_to_paths=True): + convert_rectangles_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_circles_to_paths=convert_circles_to_paths, + convert_ellipses_to_paths=convert_ellipses_to_paths, convert_lines_to_paths=convert_lines_to_paths, convert_polylines_to_paths=convert_polylines_to_paths, convert_polygons_to_paths=convert_polygons_to_paths, - convert_ellipses_to_paths=convert_ellipses_to_paths) + convert_rectangles_to_paths=convert_rectangles_to_paths) From fd3c0091b8e34cdca7355cf52b482a44e7d8c1dd Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Wed, 12 Jul 2017 18:54:50 +0200 Subject: [PATCH 08/10] Updated according to mathandys newest version --- svgpathtools/svg2paths.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py index f37c4fe..75f16df 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -39,7 +39,6 @@ def ellipse2pathd(ellipse): return d - def polyline2pathd(polyline_d): """converts the string from a polyline points-attribute to a string for a Path object d-attribute""" @@ -56,6 +55,7 @@ def polyline2pathd(polyline_d): d += 'z' return d + def polygon2pathd(polyline_d): """converts the string from a polygon points-attribute to a string for a Path object d-attribute. @@ -96,6 +96,7 @@ def rect2pathd(rect): "".format(x0, y0, x1, y1, x2, y2, x3, y3)) return d + def svg2paths(svg_file_location, return_svg_attributes=False, convert_circles_to_paths=True, @@ -104,13 +105,15 @@ 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 SVG Path, Line, Polyline, Polygon, Circle, and Ellipse elements. + Args: svg_file_location (string): the location of the svg file - return_svg_attributes (bool): Set to True and a dictionary of + return_svg_attributes (bool): Set to True and a dictionary of svg-attributes will be extracted and returned. See also the `svg2paths2()` function. convert_circles_to_paths: Set to False to exclude SVG-Circle @@ -125,10 +128,9 @@ def svg2paths(svg_file_location, elements (converted to Paths) convert_polygons_to_paths (bool): Set to False to exclude SVG-Polygon elements (converted to Paths) - return_svg_attributes (bool): Set to True and a dictionary of - svg-attributes will be extracted and returned convert_rectangles_to_paths (bool): Set to False to exclude SVG-Rect elements (converted to Paths). + Returns: list: The list of Path objects. list: The list of corresponding path attribute dictionaries. From a4b0c7e22904273fee95b545b704f0e177716482 Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Wed, 12 Jul 2017 19:00:38 +0200 Subject: [PATCH 09/10] Changed other files back to mathandys master release. --- build/lib/svgpathtools/svg2paths.py | 168 +--- svgpathtools.egg-info/PKG-INFO | 1320 +++++++++++++-------------- svgpathtools.egg-info/SOURCES.txt | 9 +- svgpathtools/path.py | 15 + 4 files changed, 712 insertions(+), 800 deletions(-) diff --git a/build/lib/svgpathtools/svg2paths.py b/build/lib/svgpathtools/svg2paths.py index fc3a42b..f1ecbea 100644 --- a/build/lib/svgpathtools/svg2paths.py +++ b/build/lib/svgpathtools/svg2paths.py @@ -5,21 +5,23 @@ 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 +from shutil import copyfile # Internal dependencies from .parser import parse_path -from .path import Path, bpoints2bezier def polyline2pathd(polyline_d): - """converts the string from a polyline points-attribute to a string for a - Path object d-attribute""" + """converts the string from a polyline d-attribute to a string for a Path + object d-attribute""" points = polyline_d.replace(', ', ',') points = points.replace(' ,', ',') points = points.split() - closed = points[0] == points[-1] + if points[0] == points[-1]: + closed = True + else: + closed = False d = 'M' + points.pop(0).replace(',', ' ') for p in points: @@ -29,30 +31,6 @@ def polyline2pathd(polyline_d): return d -def polygon2pathd(polyline_d): - """converts the string from a polygon points-attribute to a string for a - Path object d-attribute. - Note: For a polygon made from n points, the resulting path will be - composed of n lines (even if some of these lines have length zero).""" - points = polyline_d.replace(', ', ',') - points = points.replace(' ,', ',') - points = points.split() - - reduntantly_closed = points[0] == points[-1] - - 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. - if reduntantly_closed: - d += 'L' + points[0].replace(',', ' ') - - return d + 'z' - - def svg2paths(svg_file_location, convert_lines_to_paths=True, convert_polylines_to_paths=True, @@ -84,126 +62,52 @@ def svg2paths(svg_file_location, # 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))) - 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 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 + # if pathless_svg: + # for el in doc.getElementsByTagName('path'): + # el.parentNode.removeChild(el) - 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 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 - trafo_list = list(trafo_matrix.reshape((9,))[:6]) - return trafo_list[::3]+trafo_list[1::3]+trafo_list[2::3] + # 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 += [polyline2pathd(pg['points']) + 'z' for pg in pgons] + attribute_dictionary_list += pgons - 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.]) + 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 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 - else: - return ret_list, attribute_dictionary_list_int + # if pathless_svg: + # with open(pathless_svg, "wb") as f: + # doc.writexml(f) - path_list, attribute_dictionary_list = parse_node(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 diff --git a/svgpathtools.egg-info/PKG-INFO b/svgpathtools.egg-info/PKG-INFO index 85717c6..edc988c 100644 --- a/svgpathtools.egg-info/PKG-INFO +++ b/svgpathtools.egg-info/PKG-INFO @@ -1,663 +1,657 @@ -Metadata-Version: 1.1 -Name: svgpathtools -Version: 1.3.2b0 -Summary: A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves. -Home-page: https://github.com/mathandy/svgpathtools -Author: Andy Port -Author-email: AndyAPort@gmail.com -License: MIT -Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.2beta -Description: - svgpathtools - ============ - - svgpathtools is a collection of tools for manipulating and analyzing SVG - Path objects and Bézier curves. - - Features - -------- - - svgpathtools contains functions designed to **easily read, write and - display SVG files** as well as *a large selection of - geometrically-oriented tools* to **transform and analyze path - elements**. - - Additionally, the submodule *bezier.py* contains tools for for working - with general **nth order Bezier curves stored as n-tuples**. - - Some included tools: - - - **read**, **write**, and **display** SVG files containing Path (and - other) SVG elements - - convert Bézier path segments to **numpy.poly1d** (polynomial) objects - - convert polynomials (in standard form) to their Bézier form - - compute **tangent vectors** and (right-hand rule) **normal vectors** - - compute **curvature** - - break discontinuous paths into their **continuous subpaths**. - - efficiently compute **intersections** between paths and/or segments - - find a **bounding box** for a path or segment - - **reverse** segment/path orientation - - **crop** and **split** paths and segments - - **smooth** paths (i.e. smooth away kinks to make paths - differentiable) - - **transition maps** from path domain to segment domain and back (T2t - and t2T) - - compute **area** enclosed by a closed path - - compute **arc length** - - compute **inverse arc length** - - convert RGB color tuples to hexadecimal color strings and back - - Prerequisites - ------------- - - - **numpy** - - **svgwrite** - - Setup - ----- - - If not already installed, you can **install the prerequisites** using - pip. - - .. code:: bash - - $ pip install numpy - - .. code:: bash - - $ pip install svgwrite - - Then **install svgpathtools**: - - .. code:: bash - - $ pip install svgpathtools - - Alternative Setup - ~~~~~~~~~~~~~~~~~ - - You can download the source from Github and install by using the command - (from inside the folder containing setup.py): - - .. code:: bash - - $ python setup.py install - - Credit where credit's due - ------------------------- - - Much of the core of this module was taken from `the svg.path (v2.0) - module `__. Interested svg.path - users should see the compatibility notes at bottom of this readme. - - Basic Usage - ----------- - - Classes - ~~~~~~~ - - The svgpathtools module is primarily structured around four path segment - classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``. - There is also a fifth class, ``Path``, whose objects are sequences of - (connected or disconnected\ `1 <#f1>`__\ ) path segment objects. - - - ``Line(start, end)`` - - - ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See - docstring for a detailed explanation of these parameters - - - ``QuadraticBezier(start, control, end)`` - - - ``CubicBezier(start, control1, control2, end)`` - - - ``Path(*segments)`` - - See the relevant docstrings in *path.py* or the `official SVG - specifications `__ for more - information on what each parameter means. - - 1 Warning: Some of the functionality in this library has not been tested - on discontinuous Path objects. A simple workaround is provided, however, - by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__ - - .. code:: ipython2 - - from __future__ import division, print_function - - .. code:: ipython2 - - # Coordinates are given as points in the complex plane - from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc - seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300) - seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350) - path = Path(seg1, seg2) # A path traversing the cubic and then the line - - # We could alternatively created this Path object using a d-string - from svgpathtools import parse_path - path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350') - - # Let's check that these two methods are equivalent - print(path) - print(path_alt) - print(path == path_alt) - - # On a related note, the Path.d() method returns a Path object's d-string - print(path.d()) - print(parse_path(path.d()) == path) - - - .. parsed-literal:: - - Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), - Line(start=(200+300j), end=(250+350j))) - Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), - Line(start=(200+300j), end=(250+350j))) - True - M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0 - True - - - The ``Path`` class is a mutable sequence, so it behaves much like a - list. So segments can **append**\ ed, **insert**\ ed, set by index, - **del**\ eted, **enumerate**\ d, **slice**\ d out, etc. - - .. code:: ipython2 - - # Let's append another to the end of it - path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j)) - print(path) - - # Let's replace the first segment with a Line object - path[0] = Line(200+100j, 200+300j) - print(path) - - # You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end) - print("path is continuous? ", path.iscontinuous()) - print("path is closed? ", path.isclosed()) - - # The curve the path follows is not, however, smooth (differentiable) - from svgpathtools import kinks, smoothed_path - print("path contains non-differentiable points? ", len(kinks(path)) > 0) - - # If we want, we can smooth these out (Experimental and only for line/cubic paths) - # Note: smoothing will always works (except on 180 degree turns), but you may want - # to play with the maxjointsize and tightness parameters to get pleasing results - # Note also: smoothing will increase the number of segments in a path - spath = smoothed_path(path) - print("spath contains non-differentiable points? ", len(kinks(spath)) > 0) - print(spath) - - # Let's take a quick look at the path and its smoothed relative - # The following commands will open two browser windows to display path and spaths - from svgpathtools import disvg - from time import sleep - disvg(path) - sleep(1) # needed when not giving the SVGs unique names (or not using timestamp) - disvg(spath) - print("Notice that path contains {} segments and spath contains {} segments." - "".format(len(path), len(spath))) - - - .. parsed-literal:: - - Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), - Line(start=(200+300j), end=(250+350j)), - CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) - Path(Line(start=(200+100j), end=(200+300j)), - Line(start=(200+300j), end=(250+350j)), - CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) - path is continuous? True - path is closed? True - path contains non-differentiable points? True - spath contains non-differentiable points? False - Path(Line(start=(200+101.5j), end=(200+298.5j)), - CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)), - Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)), - CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)), - CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)), - CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j))) - Notice that path contains 3 segments and spath contains 6 segments. - - - Reading SVGSs - ~~~~~~~~~~~~~ - - | The **svg2paths()** function converts an svgfile to a list of Path - objects and a separate list of dictionaries containing the attributes - of each said path. - | Note: Line, Polyline, Polygon, and Path SVG elements can all be - converted to Path objects using this function. - - .. code:: ipython2 - - # Read SVG into a list of path objects and list of dictionaries of attributes - from svgpathtools import svg2paths, wsvg - paths, attributes = svg2paths('test.svg') - - # Update: You can now also extract the svg-attributes by setting - # return_svg_attributes=True, or with the convenience function svg2paths2 - from svgpathtools import svg2paths2 - paths, attributes, svg_attributes = svg2paths2('test.svg') - - # Let's print out the first path object and the color it was in the SVG - # We'll see it is composed of two CubicBezier objects and, in the SVG file it - # came from, it was red - redpath = paths[0] - redpath_attribs = attributes[0] - print(redpath) - print(redpath_attribs['stroke']) - - - .. parsed-literal:: - - Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)), - CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j))) - red - - - Writing SVGSs (and some geometric functions and methods) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - The **wsvg()** function creates an SVG file from a list of path. This - function can do many things (see docstring in *paths2svg.py* for more - information) and is meant to be quick and easy to use. Note: Use the - convenience function **disvg()** (or set 'openinbrowser=True') to - automatically attempt to open the created svg file in your default SVG - viewer. - - .. code:: ipython2 - - # Let's make a new SVG that's identical to the first - wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg') - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output1.svg - :alt: output1.svg - - output1.svg - - There will be many more examples of writing and displaying path data - below. - - The .point() method and transitioning between path and path segment parameterizations - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - SVG Path elements and their segments have official parameterizations. - These parameterizations can be accessed using the ``Path.point()``, - ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, - and ``Arc.point()`` methods. All these parameterizations are defined - over the domain 0 <= t <= 1. - - | **Note:** In this document and in inline documentation and doctrings, - I use a capital ``T`` when referring to the parameterization of a Path - object and a lower case ``t`` when referring speaking about path - segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc - objects). - | Given a ``T`` value, the ``Path.T2t()`` method can be used to find the - corresponding segment index, ``k``, and segment parameter, ``t``, such - that ``path.point(T)=path[k].point(t)``. - | There is also a ``Path.t2T()`` method to solve the inverse problem. - - .. code:: ipython2 - - # Example: - - # Let's check that the first segment of redpath starts - # at the same point as redpath - firstseg = redpath[0] - print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start) - - # Let's check that the last segment of redpath ends on the same point as redpath - lastseg = redpath[-1] - print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end) - - # This next boolean should return False as redpath is composed multiple segments - print(redpath.point(0.5) == firstseg.point(0.5)) - - # If we want to figure out which segment of redpoint the - # point redpath.point(0.5) lands on, we can use the path.T2t() method - k, t = redpath.T2t(0.5) - print(redpath[k].point(t) == redpath.point(0.5)) - - - .. parsed-literal:: - - True - True - False - True - - - Bezier curves as NumPy polynomial objects - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - | Another great way to work with the parameterizations for ``Line``, - ``QuadraticBezier``, and ``CubicBezier`` objects is to convert them to - ``numpy.poly1d`` objects. This is done easily using the - ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` - methods. - | There's also a ``polynomial2bezier()`` function in the pathtools.py - submodule to convert polynomials back to Bezier curves. - - **Note:** cubic Bezier curves are parameterized as - - .. math:: \mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3 - - where :math:`P_0`, :math:`P_1`, :math:`P_2`, and :math:`P_3` are the - control points ``start``, ``control1``, ``control2``, and ``end``, - respectively, that svgpathtools uses to define a CubicBezier object. The - ``CubicBezier.poly()`` method expands this polynomial to its standard - form - - .. math:: \mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3 - - where - - .. math:: - - \begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} = - \begin{bmatrix} - -1 & 3 & -3 & 1\\ - 3 & -6 & -3 & 0\\ - -3 & 3 & 0 & 0\\ - 1 & 0 & 0 & 0\\ - \end{bmatrix} - \begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix} - - ``QuadraticBezier.poly()`` and ``Line.poly()`` are `defined - similarly `__. - - .. code:: ipython2 - - # Example: - b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) - p = b.poly() - - # p(t) == b.point(t) - print(p(0.235) == b.point(0.235)) - - # What is p(t)? It's just the cubic b written in standard form. - bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints()) - print("The CubicBezier, b.point(x) = \n\n" + - bpretty + "\n\n" + - "can be rewritten in standard form as \n\n" + - str(p).replace('x','t')) - - - .. parsed-literal:: - - True - The CubicBezier, b.point(x) = - - (300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3 - - can be rewritten in standard form as - - 3 2 - (-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j) - - - The ability to convert between Bezier objects to NumPy polynomial - objects is very useful. For starters, we can take turn a list of Bézier - segments into a NumPy array - - Numpy Array operations on Bézier path segments - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - `Example available - here `__ - - To further illustrate the power of being able to convert our Bezier - curve objects to numpy.poly1d objects and back, lets compute the unit - tangent vector of the above CubicBezier object, b, at t=0.5 in four - different ways. - - Tangent vectors (and more on NumPy polynomials) - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - .. code:: ipython2 - - t = 0.5 - ### Method 1: the easy way - u1 = b.unit_tangent(t) - - ### Method 2: another easy way - # Note: This way will fail if it encounters a removable singularity. - u2 = b.derivative(t)/abs(b.derivative(t)) - - ### Method 2: a third easy way - # Note: This way will also fail if it encounters a removable singularity. - dp = p.deriv() - u3 = dp(t)/abs(dp(t)) - - ### Method 4: the removable-singularity-proof numpy.poly1d way - # Note: This is roughly how Method 1 works - from svgpathtools import real, imag, rational_limit - dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy - p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2 - # Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and, - # the limit_{t->t0}[f(t) / abs(f(t))] == - # sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2]) - from cmath import sqrt - u4 = sqrt(rational_limit(dp**2, p_mag2, t)) - - print("unit tangent check:", u1 == u2 == u3 == u4) - - # Let's do a visual check - mag = b.length()/4 # so it's not hard to see the tangent line - tangent_line = Line(b.point(t), b.point(t) + mag*u1) - disvg([b, tangent_line], 'bg', nodes=[b.point(t)]) - - - .. parsed-literal:: - - unit tangent check: True - - - Translations (shifts), reversing orientation, and normal vectors - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - .. code:: ipython2 - - # Speaking of tangents, let's add a normal vector to the picture - n = b.normal(t) - normal_line = Line(b.point(t), b.point(t) + mag*n) - disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)]) - - # and let's reverse the orientation of b! - # the tangent and normal lines should be sent to their opposites - br = b.reversed() - - # Let's also shift b_r over a bit to the right so we can view it next to b - # The simplest way to do this is br = br.translated(3*mag), but let's use - # the .bpoints() instead, which returns a Bezier's control points - br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] # - - tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t)) - normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t)) - wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r], - 'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg', - text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r]) - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/vectorframes.svg - :alt: vectorframes.svg - - vectorframes.svg - - Rotations and Translations - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - .. code:: ipython2 - - # Let's take a Line and an Arc and make some pictures - top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1) - midline = Line(-1.5, 1.5) - - # First let's make our ellipse whole - bottom_half = top_half.rotated(180) - decorated_ellipse = Path(top_half, bottom_half) - - # Now let's add the decorations - for k in range(12): - decorated_ellipse.append(midline.rotated(30*k)) - - # Let's move it over so we can see the original Line and Arc object next - # to the final product - decorated_ellipse = decorated_ellipse.translated(4+0j) - wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg') - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/decorated_ellipse.svg - :alt: decorated\_ellipse.svg - - decorated\_ellipse.svg - - arc length and inverse arc length - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Here we'll create an SVG that shows off the parametric and geometric - midpoints of the paths from ``test.svg``. We'll need to compute use the - ``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``, - ``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the - related inverse arc length methods ``.ilength()`` function to do this. - - .. code:: ipython2 - - # First we'll load the path data from the file test.svg - paths, attributes = svg2paths('test.svg') - - # Let's mark the parametric midpoint of each segment - # I say "parametric" midpoint because Bezier curves aren't - # parameterized by arclength - # If they're also the geometric midpoint, let's mark them - # purple and otherwise we'll mark the geometric midpoint green - min_depth = 5 - error = 1e-4 - dots = [] - ncols = [] - nradii = [] - for path in paths: - for seg in path: - parametric_mid = seg.point(0.5) - seg_length = seg.length() - if seg.length(0.5)/seg.length() == 1/2: - dots += [parametric_mid] - ncols += ['purple'] - nradii += [5] - else: - t_mid = seg.ilength(seg_length/2) - geo_mid = seg.point(t_mid) - dots += [parametric_mid, geo_mid] - ncols += ['red', 'green'] - nradii += [5] * 2 - - # In 'output2.svg' the paths will retain their original attributes - wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, - attributes=attributes, filename='output2.svg') - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output2.svg - :alt: output2.svg - - output2.svg - - Intersections between Bezier curves - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - .. code:: ipython2 - - # Let's find all intersections between redpath and the other - redpath = paths[0] - redpath_attribs = attributes[0] - intersections = [] - for path in paths[1:]: - for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path): - intersections.append(redpath.point(T1)) - - disvg(paths, filename='output_intersections.svg', attributes=attributes, - nodes = intersections, node_radii = [5]*len(intersections)) - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output_intersections.svg - :alt: output\_intersections.svg - - output\_intersections.svg - - An Advanced Application: Offsetting Paths - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Here we'll find the `offset - curve `__ for a few paths. - - .. code:: ipython2 - - from svgpathtools import parse_path, Line, Path, wsvg - def offset_curve(path, offset_distance, steps=1000): - """Takes in a Path object, `path`, and a distance, - `offset_distance`, and outputs an piecewise-linear approximation - of the 'parallel' offset curve.""" - nls = [] - for seg in path: - ct = 1 - for k in range(steps): - t = k / steps - offset_vector = offset_distance * seg.normal(t) - nl = Line(seg.point(t), seg.point(t) + offset_vector) - nls.append(nl) - connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)] - if path.isclosed(): - connect_the_dots.append(Line(nls[-1].end, nls[0].end)) - offset_path = Path(*connect_the_dots) - return offset_path - - # Examples: - path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ") - path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300) - path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j) - paths = [path1, path2, path3] - - offset_distances = [10*k for k in range(1,51)] - offset_paths = [] - for path in paths: - for distances in offset_distances: - offset_paths.append(offset_curve(path, distances)) - - # Note: This will take a few moments - wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg') - - .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/offset_curves.svg - :alt: offset\_curves.svg - - offset\_curves.svg - - Compatibility Notes for users of svg.path (v2.0) - ------------------------------------------------ - - - renamed Arc.arc attribute as Arc.large\_arc - - - Path.d() : For behavior similar\ `2 <#f2>`__\ to svg.path (v2.0), - set both useSandT and use\_closed\_attrib to be True. - - 2 The behavior would be identical, but the string formatting used in - this method has been changed to use default format (instead of the - General format, {:G}), for inceased precision. `↩ <#a2>`__ - - Licence - ------- - - This module is under a MIT License. - - -Keywords: svg,svg path,svg.path,bezier,parse svg path,display svg -Platform: OS Independent -Classifier: Development Status :: 4 - Beta -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: MIT License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python :: 2 -Classifier: Programming Language :: Python :: 3 -Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based -Classifier: Topic :: Scientific/Engineering -Classifier: Topic :: Scientific/Engineering :: Image Recognition -Classifier: Topic :: Scientific/Engineering :: Information Analysis -Classifier: Topic :: Scientific/Engineering :: Mathematics -Classifier: Topic :: Scientific/Engineering :: Visualization -Classifier: Topic :: Software Development :: Libraries :: Python Modules -Requires: numpy -Requires: svgwrite +Metadata-Version: 1.1 +Name: svgpathtools +Version: 1.3.1 +Summary: A collection of tools for manipulating and analyzing SVG Path objects and Bezier curves. +Home-page: https://github.com/mathandy/svgpathtools +Author: Andy Port +Author-email: AndyAPort@gmail.com +License: MIT +Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.1 +Description: svgpathtools + ============ + + svgpathtools is a collection of tools for manipulating and analyzing SVG + Path objects and Bézier curves. + + Features + -------- + + svgpathtools contains functions designed to **easily read, write and + display SVG files** as well as *a large selection of + geometrically-oriented tools* to **transform and analyze path + elements**. + + Additionally, the submodule *bezier.py* contains tools for for working + with general **nth order Bezier curves stored as n-tuples**. + + Some included tools: + + - **read**, **write**, and **display** SVG files containing Path (and + other) SVG elements + - convert Bézier path segments to **numpy.poly1d** (polynomial) objects + - convert polynomials (in standard form) to their Bézier form + - compute **tangent vectors** and (right-hand rule) **normal vectors** + - compute **curvature** + - break discontinuous paths into their **continuous subpaths**. + - efficiently compute **intersections** between paths and/or segments + - find a **bounding box** for a path or segment + - **reverse** segment/path orientation + - **crop** and **split** paths and segments + - **smooth** paths (i.e. smooth away kinks to make paths + differentiable) + - **transition maps** from path domain to segment domain and back (T2t + and t2T) + - compute **area** enclosed by a closed path + - compute **arc length** + - compute **inverse arc length** + - convert RGB color tuples to hexadecimal color strings and back + + Note on Python 3 + ---------------- + While I am hopeful that this package entirely works with Python 3, it was born from a larger project coded in Python 2 and has not been thoroughly tested in + Python 3. Please let me know if you find any incompatibilities. + + Prerequisites + ------------- + + - **numpy** + - **svgwrite** + + Setup + ----- + + If not already installed, you can **install the prerequisites** using + pip. + + .. code:: bash + + $ pip install numpy + + .. code:: bash + + $ pip install svgwrite + + Then **install svgpathtools**: + + .. code:: bash + + $ pip install svgpathtools + + Alternative Setup + ~~~~~~~~~~~~~~~~~ + + You can download the source from Github and install by using the command + (from inside the folder containing setup.py): + + .. code:: bash + + $ python setup.py install + + Credit where credit's due + ------------------------- + + Much of the core of this module was taken from `the svg.path (v2.0) + module `__. Interested svg.path + users should see the compatibility notes at bottom of this readme. + + Also, a big thanks to the author(s) of `A Primer on Bézier Curves `_, an outstanding resource for learning about Bézier curves and Bézier curve-related algorithms. + + Basic Usage + ----------- + + Classes + ~~~~~~~ + + The svgpathtools module is primarily structured around four path segment + classes: ``Line``, ``QuadraticBezier``, ``CubicBezier``, and ``Arc``. + There is also a fifth class, ``Path``, whose objects are sequences of + (connected or disconnected\ `1 <#f1>`__\ ) path segment objects. + + - ``Line(start, end)`` + + - ``Arc(start, radius, rotation, large_arc, sweep, end)`` Note: See + docstring for a detailed explanation of these parameters + + - ``QuadraticBezier(start, control, end)`` + + - ``CubicBezier(start, control1, control2, end)`` + + - ``Path(*segments)`` + + See the relevant docstrings in *path.py* or the `official SVG + specifications `__ for more + information on what each parameter means. + + 1 Warning: Some of the functionality in this library has not been tested + on discontinuous Path objects. A simple workaround is provided, however, + by the ``Path.continuous_subpaths()`` method. `↩ <#a1>`__ + + .. code:: python + + from __future__ import division, print_function + + .. code:: python + + # Coordinates are given as points in the complex plane + from svgpathtools import Path, Line, QuadraticBezier, CubicBezier, Arc + seg1 = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) # A cubic beginning at (300, 100) and ending at (200, 300) + seg2 = Line(200+300j, 250+350j) # A line beginning at (200, 300) and ending at (250, 350) + path = Path(seg1, seg2) # A path traversing the cubic and then the line + + # We could alternatively created this Path object using a d-string + from svgpathtools import parse_path + path_alt = parse_path('M 300 100 C 100 100 200 200 200 300 L 250 350') + + # Let's check that these two methods are equivalent + print(path) + print(path_alt) + print(path == path_alt) + + # On a related note, the Path.d() method returns a Path object's d-string + print(path.d()) + print(parse_path(path.d()) == path) + + + .. parsed-literal:: + + Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), + Line(start=(200+300j), end=(250+350j))) + Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), + Line(start=(200+300j), end=(250+350j))) + True + M 300.0,100.0 C 100.0,100.0 200.0,200.0 200.0,300.0 L 250.0,350.0 + True + + + The ``Path`` class is a mutable sequence, so it behaves much like a + list. So segments can **append**\ ed, **insert**\ ed, set by index, + **del**\ eted, **enumerate**\ d, **slice**\ d out, etc. + + .. code:: python + + # Let's append another to the end of it + path.append(CubicBezier(250+350j, 275+350j, 250+225j, 200+100j)) + print(path) + + # Let's replace the first segment with a Line object + path[0] = Line(200+100j, 200+300j) + print(path) + + # You may have noticed that this path is connected and now is also closed (i.e. path.start == path.end) + print("path is continuous? ", path.iscontinuous()) + print("path is closed? ", path.isclosed()) + + # The curve the path follows is not, however, smooth (differentiable) + from svgpathtools import kinks, smoothed_path + print("path contains non-differentiable points? ", len(kinks(path)) > 0) + + # If we want, we can smooth these out (Experimental and only for line/cubic paths) + # Note: smoothing will always works (except on 180 degree turns), but you may want + # to play with the maxjointsize and tightness parameters to get pleasing results + # Note also: smoothing will increase the number of segments in a path + spath = smoothed_path(path) + print("spath contains non-differentiable points? ", len(kinks(spath)) > 0) + print(spath) + + # Let's take a quick look at the path and its smoothed relative + # The following commands will open two browser windows to display path and spaths + from svgpathtools import disvg + from time import sleep + disvg(path) + sleep(1) # needed when not giving the SVGs unique names (or not using timestamp) + disvg(spath) + print("Notice that path contains {} segments and spath contains {} segments." + "".format(len(path), len(spath))) + + + .. parsed-literal:: + + Path(CubicBezier(start=(300+100j), control1=(100+100j), control2=(200+200j), end=(200+300j)), + Line(start=(200+300j), end=(250+350j)), + CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) + Path(Line(start=(200+100j), end=(200+300j)), + Line(start=(200+300j), end=(250+350j)), + CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j))) + path is continuous? True + path is closed? True + path contains non-differentiable points? True + spath contains non-differentiable points? False + Path(Line(start=(200+101.5j), end=(200+298.5j)), + CubicBezier(start=(200+298.5j), control1=(200+298.505j), control2=(201.057124638+301.057124638j), end=(201.060660172+301.060660172j)), + Line(start=(201.060660172+301.060660172j), end=(248.939339828+348.939339828j)), + CubicBezier(start=(248.939339828+348.939339828j), control1=(249.649982143+349.649982143j), control2=(248.995+350j), end=(250+350j)), + CubicBezier(start=(250+350j), control1=(275+350j), control2=(250+225j), end=(200+100j)), + CubicBezier(start=(200+100j), control1=(199.62675237+99.0668809257j), control2=(200+100.495j), end=(200+101.5j))) + Notice that path contains 3 segments and spath contains 6 segments. + + + Reading SVGSs + ~~~~~~~~~~~~~ + + | The **svg2paths()** function converts an svgfile to a list of Path + objects and a separate list of dictionaries containing the attributes + of each said path. + | Note: Line, Polyline, Polygon, and Path SVG elements can all be + converted to Path objects using this function. + + .. code:: python + + # Read SVG into a list of path objects and list of dictionaries of attributes + from svgpathtools import svg2paths, wsvg + paths, attributes = svg2paths('test.svg') + + # Update: You can now also extract the svg-attributes by setting + # return_svg_attributes=True, or with the convenience function svg2paths2 + from svgpathtools import svg2paths2 + paths, attributes, svg_attributes = svg2paths2('test.svg') + + # Let's print out the first path object and the color it was in the SVG + # We'll see it is composed of two CubicBezier objects and, in the SVG file it + # came from, it was red + redpath = paths[0] + redpath_attribs = attributes[0] + print(redpath) + print(redpath_attribs['stroke']) + + + .. parsed-literal:: + + Path(CubicBezier(start=(10.5+80j), control1=(40+10j), control2=(65+10j), end=(95+80j)), + CubicBezier(start=(95+80j), control1=(125+150j), control2=(150+150j), end=(180+80j))) + red + + + Writing SVGSs (and some geometric functions and methods) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + The **wsvg()** function creates an SVG file from a list of path. This + function can do many things (see docstring in *paths2svg.py* for more + information) and is meant to be quick and easy to use. Note: Use the + convenience function **disvg()** (or set 'openinbrowser=True') to + automatically attempt to open the created svg file in your default SVG + viewer. + + .. code:: python + + # Let's make a new SVG that's identical to the first + wsvg(paths, attributes=attributes, svg_attributes=svg_attributes, filename='output1.svg') + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output1.svg + :alt: output1.svg + + output1.svg + + There will be many more examples of writing and displaying path data + below. + + The .point() method and transitioning between path and path segment parameterizations + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + SVG Path elements and their segments have official parameterizations. + These parameterizations can be accessed using the ``Path.point()``, + ``Line.point()``, ``QuadraticBezier.point()``, ``CubicBezier.point()``, + and ``Arc.point()`` methods. All these parameterizations are defined + over the domain 0 <= t <= 1. + + | **Note:** In this document and in inline documentation and doctrings, + I use a capital ``T`` when referring to the parameterization of a Path + object and a lower case ``t`` when referring speaking about path + segment objects (i.e. Line, QaudraticBezier, CubicBezier, and Arc + objects). + | Given a ``T`` value, the ``Path.T2t()`` method can be used to find the + corresponding segment index, ``k``, and segment parameter, ``t``, such + that ``path.point(T)=path[k].point(t)``. + | There is also a ``Path.t2T()`` method to solve the inverse problem. + + .. code:: python + + # Example: + + # Let's check that the first segment of redpath starts + # at the same point as redpath + firstseg = redpath[0] + print(redpath.point(0) == firstseg.point(0) == redpath.start == firstseg.start) + + # Let's check that the last segment of redpath ends on the same point as redpath + lastseg = redpath[-1] + print(redpath.point(1) == lastseg.point(1) == redpath.end == lastseg.end) + + # This next boolean should return False as redpath is composed multiple segments + print(redpath.point(0.5) == firstseg.point(0.5)) + + # If we want to figure out which segment of redpoint the + # point redpath.point(0.5) lands on, we can use the path.T2t() method + k, t = redpath.T2t(0.5) + print(redpath[k].point(t) == redpath.point(0.5)) + + + .. parsed-literal:: + + True + True + False + True + + + Tangent vectors and Bezier curves as numpy polynomial objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + | Another great way to work with the parameterizations for Line, + QuadraticBezier, and CubicBezier objects is to convert them to + ``numpy.poly1d`` objects. This is done easily using the + ``Line.poly()``, ``QuadraticBezier.poly()`` and ``CubicBezier.poly()`` + methods. + | There's also a ``polynomial2bezier()`` function in the pathtools.py + submodule to convert polynomials back to Bezier curves. + + **Note:** cubic Bezier curves are parameterized as + + .. math:: \mathcal{B}(t) = P_0(1-t)^3 + 3P_1(1-t)^2t + 3P_2(1-t)t^2 + P_3t^3 + + where :math:`P_0`, :math:`P_1`, :math:`P_2`, and :math:`P_3` are the + control points ``start``, ``control1``, ``control2``, and ``end``, + respectively, that svgpathtools uses to define a CubicBezier object. The + ``CubicBezier.poly()`` method expands this polynomial to its standard + form + + .. math:: \mathcal{B}(t) = c_0t^3 + c_1t^2 +c_2t+c3 + + where + + .. math:: + + \begin{bmatrix}c_0\\c_1\\c_2\\c_3\end{bmatrix} = + \begin{bmatrix} + -1 & 3 & -3 & 1\\ + 3 & -6 & -3 & 0\\ + -3 & 3 & 0 & 0\\ + 1 & 0 & 0 & 0\\ + \end{bmatrix} + \begin{bmatrix}P_0\\P_1\\P_2\\P_3\end{bmatrix} + + QuadraticBezier.poly() and Line.poly() are defined similarly. + + .. code:: python + + # Example: + b = CubicBezier(300+100j, 100+100j, 200+200j, 200+300j) + p = b.poly() + + # p(t) == b.point(t) + print(p(0.235) == b.point(0.235)) + + # What is p(t)? It's just the cubic b written in standard form. + bpretty = "{}*(1-t)^3 + 3*{}*(1-t)^2*t + 3*{}*(1-t)*t^2 + {}*t^3".format(*b.bpoints()) + print("The CubicBezier, b.point(x) = \n\n" + + bpretty + "\n\n" + + "can be rewritten in standard form as \n\n" + + str(p).replace('x','t')) + + + .. parsed-literal:: + + True + The CubicBezier, b.point(x) = + + (300+100j)*(1-t)^3 + 3*(100+100j)*(1-t)^2*t + 3*(200+200j)*(1-t)*t^2 + (200+300j)*t^3 + + can be rewritten in standard form as + + 3 2 + (-400 + -100j) t + (900 + 300j) t - 600 t + (300 + 100j) + + + To illustrate the awesomeness of being able to convert our Bezier curve + objects to numpy.poly1d objects and back, lets compute the unit tangent + vector of the above CubicBezier object, b, at t=0.5 in four different + ways. + + Tangent vectors (and more on polynomials) + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: python + + t = 0.5 + ### Method 1: the easy way + u1 = b.unit_tangent(t) + + ### Method 2: another easy way + # Note: This way will fail if it encounters a removable singularity. + u2 = b.derivative(t)/abs(b.derivative(t)) + + ### Method 2: a third easy way + # Note: This way will also fail if it encounters a removable singularity. + dp = p.deriv() + u3 = dp(t)/abs(dp(t)) + + ### Method 4: the removable-singularity-proof numpy.poly1d way + # Note: This is roughly how Method 1 works + from svgpathtools import real, imag, rational_limit + dx, dy = real(dp), imag(dp) # dp == dx + 1j*dy + p_mag2 = dx**2 + dy**2 # p_mag2(t) = |p(t)|**2 + # Note: abs(dp) isn't a polynomial, but abs(dp)**2 is, and, + # the limit_{t->t0}[f(t) / abs(f(t))] == + # sqrt(limit_{t->t0}[f(t)**2 / abs(f(t))**2]) + from cmath import sqrt + u4 = sqrt(rational_limit(dp**2, p_mag2, t)) + + print("unit tangent check:", u1 == u2 == u3 == u4) + + # Let's do a visual check + mag = b.length()/4 # so it's not hard to see the tangent line + tangent_line = Line(b.point(t), b.point(t) + mag*u1) + disvg([b, tangent_line], 'bg', nodes=[b.point(t)]) + + + .. parsed-literal:: + + unit tangent check: True + + + Translations (shifts), reversing orientation, and normal vectors + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: python + + # Speaking of tangents, let's add a normal vector to the picture + n = b.normal(t) + normal_line = Line(b.point(t), b.point(t) + mag*n) + disvg([b, tangent_line, normal_line], 'bgp', nodes=[b.point(t)]) + + # and let's reverse the orientation of b! + # the tangent and normal lines should be sent to their opposites + br = b.reversed() + + # Let's also shift b_r over a bit to the right so we can view it next to b + # The simplest way to do this is br = br.translated(3*mag), but let's use + # the .bpoints() instead, which returns a Bezier's control points + br.start, br.control1, br.control2, br.end = [3*mag + bpt for bpt in br.bpoints()] # + + tangent_line_r = Line(br.point(t), br.point(t) + mag*br.unit_tangent(t)) + normal_line_r = Line(br.point(t), br.point(t) + mag*br.normal(t)) + wsvg([b, tangent_line, normal_line, br, tangent_line_r, normal_line_r], + 'bgpkgp', nodes=[b.point(t), br.point(t)], filename='vectorframes.svg', + text=["b's tangent", "br's tangent"], text_path=[tangent_line, tangent_line_r]) + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/vectorframes.svg + :alt: vectorframes.svg + + vectorframes.svg + + Rotations and Translations + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: python + + # Let's take a Line and an Arc and make some pictures + top_half = Arc(start=-1, radius=1+2j, rotation=0, large_arc=1, sweep=1, end=1) + midline = Line(-1.5, 1.5) + + # First let's make our ellipse whole + bottom_half = top_half.rotated(180) + decorated_ellipse = Path(top_half, bottom_half) + + # Now let's add the decorations + for k in range(12): + decorated_ellipse.append(midline.rotated(30*k)) + + # Let's move it over so we can see the original Line and Arc object next + # to the final product + decorated_ellipse = decorated_ellipse.translated(4+0j) + wsvg([top_half, midline, decorated_ellipse], filename='decorated_ellipse.svg') + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/decorated_ellipse.svg + :alt: decorated\_ellipse.svg + + decorated\_ellipse.svg + + arc length and inverse arc length + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Here we'll create an SVG that shows off the parametric and geometric + midpoints of the paths from ``test.svg``. We'll need to compute use the + ``Path.length()``, ``Line.length()``, ``QuadraticBezier.length()``, + ``CubicBezier.length()``, and ``Arc.length()`` methods, as well as the + related inverse arc length methods ``.ilength()`` function to do this. + + .. code:: python + + # First we'll load the path data from the file test.svg + paths, attributes = svg2paths('test.svg') + + # Let's mark the parametric midpoint of each segment + # I say "parametric" midpoint because Bezier curves aren't + # parameterized by arclength + # If they're also the geometric midpoint, let's mark them + # purple and otherwise we'll mark the geometric midpoint green + min_depth = 5 + error = 1e-4 + dots = [] + ncols = [] + nradii = [] + for path in paths: + for seg in path: + parametric_mid = seg.point(0.5) + seg_length = seg.length() + if seg.length(0.5)/seg.length() == 1/2: + dots += [parametric_mid] + ncols += ['purple'] + nradii += [5] + else: + t_mid = seg.ilength(seg_length/2) + geo_mid = seg.point(t_mid) + dots += [parametric_mid, geo_mid] + ncols += ['red', 'green'] + nradii += [5] * 2 + + # In 'output2.svg' the paths will retain their original attributes + wsvg(paths, nodes=dots, node_colors=ncols, node_radii=nradii, + attributes=attributes, filename='output2.svg') + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output2.svg + :alt: output2.svg + + output2.svg + + Intersections between Bezier curves + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: python + + # Let's find all intersections between redpath and the other + redpath = paths[0] + redpath_attribs = attributes[0] + intersections = [] + for path in paths[1:]: + for (T1, seg1, t1), (T2, seg2, t2) in redpath.intersect(path): + intersections.append(redpath.point(T1)) + + disvg(paths, filename='output_intersections.svg', attributes=attributes, + nodes = intersections, node_radii = [5]*len(intersections)) + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/output_intersections.svg + :alt: output\_intersections.svg + + output\_intersections.svg + + An Advanced Application: Offsetting Paths + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Here we'll find the `offset + curve `__ for a few paths. + + .. code:: python + + from svgpathtools import parse_path, Line, Path, wsvg + def offset_curve(path, offset_distance, steps=1000): + """Takes in a Path object, `path`, and a distance, + `offset_distance`, and outputs an piecewise-linear approximation + of the 'parallel' offset curve.""" + nls = [] + for seg in path: + ct = 1 + for k in range(steps): + t = k / steps + offset_vector = offset_distance * seg.normal(t) + nl = Line(seg.point(t), seg.point(t) + offset_vector) + nls.append(nl) + connect_the_dots = [Line(nls[k].end, nls[k+1].end) for k in range(len(nls)-1)] + if path.isclosed(): + connect_the_dots.append(Line(nls[-1].end, nls[0].end)) + offset_path = Path(*connect_the_dots) + return offset_path + + # Examples: + path1 = parse_path("m 288,600 c -52,-28 -42,-61 0,-97 ") + path2 = parse_path("M 151,395 C 407,485 726.17662,160 634,339").translated(300) + path3 = parse_path("m 117,695 c 237,-7 -103,-146 457,0").translated(500+400j) + paths = [path1, path2, path3] + + offset_distances = [10*k for k in range(1,51)] + offset_paths = [] + for path in paths: + for distances in offset_distances: + offset_paths.append(offset_curve(path, distances)) + + # Note: This will take a few moments + wsvg(paths + offset_paths, 'g'*len(paths) + 'r'*len(offset_paths), filename='offset_curves.svg') + + .. figure:: https://cdn.rawgit.com/mathandy/svgpathtools/master/offset_curves.svg + :alt: offset\_curves.svg + + offset\_curves.svg + + Compatibility Notes for users of svg.path (v2.0) + ------------------------------------------------ + + - renamed Arc.arc attribute as Arc.large\_arc + + - Path.d() : For behavior similar\ `2 <#f2>`__\ to svg.path (v2.0), + set both useSandT and use\_closed\_attrib to be True. + + 2 The behavior would be identical, but the string formatting used in + this method has been changed to use default format (instead of the + General format, {:G}), for inceased precision. `↩ <#a2>`__ + + Licence + ------- + + This module is under a MIT License. + +Keywords: svg,svg path,svg.path,bezier,parse svg path,display svg +Platform: OS Independent +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Topic :: Multimedia :: Graphics :: Editors :: Vector-Based +Classifier: Topic :: Scientific/Engineering +Classifier: Topic :: Scientific/Engineering :: Image Recognition +Classifier: Topic :: Scientific/Engineering :: Information Analysis +Classifier: Topic :: Scientific/Engineering :: Mathematics +Classifier: Topic :: Scientific/Engineering :: Visualization +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires: numpy +Requires: svgwrite diff --git a/svgpathtools.egg-info/SOURCES.txt b/svgpathtools.egg-info/SOURCES.txt index fcc0a2a..99a5197 100644 --- a/svgpathtools.egg-info/SOURCES.txt +++ b/svgpathtools.egg-info/SOURCES.txt @@ -15,10 +15,12 @@ test.svg vectorframes.svg svgpathtools/__init__.py svgpathtools/bezier.py +svgpathtools/directional_field.py svgpathtools/misctools.py svgpathtools/parser.py svgpathtools/path.py svgpathtools/paths2svg.py +svgpathtools/pathtools.py svgpathtools/polytools.py svgpathtools/smoothing.py svgpathtools/svg2paths.py @@ -26,13 +28,10 @@ svgpathtools.egg-info/PKG-INFO svgpathtools.egg-info/SOURCES.txt svgpathtools.egg-info/dependency_links.txt svgpathtools.egg-info/top_level.txt -test/groups.svg -test/polygons.svg test/test.svg test/test_bezier.py test/test_generation.py test/test_parsing.py test/test_path.py -test/test_polytools.py -test/test_svg2paths.py -test/test_svg2paths_groups.py \ No newline at end of file +test/test_pathtools.py +test/test_polytools.py \ No newline at end of file diff --git a/svgpathtools/path.py b/svgpathtools/path.py index 7afd58e..41989de 100644 --- a/svgpathtools/path.py +++ b/svgpathtools/path.py @@ -97,6 +97,21 @@ def bbox2path(xmin, xmax, ymin, ymax): return Path(b, r, t.reversed(), l.reversed()) +def polyline(*points): + """Converts a list of points to a Path composed of lines connecting those + points (i.e. a linear spline or polyline). See also `polygon()`.""" + return Path(*[Line(points[i], points[i+1]) + for i in range(len(points) - 1)]) + + +def polygon(*points): + """Converts a list of points to a Path composed of lines connecting those + points, then closes the path by connecting the last point to the first. + See also `polyline()`.""" + return Path(*[Line(points[i], points[(i + 1) % len(points)]) + for i in range(len(points))]) + + # Conversion################################################################### def bpoints2bezier(bpoints): From 87a54de2a386dca42048d7c26608c5dc7137e51d Mon Sep 17 00:00:00 2001 From: alphanoob1337 Date: Wed, 12 Jul 2017 19:04:31 +0200 Subject: [PATCH 10/10] Another try... --- build/lib/svgpathtools/path.py | 8 +------- svgpathtools/path.py | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/build/lib/svgpathtools/path.py b/build/lib/svgpathtools/path.py index 7afd58e..ec23b71 100644 --- a/build/lib/svgpathtools/path.py +++ b/build/lib/svgpathtools/path.py @@ -2136,13 +2136,7 @@ class Path(MutableSequence): def cropped(self, T0, T1): """returns a cropped copy of the path.""" - assert 0 <= T0 <= 1 and 0 <= T1<= 1 assert T0 != T1 - assert not (T0 == 1 and T1 == 0) - - if T0 == 1 and 0 < T1 < 1 and self.isclosed(): - return self.cropped(0, T1) - if T1 == 1: seg1 = self[-1] t_seg1 = 1 @@ -2177,7 +2171,7 @@ class Path(MutableSequence): # T1