commit
b58bf0f407
|
@ -5,9 +5,38 @@ The main tool being the svg2paths() function."""
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
from xml.dom.minidom import parse
|
from xml.dom.minidom import parse
|
||||||
from os import path as os_path, getcwd
|
from os import path as os_path, getcwd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .parser import parse_path
|
from .parser import parse_path
|
||||||
|
from .path import Path, bpoints2bezier
|
||||||
|
|
||||||
|
|
||||||
|
def ellipse2pathd(ellipse):
|
||||||
|
"""converts the parameters from an ellipse or a circle to a string for a
|
||||||
|
Path object d-attribute"""
|
||||||
|
|
||||||
|
cx = ellipse.get('cx', None)
|
||||||
|
cy = ellipse.get('cy', None)
|
||||||
|
rx = ellipse.get('rx', None)
|
||||||
|
ry = ellipse.get('ry', None)
|
||||||
|
r = ellipse.get('r', None)
|
||||||
|
|
||||||
|
if r is not None:
|
||||||
|
rx = ry = float(r)
|
||||||
|
else:
|
||||||
|
rx = float(rx)
|
||||||
|
ry = float(ry)
|
||||||
|
|
||||||
|
cx = float(cx)
|
||||||
|
cy = float(cy)
|
||||||
|
|
||||||
|
d = ''
|
||||||
|
d += 'M' + str(cx - rx) + ',' + str(cy)
|
||||||
|
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(2 * rx) + ',0'
|
||||||
|
d += 'a' + str(rx) + ',' + str(ry) + ' 0 1,0 ' + str(-2 * rx) + ',0'
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
def ellipse2pathd(ellipse):
|
def ellipse2pathd(ellipse):
|
||||||
|
@ -71,7 +100,8 @@ def polygon2pathd(polyline_d):
|
||||||
|
|
||||||
# The `parse_path` call ignores redundant 'z' (closure) commands
|
# The `parse_path` call ignores redundant 'z' (closure) commands
|
||||||
# e.g. `parse_path('M0 0L100 100Z') == parse_path('M0 0L100 100L0 0Z')`
|
# 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:
|
if reduntantly_closed:
|
||||||
d += 'L' + points[0].replace(',', ' ')
|
d += 'L' + points[0].replace(',', ' ')
|
||||||
|
|
||||||
|
@ -144,30 +174,139 @@ def svg2paths(svg_file_location,
|
||||||
values = [val.value for val in list(element.attributes.values())]
|
values = [val.value for val in list(element.attributes.values())]
|
||||||
return dict(list(zip(keys, values)))
|
return dict(list(zip(keys, values)))
|
||||||
|
|
||||||
# Use minidom to extract path strings from input SVG
|
def parse_trafo(trafo_str):
|
||||||
paths = [dom2dict(el) for el in doc.getElementsByTagName('path')]
|
"""Returns six matrix elements for a matrix transformation for any
|
||||||
d_strings = [el['d'] for el in paths]
|
valid SVG transformation string."""
|
||||||
attribute_dictionary_list = paths
|
trafos = trafo_str.split(')')[:-1]
|
||||||
|
trafo_matrix = np.array([1., 0., 0., 0., 1., 0., 0., 0., 1.]).reshape(
|
||||||
|
(3, 3)) # Start with neutral matrix
|
||||||
|
|
||||||
# Use minidom to extract polyline strings from input SVG, convert to
|
for trafo_sub_str in trafos:
|
||||||
# path strings, add to list
|
trafo_sub_str = trafo_sub_str.lstrip(', ')
|
||||||
if convert_polylines_to_paths:
|
value_str = trafo_sub_str.split('(')[1]
|
||||||
plins = [dom2dict(el) for el in doc.getElementsByTagName('polyline')]
|
values = list(map(float, value_str.split(',')))
|
||||||
d_strings += [polyline2pathd(pl['points']) for pl in plins]
|
if 'translate' in trafo_sub_str:
|
||||||
attribute_dictionary_list += plins
|
x = values[0]
|
||||||
|
y = values[1] if (len(values) > 1) else 0.
|
||||||
|
trafo_matrix = np.dot(trafo_matrix, np.array(
|
||||||
|
[1., 0., x, 0., 1., y, 0., 0., 1.]).reshape((3, 3)))
|
||||||
|
elif 'scale' in trafo_sub_str:
|
||||||
|
x = values[0]
|
||||||
|
y = values[1] if (len(values) > 1) else 0.
|
||||||
|
trafo_matrix = np.dot(trafo_matrix,
|
||||||
|
np.array([x, 0., 0., 0., y, 0., 0., 0.,
|
||||||
|
1.]).reshape((3, 3)))
|
||||||
|
elif 'rotate' in trafo_sub_str:
|
||||||
|
a = values[0] * np.pi / 180.
|
||||||
|
x = values[1] if (len(values) > 1) else 0.
|
||||||
|
y = values[2] if (len(values) > 2) else 0.
|
||||||
|
am = np.dot(np.array(
|
||||||
|
[np.cos(a), -np.sin(a), 0., np.sin(a), np.cos(a), 0., 0.,
|
||||||
|
0., 1.]).reshape((3, 3)),
|
||||||
|
np.array(
|
||||||
|
[1., 0., -x, 0., 1., -y, 0., 0., 1.]).reshape(
|
||||||
|
(3, 3)))
|
||||||
|
am = np.dot(
|
||||||
|
np.array([1., 0., x, 0., 1., y, 0., 0., 1.]).reshape(
|
||||||
|
(3, 3)), am)
|
||||||
|
trafo_matrix = np.dot(trafo_matrix, am)
|
||||||
|
elif 'skewX' in trafo_sub_str:
|
||||||
|
a = values[0] * np.pi / 180.
|
||||||
|
trafo_matrix = np.dot(trafo_matrix,
|
||||||
|
np.array(
|
||||||
|
[1., np.tan(a), 0., 0., 1., 0., 0.,
|
||||||
|
0., 1.]).reshape((3, 3)))
|
||||||
|
elif 'skewY' in trafo_sub_str:
|
||||||
|
a = values[0] * np.pi / 180.
|
||||||
|
trafo_matrix = np.dot(trafo_matrix,
|
||||||
|
np.array(
|
||||||
|
[1., 0., 0., np.tan(a), 1., 0., 0.,
|
||||||
|
0., 1.]).reshape((3, 3)))
|
||||||
|
else: # Assume matrix transformation
|
||||||
|
while len(values) < 6:
|
||||||
|
values += [0.]
|
||||||
|
trafo_matrix = np.dot(trafo_matrix,
|
||||||
|
np.array([values[::2], values[1::2],
|
||||||
|
[0., 0., 1.]]))
|
||||||
|
|
||||||
# Use minidom to extract polygon strings from input SVG, convert to
|
trafo_list = list(trafo_matrix.reshape((9,))[:6])
|
||||||
# path strings, add to list
|
return trafo_list[::3] + trafo_list[1::3] + trafo_list[2::3]
|
||||||
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:
|
def parse_node(node):
|
||||||
lines = [dom2dict(el) for el in doc.getElementsByTagName('line')]
|
"""Recursively iterate over nodes. Parse the groups individually to
|
||||||
d_strings += [('M' + l['x1'] + ' ' + l['y1'] +
|
apply group transformations."""
|
||||||
'L' + l['x2'] + ' ' + l['y2']) for l in lines]
|
# Get everything in this tag
|
||||||
attribute_dictionary_list += lines
|
data = [parse_node(child) for child in node.childNodes]
|
||||||
|
if len(data) == 0:
|
||||||
|
ret_list = []
|
||||||
|
attribute_dictionary_list_int = []
|
||||||
|
else:
|
||||||
|
# Flatten the lists
|
||||||
|
ret_list = []
|
||||||
|
attribute_dictionary_list_int = []
|
||||||
|
for item in data:
|
||||||
|
if type(item) == tuple:
|
||||||
|
if len(item[0]) > 0:
|
||||||
|
ret_list += item[0]
|
||||||
|
attribute_dictionary_list_int += item[1]
|
||||||
|
|
||||||
|
if node.nodeName == 'g':
|
||||||
|
# Group found
|
||||||
|
# Analyse group properties
|
||||||
|
group = dom2dict(node)
|
||||||
|
if 'transform' in group.keys():
|
||||||
|
trafo = group['transform']
|
||||||
|
|
||||||
|
# Convert all transformations into a matrix operation
|
||||||
|
am = parse_trafo(trafo)
|
||||||
|
am = np.array([am[::2], am[1::2], [0., 0., 1.]])
|
||||||
|
|
||||||
|
# Apply transformation to all elements of the paths
|
||||||
|
def xy(p):
|
||||||
|
return np.array([p.real, p.imag, 1.])
|
||||||
|
|
||||||
|
def z(coords):
|
||||||
|
return coords[0] + 1j * coords[1]
|
||||||
|
|
||||||
|
ret_list = [Path(*[bpoints2bezier([z(np.dot(am, xy(pt)))
|
||||||
|
for pt in seg.bpoints()])
|
||||||
|
for seg in path])
|
||||||
|
for path in ret_list]
|
||||||
|
return ret_list, attribute_dictionary_list_int
|
||||||
|
elif node.nodeName == 'path':
|
||||||
|
# Path found; parsing it
|
||||||
|
path = dom2dict(node)
|
||||||
|
d_string = path['d']
|
||||||
|
return [parse_path(d_string)] + ret_list, [
|
||||||
|
path] + attribute_dictionary_list_int
|
||||||
|
elif convert_polylines_to_paths and node.nodeName == 'polyline':
|
||||||
|
attrs = dom2dict(node)
|
||||||
|
path = parse_path(polyline2pathd(node['points']))
|
||||||
|
return [path] + ret_list, [attrs] + attribute_dictionary_list_int
|
||||||
|
elif convert_polygons_to_paths and node.nodeName == 'polygon':
|
||||||
|
attrs = dom2dict(node)
|
||||||
|
path = parse_path(polygon2pathd(attrs['points']))
|
||||||
|
return [path] + ret_list, [attrs] + attribute_dictionary_list_int
|
||||||
|
elif convert_lines_to_paths and node.nodeName == 'line':
|
||||||
|
line = dom2dict(node)
|
||||||
|
d_string = ('M' + line['x1'] + ' ' + line['y1'] +
|
||||||
|
'L' + line['x2'] + ' ' + line['y2'])
|
||||||
|
path = parse_path(d_string)
|
||||||
|
return [path] + ret_list, [line] + attribute_dictionary_list_int
|
||||||
|
elif convert_ellipses_to_paths and node.nodeName == 'ellipse':
|
||||||
|
attrs = dom2dict(node)
|
||||||
|
path = parse_path(ellipse2pathd(attrs))
|
||||||
|
return [path] + ret_list, [attrs] + attribute_dictionary_list_int
|
||||||
|
elif convert_circles_to_paths and node.nodeName == 'circle':
|
||||||
|
attrs = dom2dict(node)
|
||||||
|
path = parse_path(ellipse2pathd(attrs))
|
||||||
|
return [path] + ret_list, [attrs] + attribute_dictionary_list_int
|
||||||
|
elif convert_rectangles_to_paths and node.nodeName == 'rect':
|
||||||
|
attrs = dom2dict(node)
|
||||||
|
path = parse_path(rect2pathd(attrs))
|
||||||
|
return [path] + ret_list, [attrs] + attribute_dictionary_list_int
|
||||||
|
else:
|
||||||
|
return ret_list, attribute_dictionary_list_int
|
||||||
|
|
||||||
if convert_ellipses_to_paths:
|
if convert_ellipses_to_paths:
|
||||||
ellipses = [dom2dict(el) for el in doc.getElementsByTagName('ellipse')]
|
ellipses = [dom2dict(el) for el in doc.getElementsByTagName('ellipse')]
|
||||||
|
@ -187,11 +326,9 @@ def svg2paths(svg_file_location,
|
||||||
if return_svg_attributes:
|
if return_svg_attributes:
|
||||||
svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0])
|
svg_attributes = dom2dict(doc.getElementsByTagName('svg')[0])
|
||||||
doc.unlink()
|
doc.unlink()
|
||||||
path_list = [parse_path(d) for d in d_strings]
|
|
||||||
return path_list, attribute_dictionary_list, svg_attributes
|
return path_list, attribute_dictionary_list, svg_attributes
|
||||||
else:
|
else:
|
||||||
doc.unlink()
|
doc.unlink()
|
||||||
path_list = [parse_path(d) for d in d_strings]
|
|
||||||
return path_list, attribute_dictionary_list
|
return path_list, attribute_dictionary_list
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" ?>
|
||||||
|
<svg id="rootn" version="1.1" baseProfile="full" width="1250" height="200" xmlns="http://www.w3.org/2000/svg" xmlns:ev="http://www.w3.org/2001/xml-events" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path
|
||||||
|
id="p0"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 50.,100. 100.,0." />
|
||||||
|
<g id="translate" transform="translate(100.,200.)">
|
||||||
|
<path
|
||||||
|
id="p1"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 0.,-150. 0,100." />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path
|
||||||
|
id="p2"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 200.,100. 100.,0." />
|
||||||
|
<g id="scale" transform="scale(1.,10.)">
|
||||||
|
<path
|
||||||
|
id="p3"
|
||||||
|
style="stroke:#ff0000;stroke-width:.20"
|
||||||
|
d="m 250.,5. 0,10." />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path
|
||||||
|
id="p4"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 350.,100. 100.,0." />
|
||||||
|
<g id="rotate" transform="rotate(-90.,50.,0.)">
|
||||||
|
<path
|
||||||
|
id="p5"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m -100.,350. 100.,0." />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path
|
||||||
|
id="p6"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 500.,100. 100.,0." />
|
||||||
|
<g id="skewX" transform="skewX(-45.)">
|
||||||
|
<path
|
||||||
|
id="p7"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 600.,50. 100.,100." />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g id="skewY" transform="skewY(-45.)">
|
||||||
|
<path
|
||||||
|
id="p8"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 650.,750. 100.,100." />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
id="p9"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 700.,50. 0.,100." />
|
||||||
|
|
||||||
|
<path
|
||||||
|
id="p10"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 800.,100. 100.,0." />
|
||||||
|
<g id="matrix" transform="matrix(0.,-1.,1.,0.,850.,150.)">
|
||||||
|
<path
|
||||||
|
id="p11"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 0.,0. 100.,0." />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path
|
||||||
|
id="p12"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 950.,100. 100.,0." />
|
||||||
|
<g id="translatenested" transform="translate(1000.,150.)">
|
||||||
|
<g id="rotatenested" transform="rotate(-90.)">
|
||||||
|
<path
|
||||||
|
id="p13"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 0.,0. 100.,0." />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<path
|
||||||
|
id="p14"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 1100.,100. 100.,0." />
|
||||||
|
<g id="concatenated" transform="translate(1150.,150.) rotate(-90.)">
|
||||||
|
<path
|
||||||
|
id="p15"
|
||||||
|
style="stroke:#ff0000;stroke-width:2."
|
||||||
|
d="m 0.,0. 100.,0." />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -0,0 +1,15 @@
|
||||||
|
from __future__ import division, absolute_import, print_function
|
||||||
|
from unittest import TestCase
|
||||||
|
from svgpathtools import *
|
||||||
|
from os.path import join, dirname
|
||||||
|
|
||||||
|
class TestSvg2pathsGroups(TestCase):
|
||||||
|
def test_svg2paths(self):
|
||||||
|
paths, _ = svg2paths(join(dirname(__file__), 'groups.svg'))
|
||||||
|
|
||||||
|
# the paths should form crosses after being transformed
|
||||||
|
self.assertTrue((len(paths) % 2) == 0)
|
||||||
|
|
||||||
|
for i in range(len(paths)//2):
|
||||||
|
print(i * 2)
|
||||||
|
self.assertTrue(len(paths[i * 2].intersect(paths[i * 2 + 1])) > 0, 'Path '+str(i * 2)+' does not intersect path '+str(i * 2 + 1)+'!')
|
Loading…
Reference in New Issue