Skip to content

Commit 522d749

Browse files
committed
Add experimental SVG converter
1 parent 8c1cb7e commit 522d749

File tree

3 files changed

+267
-0
lines changed

3 files changed

+267
-0
lines changed

‎py2d/SVG.py‎

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"""SVG to Polygon conversion
2+
3+
This is still experimental code and only a tiny subset of SVG is supported. Things that are NOT supported:
4+
5+
- open line segments
6+
- curves of any kind
7+
8+
Testing files were generated using Inkscape. Make sure to use "Make selected nodes corner" on all polygons to be converted so they contain no problematic SVG path commands!
9+
"""
10+
11+
from xml.etree import ElementTree
12+
import re
13+
import warnings
14+
from collections import deque
15+
from Math import Polygon, Vector, Transform
16+
17+
def convert_svg(f, transform=Transform.unit()):
18+
"""Convert an SVG file to a hash of Py2D Polygons.
19+
20+
The hash keys will be the ids set to the corresponding <path> elements in the SVG file.
21+
The hash value is a list of polygons, the first one being the outline polygon and all additional polygons being holes.
22+
23+
@param f: File object or file name to a SVG file.
24+
25+
@type transform: Transform
26+
@param transform: A transformation to apply to all polygons
27+
"""
28+
29+
svg_ns = "http://www.w3.org/2000/svg"
30+
et = ElementTree.parse(f)
31+
32+
def transform_element(e, transform):
33+
new_t = e.get("transform", "")
34+
35+
nt = transform
36+
m = re.match(r'translate\((?P<x>[0-9.-]+),(?P<y>[0-9.-]+)\)', new_t)
37+
if m:
38+
x, y = float(m.group("x")), float(m.group("y"))
39+
nt = Transform.move(x,y) * transform
40+
41+
return nt
42+
43+
def path_find(e, transform=Transform.unit()):
44+
"""Generator function to recursively list all paths under an element e"""
45+
46+
# yield path nodes within current element
47+
for p in e.iterfind("{%s}path" % svg_ns):
48+
yield (p, transform_element(p, transform))
49+
50+
# yield path nodes in subgroups
51+
for g in e.iterfind("{%s}g" % svg_ns):
52+
for p,t in path_find(g, transform_element(g, transform)):
53+
yield (p, t)
54+
55+
56+
def convert_element(e, transform):
57+
"""Convert an SVG path element to one or multiple Py2D polygons."""
58+
59+
# get data from the <path> element
60+
id = e.get("id")
61+
d = e.get("d")
62+
63+
#print "CONVERTING %s: %s" % (id, d)
64+
65+
def parse_commands(draw_commands):
66+
"""Generator Function to parse a SVG draw command sequence into command and parameter tuples"""
67+
tokens = draw_commands.split(" ")
68+
while tokens:
69+
# find the next token that is a command
70+
par_index = next( i for i,v in enumerate(tokens[1:] + ["E"]) if re.match('^[a-zA-Z]$', v) ) + 1
71+
72+
# first token should always be the command, rest the parameters
73+
cmd = tokens[0]
74+
pars = tokens[1:par_index]
75+
76+
# remove the parsed tokens
77+
del tokens[:par_index]
78+
79+
yield cmd, pars
80+
81+
def parse_vec(s):
82+
x,y = s.split(",")
83+
return Vector(float(x), float(y))
84+
85+
polys = []
86+
verts = []
87+
relative_pos = Vector(0.0,0.0)
88+
for cmd, pars in parse_commands(d):
89+
90+
#print "cmd: %s, pars: %s" % (cmd, pars)
91+
92+
if cmd == "m":
93+
94+
for p in pars:
95+
relative_pos += parse_vec(p)
96+
verts.append(relative_pos)
97+
98+
elif cmd == "z":
99+
# close line by only moving relative_pos to first vertex
100+
101+
polys.append(transform * Polygon.from_pointlist(verts))
102+
relative_pos = verts[0]
103+
verts = []
104+
105+
pass
106+
107+
else:
108+
warnings.warn("Unrecognized SVG path command: %s - path skipped" % cmd)
109+
polys = []
110+
break
111+
112+
#print "----"
113+
114+
return id, polys
115+
116+
out = {}
117+
for p,tr in path_find(et.getroot(), transform):
118+
id, polys = convert_element(p,tr)
119+
out[id] = polys
120+
121+
return out
122+
123+
if __name__ == "__main__":
124+
print convert_svg("py2d/examples/shapes.svg")

‎py2d/examples/SVG.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import pygame
2+
from pygame.locals import *
3+
4+
from py2d.Math import *
5+
from py2d.SVG import convert_svg
6+
7+
import py2d.examples.Main
8+
9+
class SVGConverter(py2d.examples.Main.Example):
10+
"""SVG Converter Sample
11+
12+
Polygons are imported from an SVG file created in Inkscape.
13+
14+
Have fun!
15+
"""
16+
def __init__(self, runner):
17+
self.runner = runner
18+
self.title = "Polygon Decomposition"
19+
20+
self.polys = []
21+
22+
for id, polys in convert_svg("py2d/examples/shapes.svg").iteritems():
23+
self.polys.append(polys)
24+
25+
self.decomp = []
26+
27+
self.poly_colors = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff, 0x00ffff ]
28+
29+
print self.polys
30+
31+
32+
def render(self):
33+
34+
t = Transform.unit() #Transform.move(-22,-700)
35+
36+
for j, p in enumerate(self.polys):
37+
for i,h in enumerate(p):
38+
self.draw_poly(t * h, self.poly_colors[j % len(self.poly_colors)], False)
39+
40+
41+
def draw_poly(self, poly, color, fill):
42+
if len(poly) > 1:
43+
if fill and len(poly) > 2:
44+
pygame.draw.polygon(self.runner.screen, color, poly.as_tuple_list())
45+
46+
pygame.draw.lines(self.runner.screen, color, True, poly.as_tuple_list())
47+
elif poly.points:
48+
pygame.draw.circle(self.runner.screen, color, poly.points[0].as_tuple(), 2)
49+
50+

‎py2d/examples/shapes.svg‎

Lines changed: 93 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)