-
-
Save cjlano/5109445 to your computer and use it in GitHub Desktop.
plot bezier curve from SVG
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import Image, ImageDraw | |
def bezier1(p0, p1, t): | |
x = p0[0] + t * (p1[0] - p0[0]) | |
y = p0[1] + t * (p1[1] - p0[1]) | |
return (x,y) | |
def bezierN(pts, t): | |
res = list(pts) | |
for n in range(len(pts), 1, -1): | |
for i in range(0,n-1): | |
res[i] = bezier1(res[i], res[i+1], t) | |
return res[0] | |
im = Image.new("RGB", (100,100), "white") | |
draw = ImageDraw.Draw(im) | |
red = (255,0,0) | |
green = (0,255,0) | |
blue = (0,0,255) | |
pts = [(10,90),(5,40),(60,40),(90,90)] | |
for pt in pts: | |
im.putpixel(pt, red) | |
draw.line(pts,fill=red) | |
bezier = [] | |
for t in range(0,10): | |
xy = bezierN(pts, t*0.1) | |
bezier.append(xy) | |
im.putpixel((int(round(xy[0])), int(round(xy[1]))), green) | |
draw.line(bezier, fill=blue) | |
#im.save(os.path.expanduser("~/public_html/bezier.png")) | |
im.show() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
M37.441-3.5c8.951,0,16.572,3.125,22.857,9.372c3.008,3.009,5.295,6.448,6.857,10.314 c1.561,3.867,2.344,7.971,2.344,12.314c0,4.381-0.773,8.486-2.314,12.313c-1.543,3.828-3.82,7.21-6.828,10.143 c-3.123,3.085-6.666,5.448-10.629,7.086c-3.961,1.638-8.057,2.457-12.285,2.457s-8.276-0.808-12.143-2.429 c-3.866-1.618-7.333-3.961-10.4-7.027c-3.067-3.066-5.4-6.524-7-10.372S5.5,32.767,5.5,28.5c0-4.229,0.809-8.295,2.428-12.2 c1.619-3.905,3.972-7.4,7.057-10.486C21.08-0.394,28.565-3.5,37.441-3.5z M37.557,2.272c-7.314,0-13.467,2.553-18.458,7.657 c-2.515,2.553-4.448,5.419-5.8,8.6c-1.354,3.181-2.029,6.505-2.029,9.972c0,3.429,0.675,6.734,2.029,9.913 c1.353,3.183,3.285,6.021,5.8,8.516c2.514,2.496,5.351,4.399,8.515,5.715c3.161,1.314,6.476,1.971,9.943,1.971 c3.428,0,6.75-0.665,9.973-1.999c3.219-1.335,6.121-3.257,8.713-5.771c4.99-4.876,7.484-10.99,7.484-18.344 c0-3.543-0.648-6.895-1.943-10.057c-1.293-3.162-3.18-5.98-5.654-8.458C50.984,4.844,44.795,2.272,37.557,2.272z M37.156,23.187 l-4.287,2.229c-0.458-0.951-1.019-1.619-1.685-2c-0.667-0.38-1.286-0.571-1.858-0.571c-2.856,0-4.286,1.885-4.286,5.657 c0,1.714,0.362,3.084,1.085,4.113c0.724,1.029,1.791,1.544,3.201,1.544c1.867,0,3.181-0.915,3.944-2.743l3.942,2 c-0.838,1.563-2,2.791-3.486,3.686c-1.484,0.896-3.123,1.343-4.914,1.343c-2.857,0-5.163-0.875-6.915-2.629 c-1.752-1.752-2.628-4.19-2.628-7.313c0-3.048,0.886-5.466,2.657-7.257c1.771-1.79,4.009-2.686,6.715-2.686 C32.604,18.558,35.441,20.101,37.156,23.187z M55.613,23.187l-4.229,2.229c-0.457-0.951-1.02-1.619-1.686-2 c-0.668-0.38-1.307-0.571-1.914-0.571c-2.857,0-4.287,1.885-4.287,5.657c0,1.714,0.363,3.084,1.086,4.113 c0.723,1.029,1.789,1.544,3.201,1.544c1.865,0,3.18-0.915,3.941-2.743l4,2c-0.875,1.563-2.057,2.791-3.541,3.686 c-1.486,0.896-3.105,1.343-4.857,1.343c-2.896,0-5.209-0.875-6.941-2.629c-1.736-1.752-2.602-4.19-2.602-7.313 c0-3.048,0.885-5.466,2.658-7.257c1.77-1.79,4.008-2.686,6.713-2.686C51.117,18.558,53.938,20.101,55.613,23.187z |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8" standalone="no"?> | |
<!-- Created with Inkscape (http://www.inkscape.org/) --> | |
<svg | |
xmlns:svg="http://www.w3.org/2000/svg" | |
xmlns="http://www.w3.org/2000/svg" | |
version="1.1" | |
width="525" | |
height="525" | |
id="svg2894"> | |
<defs | |
id="defs2898" /> | |
<path | |
d="M 258.02822,449.92907 C 216.23561,444.16139 175.4234,427.25125 143.5,402.47542 133.44605,394.67252 115.73782,376.40039 109.18036,367.06291 94.835361,346.63637 87,321.34904 87,295.47948 87,232.75299 133.79919,178.8344 211.49716,152.043 c 12.57887,-4.33737 22.38442,-6.44909 54.00284,-11.62999 21.98019,-3.60161 22.02147,-3.60438 45.84382,-3.07652 22.28554,0.49381 23.74755,0.41839 22.37059,-1.15407 -4.73363,-5.40575 -5.94414,-12.7887 -3.0848,-18.81431 7.34694,-15.48252 38.4631,-19.396203 53.29234,-6.70293 11.63658,9.96048 8.84962,23.71074 -6.4033,31.59256 l -2.98135,1.54058 4.48135,1.13653 c 29.97155,7.6012 51.58857,15.78955 62.74173,23.76607 43.9615,31.44035 65.76184,69.17043 76.31237,132.07458 2.59087,15.44724 3.06152,20.66622 2.76579,30.66969 -0.32759,11.08174 -0.61314,12.59838 -3.53742,18.78881 -12.54914,26.56529 -45.29719,58.10305 -77.01214,74.16596 -9.30679,4.71368 -36.50951,15.59929 -40.66867,16.27423 -4.47109,0.72556 -5.52752,0.0301 -13.20131,-8.68994 -16.98624,-19.30225 -37.26172,-26.84624 -56.5324,-21.03426 -15.54003,4.68683 -26.35266,13.1073 -38.00509,29.59693 L 283.78817,452 l -6.14408,-0.10587 c -3.37925,-0.0582 -12.20639,-0.9425 -19.61587,-1.96506 z M 53.370467,205.66727 C 28.249651,197.29509 9.6491005,173.14627 4.8739644,142.70497 3.2875779,132.59182 4.3814573,113.41291 7.0815232,104 13.400552,81.970748 26.642885,65.164131 44.515165,56.490784 66.407186,45.866672 90.11011,48.494026 109.47263,63.691002 c 10.99999,8.633515 21.28575,25.372047 25.69146,41.808998 2.93139,10.93652 3.17098,33.37846 0.47933,44.89733 -3.70261,15.84522 -12.00927,31.16159 -21.7755,40.15109 C 101.02749,202.36763 89.929278,206.88516 72,207.59079 61.824211,207.99127 59.679043,207.76977 53.370467,205.66727 z M 286.3585,134.90933 c -16.39324,-4.75014 -29.32495,-29.97227 -26.30291,-51.301407 2.47678,-17.480694 12.49975,-27.621562 27.27732,-27.598154 23.44621,0.03714 40.59738,31.74999 32.21918,59.573871 -4.65394,15.45567 -18.57608,23.5613 -33.19359,19.32569 z m -123.4487,-23.2191 c -10.73876,-4.02211 -18.3823,-15.203079 -21.48735,-31.431676 -4.60026,-24.043396 3.1508,-47.582045 18.19894,-55.267068 7.13808,-3.64539 17.09219,-3.541645 23.5341,0.245281 4.81488,2.830469 9.31944,7.818493 12.27299,13.590231 l 1.70251,3.326998 2.68451,-3.490673 c 7.94555,-10.33164 20.80498,-12.551286 30.37613,-5.243174 15.61603,11.923728 17.20366,48.030713 2.74755,62.486821 -9.46114,9.46114 -22.25377,9.28757 -31.08911,-0.421812 C 200.10578,93.568321 198.38429,92 198.02453,92 c -0.35976,0 -1.45137,1.671884 -2.42581,3.715298 -2.78156,5.833012 -7.39457,11.080752 -12.23303,13.916282 -5.37156,3.14794 -14.96879,4.11379 -20.45589,2.05865 z" | |
id="path2928" | |
style="fill:#000000" /> | |
</svg> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import time | |
LIBMODULE_HEADER = 'PCBNEW-LibModule-V1 ' | |
LIBMODULE_HEADER += time.strftime("%c", time.localtime()) | |
LIBMODULE_HEADER += '\n# encoding utf-8\n' | |
LIBMODULE_HEADER += 'Units mm\n' | |
class LibModule: | |
'''Container (file) for all the Modules''' | |
def __init__(self, filename): | |
self.filename = filename | |
self.modules = [] | |
def add_module(self, mod): | |
self.modules.append(mod) | |
def write(self): | |
f = open(self.filename, 'w') | |
f.write(LIBMODULE_HEADER) | |
# Index | |
f.write("$INDEX\n") | |
for m in self.modules: | |
f.write(m.name + '\n') | |
f.write("$EndINDEX\n") | |
for m in self.modules: | |
f.write(str(m)) | |
f.close() | |
class Module: | |
def __init__(self, name): | |
self.name = name | |
self.position = { | |
'Xpos': 0, | |
'Ypos': 0, | |
'orientation': 0, | |
'layer': 15, | |
'timestamp': '00000000 00000000', | |
'attr1': '~', | |
'attr2': '~'} | |
self.cd = '' | |
self.kw = '' | |
self.fields = [] | |
self.drawings = [] | |
self.pads = [] # not implemented | |
self.reference('M*') | |
self.value(name) | |
def __str__(self): | |
s = '$MODULE ' + self.name + '\n' | |
# Position | |
s += 'Po {Xpos} {Ypos} {orientation} {layer} {timestamp} {attr1}{attr2}\n'.format(**self.position) | |
# Module lib name | |
s += 'Li ' + self.name + '\n' | |
# Comments & keywords | |
s += 'Cd ' + self.cd + '\n' | |
s += 'Kw ' + self.kw + '\n' | |
# TimeStampOp (?) | |
s += 'Sc 00000000\n' | |
# AR (?) | |
s += 'AR ' + self.name + '\n' | |
# Op (?) | |
s += 'Op 0 0 0\n' | |
# fields | |
for f in self.fields: | |
s += 'T{nb} {Xpos} {Ypos} {Xsize} {Ysize} {rotation} {penWidth} N {visible} {layer} N "{text}"\n'.format(**f) | |
# drawings | |
for d in self.drawings: | |
s += str(d) | |
s += '$EndMODULE ' + self.name + '\n' | |
return s | |
def position(self, x=None, y=None, orientation=None, layer=None): | |
if x is not None: | |
self.position['Xpos'] = x | |
if y is not None: | |
self.position['Ypos'] = y | |
if orientation is not None: | |
self.position['orientation'] = orientation | |
if layer is not None: | |
self.position['layer'] = layer | |
def comment(self, desc=''): | |
self.cd = desc | |
def keywords(self, kw=''): | |
self.kw = kw | |
def field(self, nb, Xpos=0, Ypos=0, Xsize=0.8128, Ysize=0.8128, rotation=0, penWidth=0.1524, visible=True, layer=21, text=''): | |
if visible: | |
visible = 'V' | |
else: | |
visible = 'I' | |
# Remove posible duplicate | |
self.fields = [f for f in self.fields if f.get('nb') != nb] | |
f = { | |
'nb': nb, | |
'Xpos': Xpos, | |
'Ypos': Ypos, | |
'Xsize': Xsize, | |
'Ysize': Ysize, | |
'rotation': rotation, | |
'penWidth': penWidth, | |
'visible': visible, | |
'layer': layer, | |
'text': text} | |
self.fields.append(f) | |
def reference(self, ref): | |
self.field(0, text=ref) | |
def value(self, value): | |
self.field(1, text=value) | |
def draw(self, d): | |
self.drawings.append(d) | |
class Segment: | |
def __init__(self, start, end, width, layer=21): | |
self.start = start | |
self.end = end | |
self.width = width | |
self.layer = layer | |
def __str__(self): | |
s = 'DS ' | |
s += ' '.join(map(str, self.start)) + ' ' | |
s += ' '.join(map(str, self.end)) + ' ' | |
s += str(self.width) + ' ' | |
s += str(self.layer) + '\n' | |
return s | |
class Polygon: | |
def __init__(self, pts, width=0, layer=21): | |
self.length = len(pts) | |
self.pts = list(pts) | |
self.width = width | |
self.layer = layer | |
def __str__(self): | |
s = 'DP 0 0 0 0 ' | |
s += str(self.length) + ' ' | |
s += str(self.width) + ' ' | |
s += str(self.layer) + '\n' | |
for pt in self.pts: | |
s += 'Dl ' + ' '.join(map(str, pt)) + '\n' | |
return s | |
class Circle: | |
def __init__(self, center, radius, width, layer=21): | |
self.center = center | |
self.pt = (center[0] + radius, center[1]) | |
self.width = width | |
self.layer = layer | |
def __str__(self): | |
s = 'DC ' | |
s += ' '.join(map(str, self.center)) + ' ' | |
s += ' '.join(map(str, self.pt)) + ' ' | |
s += str(self.width) + ' ' | |
s += str(self.layer) + '\n' | |
return s |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import kicad | |
import svg | |
import sys | |
f = svg.Svg(sys.argv[1]) | |
l = kicad.LibModule("/tmp/1.mod") | |
m = kicad.Module("MyTest") | |
m.reference('G*') | |
m.value(f.title()) | |
a,b = f.bbox() | |
# We want a 10.0mm width logo | |
width = 100.0 | |
ratio = width/(b-a).x | |
# Centering offset | |
offset = (a-b)*0.5*ratio | |
for draw in f.drawing: | |
if isinstance(draw, svg.Path): | |
for segment in draw.simplify(0.1): | |
pts = [x.coord() for x in segment] | |
pts.reverse() | |
p1 = pts.pop() | |
while pts: | |
p2 = pts.pop() | |
m.draw(kicad.Segment(p1, p2, 0.20)) | |
p1 = p2 | |
elif isinstance(draw, svg.Circle): | |
m.draw(kicad.Circle(draw.center.coord(), draw.radius, 0.20)) | |
else: | |
print("Unsupported SVG element" + draw) | |
#for s in f.scale(ratio).translate(offset).simplify(0.01): | |
# m.draw(kicad.Polygon([x.coord() for x in s])) | |
l.add_module(m) | |
l.write() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
M 258.02822,449.92907 C 216.23561,444.16139 175.4234,427.25125 143.5,402.47542 133.44605,394.67252 115.73782,376.40039 109.18036,367.06291 94.835361,346.63637 87,321.34904 87,295.47948 87,232.75299 133.79919,178.8344 211.49716,152.043 c 12.57887,-4.33737 22.38442,-6.44909 54.00284,-11.62999 21.98019,-3.60161 22.02147,-3.60438 45.84382,-3.07652 22.28554,0.49381 23.74755,0.41839 22.37059,-1.15407 -4.73363,-5.40575 -5.94414,-12.7887 -3.0848,-18.81431 7.34694,-15.48252 38.4631,-19.396203 53.29234,-6.70293 11.63658,9.96048 8.84962,23.71074 -6.4033,31.59256 l -2.98135,1.54058 4.48135,1.13653 c 29.97155,7.6012 51.58857,15.78955 62.74173,23.76607 43.9615,31.44035 65.76184,69.17043 76.31237,132.07458 2.59087,15.44724 3.06152,20.66622 2.76579,30.66969 -0.32759,11.08174 -0.61314,12.59838 -3.53742,18.78881 -12.54914,26.56529 -45.29719,58.10305 -77.01214,74.16596 -9.30679,4.71368 -36.50951,15.59929 -40.66867,16.27423 -4.47109,0.72556 -5.52752,0.0301 -13.20131,-8.68994 -16.98624,-19.30225 -37.26172,-26.84624 -56.5324,-21.03426 -15.54003,4.68683 -26.35266,13.1073 -38.00509,29.59693 L 283.78817,452 l -6.14408,-0.10587 c -3.37925,-0.0582 -12.20639,-0.9425 -19.61587,-1.96506 z M 53.370467,205.66727 C 28.249651,197.29509 9.6491005,173.14627 4.8739644,142.70497 3.2875779,132.59182 4.3814573,113.41291 7.0815232,104 13.400552,81.970748 26.642885,65.164131 44.515165,56.490784 66.407186,45.866672 90.11011,48.494026 109.47263,63.691002 c 10.99999,8.633515 21.28575,25.372047 25.69146,41.808998 2.93139,10.93652 3.17098,33.37846 0.47933,44.89733 -3.70261,15.84522 -12.00927,31.16159 -21.7755,40.15109 C 101.02749,202.36763 89.929278,206.88516 72,207.59079 61.824211,207.99127 59.679043,207.76977 53.370467,205.66727 z M 286.3585,134.90933 c -16.39324,-4.75014 -29.32495,-29.97227 -26.30291,-51.301407 2.47678,-17.480694 12.49975,-27.621562 27.27732,-27.598154 23.44621,0.03714 40.59738,31.74999 32.21918,59.573871 -4.65394,15.45567 -18.57608,23.5613 -33.19359,19.32569 z m -123.4487,-23.2191 c -10.73876,-4.02211 -18.3823,-15.203079 -21.48735,-31.431676 -4.60026,-24.043396 3.1508,-47.582045 18.19894,-55.267068 7.13808,-3.64539 17.09219,-3.541645 23.5341,0.245281 4.81488,2.830469 9.31944,7.818493 12.27299,13.590231 l 1.70251,3.326998 2.68451,-3.490673 c 7.94555,-10.33164 20.80498,-12.551286 30.37613,-5.243174 15.61603,11.923728 17.20366,48.030713 2.74755,62.486821 -9.46114,9.46114 -22.25377,9.28757 -31.08911,-0.421812 C 200.10578,93.568321 198.38429,92 198.02453,92 c -0.35976,0 -1.45137,1.671884 -2.42581,3.715298 -2.78156,5.833012 -7.39457,11.080752 -12.23303,13.916282 -5.37156,3.14794 -14.96879,4.11379 -20.45589,2.05865 z |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python2 | |
import sys | |
import re | |
if len(sys.argv) < 2: | |
print "Usage: %s \"path string\"" % sys.argv[0] | |
sys.exit() | |
path = re.split(r"([+-]?\ *\d+(?:\.\d*)?|\.\d+)", sys.argv[1]) | |
# (?:...)non-capturing version of regular parentheses | |
# because re.split() If capturing parentheses are used in pattern, then the text of all groups in the pattern are also returned as part of the resulting list | |
# Number of expected values per commands | |
cmd = {'M':2, 'L':2, 'Z':0, 'H':1, 'V':1, 'A':7, 'Q':4, 'T':2, 'C':6, 'S':4} | |
# 'pair' : (x,y) | |
# 'coord' : x | |
# 'length' : L > 0 | |
# 'bool' : 0|1 | |
syntax = { | |
'M': ['pair'], | |
'L': ['pair'], | |
'Z': [], | |
'H': ['coord'], | |
'V': ['coord'], | |
'A': ['length', 'length', 'length', 'bool', 'bool', 'pair'], | |
'Q': ['pair', 'pair'], | |
'T': ['pair'], | |
'C': ['pair', 'pair', 'pair'], | |
'S': ['pair', 'pair'] | |
} | |
# clean-up path in p[] | |
p = [] | |
for elt in path: | |
# remove all spaces | |
elt = elt.strip() | |
elt = elt.replace(' ','') | |
if (elt == ''): | |
continue; | |
# remove all commas (not strictly necessary in SVG) | |
if (elt == ','): | |
continue; | |
# split commands into single one (e.g.: 'ZM' -> 'Z','M') | |
# check command validity | |
if elt.isalpha(): | |
for i in list(elt): | |
if (i.upper() in cmd.keys()): p.append(i) | |
else: print i, " is not a valid command" | |
# not a command? should be a numeric | |
else: | |
try: p.append(float(elt)) | |
except ValueError: print elt, " should be numeric" | |
# Split series of identical commands into individual blocks | |
i = 1 | |
elt = p[0] # current element | |
c = 'M' # Current command | |
while i <= len(p): | |
# Expect a command, remember it | |
if str(elt).upper() in cmd.keys(): | |
c = elt | |
l = [c] | |
for j in range(0, cmd[c.upper()]): | |
l.append(p[i+j]) | |
i += cmd[c.upper()] | |
# Get next element | |
try: elt = p[i] | |
except: elt = '' # End of list? | |
i += 1 | |
print l | |
else: | |
# We expect a new command but did not get one: use previous one and realign | |
elt = c | |
i -= 1 | |
# Change lower case command to upper case (relative to absolute) | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import re | |
import numbers, math | |
import xml.etree.ElementTree as etree | |
class Transformable: | |
'''Abstract class for objects that can be geometrically drawn & transformed''' | |
def __init__(self, elt=None): | |
# a 'Transformable' is represented as a list of Transformable items | |
self.items = [] | |
# Unit transformation matrix on init | |
self.matrix = Matrix() | |
self.xmin = None | |
self.xmax = None | |
self.ymin = None | |
self.ymax = None | |
if elt is not None: | |
# Parse transform attibute to update self.matrix | |
self.getTransformations(elt) | |
def bbox(self): | |
'''Bounding box''' | |
for x in self.items: | |
pmin, pmax = x.bbox() | |
if self.xmin == None or pmin.x < self.xmin: | |
self.xmin = pmin.x | |
if self.ymin == None or pmin.y < self.ymin: | |
self.ymin = pmin.y | |
if self.xmax == None or pmax.x > self.xmax: | |
self.xmax = pmax.x | |
if self.ymax == None or pmax.y > self.ymax: | |
self.ymax = pmax.y | |
return (Point(self.xmin,self.ymin), Point(self.xmax,self.ymax)) | |
# Parse transform field | |
def getTransformations(self, elt): | |
t = elt.get('transform') | |
if t is None: return | |
svg_transforms = [ | |
'matrix', 'translate', 'scale', 'rotate', 'skewX', 'skewY'] | |
# match any SVG transformation with its parameter (until final parenthese) | |
# [^)]* == anything but a closing parenthese | |
# '|'.join == OR-list of SVG transformations | |
transforms = re.findall( | |
'|'.join([x + '[^)]*\)' for x in svg_transforms]), t) | |
for t in transforms: | |
op, arg = t.split('(') | |
op = op.strip() | |
# Keep only numbers | |
arg = [float(x) for x in | |
re.findall(r"([+-]?\ *\d+(?:\.\d*)?|\.\d+)", arg)] | |
print('transform: ' + op + ' '+ str(arg)) | |
if op == 'matrix': | |
self.matrix *= Matrix(arg) | |
if op == 'translate': | |
tx, ty = arg | |
self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) | |
if op == 'scale': | |
sx = arg[0] | |
if len(arg) == 1: sy = sx | |
else: sy = arg[1] | |
self.matrix *= Matrix([sx, 0, 0, sy, 0, 0]) | |
if op == 'rotate': | |
cosa = math.cos(math.radians(arg[0])) | |
sina = math.sin(math.radians(arg[0])) | |
if len(arg) != 1: | |
tx, ty = arg[1:3] | |
self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) | |
self.matrix *= Matrix([cosa, sina, -sina, cosa, 0, 0]) | |
if len(arg) != 1: | |
self.matrix *= Matrix([1, 0, 0, 1, -tx, -ty]) | |
if op == 'skewX': | |
tana = math.tan(math.radians(arg[0])) | |
self.matrix *= Matrix([1, 0, tana, 1, 0, 0]) | |
if op == 'skewY': | |
tana = math.tan(math.radians(arg[0])) | |
self.matrix *= Matrix([1, tana, 0, 1, 0, 0]) | |
def transform(self, matrix=None): | |
for x in self.items: | |
x.transform(self.matrix) | |
def scale(self, ratio): | |
for x in self.items: | |
x.scale(ratio) | |
return self | |
def translate(self, offset): | |
for x in self.items: | |
x.translate(offset) | |
return self | |
def rotate(self, angle): | |
for x in self.items: | |
x.rotate(angle) | |
return self | |
class Svg(Transformable): | |
'''SVG class: use parse to parse a file''' | |
def __init__(self, filename=None): | |
Transformable.__init__(self) | |
if filename: | |
self.parse(filename) | |
def parse(self, filename): | |
self.filename = filename | |
tree = etree.parse(filename) | |
self.root = tree.getroot() | |
if self.root.tag[-3:] != 'svg': | |
raise TypeError('file %s does not seem to be a valid SVG file', filename) | |
self.ns = self.root.tag[:-3] | |
# Parse XML elements hierarchically with groups <g> | |
self.addGroup(self.items, self.root) | |
self.transform() | |
# Flatten XML tree into a one dimension list | |
self.flatten() | |
def addGroup(self, group, element): | |
for elt in element: | |
if elt.tag == self.ns + 'g': | |
g = Group(elt) | |
# Append to parent group before looking for child elements | |
# because Group.append() applies transformations | |
# We need to record transformation to propagate to children | |
group.append(g) | |
self.addGroup(g, elt) | |
elif elt.tag == self.ns + 'path': | |
group.append(Path(elt)) | |
elif elt.tag == self.ns + 'circle': | |
group.append(Circle(elt)) | |
else: | |
print('Unsupported element: ' + elt.tag) | |
#group.append(elt.tag[len(self.ns):]) | |
def flatten(self): | |
self.drawing = [] | |
for i in self.items: | |
if isinstance(i, Group): | |
self.drawing += i.flatten() | |
else: | |
self.drawing.append(i) | |
def title(self): | |
t = self.root.find(self.ns + 'title') | |
if t is not None: | |
return t | |
else: | |
return self.filename.split('.')[0] | |
class Group(Transformable): | |
'''Handle svg <g> elements''' | |
def __init__(self, elt=None): | |
Transformable.__init__(self, elt) | |
if elt is not None: | |
self.ident = elt.get('id') | |
def append(self, item): | |
item.matrix = self.matrix * item.matrix | |
self.items.append(item) | |
def __repr__(self): | |
return 'Group id ' + self.ident + ':\n' + repr(self.items) + '\n' | |
def flatten(self): | |
ret = [] | |
for i in self.items: | |
if isinstance(i, Group): | |
ret += i.flatten() | |
else: | |
ret.append(i) | |
return ret | |
class Matrix: | |
''' SVG transformation matrix and its operations | |
a SVG matrix is represented as a list of 6 values [a, b, c, d, e, f] | |
(named vect hereafter) which represent the 3x3 matrix | |
((a, c, e) | |
(b, d, f) | |
(0, 0, 1)) | |
see http://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace ''' | |
def __init__(self, vect=[1, 0, 0, 1, 0, 0]): | |
# Unit transformation vect by default | |
if len(vect) != 6: | |
raise ValueError("Bad vect size %d" % len(vect)) | |
self.vect = list(vect) | |
def __mul__(self, other): | |
'''Matrix multiplication''' | |
if isinstance(other, Matrix): | |
a = self.vect[0] * other.vect[0] + self.vect[2] * other.vect[1] | |
b = self.vect[1] * other.vect[0] + self.vect[3] * other.vect[1] | |
c = self.vect[0] * other.vect[2] + self.vect[2] * other.vect[3] | |
d = self.vect[1] * other.vect[2] + self.vect[3] * other.vect[3] | |
e = self.vect[0] * other.vect[4] + self.vect[2] * other.vect[5] \ | |
+ self.vect[4] | |
f = self.vect[1] * other.vect[4] + self.vect[3] * other.vect[5] \ | |
+ self.vect[5] | |
return Matrix([a, b, c, d, e, f]) | |
elif isinstance(other, Point): | |
x = other.x * self.vect[0] + other.y * self.vect[2] + self.vect[4] | |
y = other.x * self.vect[1] + other.y * self.vect[3] + self.vect[5] | |
return Point(x,y) | |
else: | |
return NotImplemented | |
def __str__(self): | |
return str(self.vect) | |
COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' | |
class Path(Transformable): | |
'''SVG <path>''' | |
def __init__(self, elt=None): | |
Transformable.__init__(self, elt) | |
if elt is not None: | |
self.ident = elt.get('id') | |
self.style = elt.get('style') | |
self.parse(elt.get('d')) | |
def parse(self, pathstr): | |
"""Parse path string and build elements list""" | |
# (?:...) : non-capturing version of regular parentheses | |
pathlst = re.findall(r"([+-]?\ *\d+(?:\.\d*)?|\.\d+|\ *[%s]\ *)" | |
% COMMANDS, pathstr) | |
pathlst.reverse() | |
command = None | |
current_pt = Point(0,0) | |
start_pt = None | |
while pathlst: | |
if pathlst[-1].strip() in COMMANDS: | |
last_command = command | |
command = pathlst.pop().strip() | |
absolute = (command == command.upper()) | |
command = command.upper() | |
else: | |
if command is None: | |
raise ValueError("No command found at %d" % len(pathlst)) | |
if command == 'M': | |
# MoveTo | |
x = pathlst.pop() | |
y = pathlst.pop() | |
pt = Point(float(x), float(y)) | |
if absolute: | |
current_pt = pt | |
else: | |
current_pt += pt | |
start_pt = current_pt | |
self.items.append(MoveTo(current_pt)) | |
# MoveTo with multiple coordinates means LineTo | |
command = 'L' | |
elif command == 'Z': | |
# Close Path | |
l = Line(current_pt, start_pt) | |
self.items.append(l) | |
elif command in 'LHV': | |
# LineTo, Horizontal & Vertical line | |
# extra coord for H,V | |
if absolute: | |
x,y = current_pt.coord() | |
else: | |
x,y = (0,0) | |
if command in 'LH': | |
x = pathlst.pop() | |
if command in 'LV': | |
y = pathlst.pop() | |
pt = Point(float(x), float(y)) | |
if not absolute: | |
pt += current_pt | |
self.items.append(Line(current_pt, pt)) | |
current_pt = pt | |
elif command in 'CQ': | |
dimension = {'Q':3, 'C':4} | |
bezier_pts = [] | |
bezier_pts.append(current_pt) | |
for i in range(1,dimension[command]): | |
x = pathlst.pop() | |
y = pathlst.pop() | |
pt = Point(float(x), float(y)) | |
if not absolute: | |
pt += current_pt | |
bezier_pts.append(pt) | |
self.items.append(Bezier(bezier_pts)) | |
current_pt = pt | |
elif command in 'TS': | |
# number of points to read | |
nbpts = {'T':1, 'S':2} | |
# the control point, from previous Bezier to mirror | |
ctrlpt = {'T':1, 'S':2} | |
# last command control | |
last = {'T': 'QT', 'S':'CS'} | |
bezier_pts = [] | |
bezier_pts.append(current_pt) | |
if last_command in last[command]: | |
pt0 = self.items[-1].control_point(ctrlpt[command]) | |
else: | |
pt0 = current_pt | |
pt1 = current_pt | |
# Symetrical of pt1 against pt0 | |
bezier_pts.append(pt1 + pt1 - pt0) | |
for i in range(0,nbpts[command]): | |
x = pathlst.pop() | |
y = pathlst.pop() | |
pt = Point(float(x), float(y)) | |
if not absolute: | |
pt += current_pt | |
bezier_pts.append(pt) | |
self.items.append(Bezier(bezier_pts)) | |
current_pt = pt | |
elif command == 'A': | |
for i in range(0,7): | |
pathlst.pop() | |
# TODO | |
else: | |
pathlst.pop() | |
def __str__(self): | |
return '\n'.join(str(x) for x in self.items) | |
def __repr__(self): | |
return 'Path id ' + self.ident | |
def segments(self, precision=0): | |
'''Return a list of segments, each segment is ended by a MoveTo. | |
A segment is a list of Points''' | |
ret = [] | |
seg = [] | |
for x in self.items: | |
if isinstance(x, MoveTo): | |
if seg != []: | |
ret.append(seg) | |
seg = [] | |
else: | |
seg += x.segments(precision) | |
ret.append(seg) | |
return ret | |
def simplify(self, precision): | |
'''Simplify segment with precision: | |
Remove any point which are ~aligned''' | |
ret = [] | |
for seg in self.segments(precision): | |
ret.append(simplify_segment(seg, precision)) | |
return ret | |
class Point: | |
def __init__(self, x=0, y=0): | |
'''A Point is defined either by a tuple/list of length 2 or | |
by 2 coordinates''' | |
if (isinstance(x, tuple) or isinstance(x, list)) and len(x) == 2: | |
self.x = x[0] | |
self.y = x[1] | |
elif isinstance(x, numbers.Real) and isinstance(y, numbers.Real): | |
self.x = x | |
self.y = y | |
else: | |
raise TypeError("A Point is defined by 2 numbers or a tuple") | |
def __add__(self, other): | |
'''Add 2 points by adding coordinates. | |
Try to convert other to Point if necessary''' | |
if not isinstance(other, Point): | |
try: other = Point(other) | |
except: return NotImplemented | |
return Point(self.x + other.x, self.y + other.y) | |
def __sub__(self, other): | |
if not isinstance(other, Point): | |
try: other = Point(other) | |
except: return NotImplemented | |
return Point(self.x - other.x, self.y - other.y) | |
def __mul__(self, other): | |
if not isinstance(other, numbers.Real): | |
return NotImplemented | |
return Point(self.x * other, self.y * other) | |
def __rmul__(self, other): | |
return self.__mul__(other) | |
def __eq__(self, other): | |
if not isinstance(other, Point): | |
try: other = Point(other) | |
except: return NotImplemented | |
return (self.x == other.x) and (self.y == other.y) | |
def __repr__(self): | |
return '(' + format(self.x,'.3f') + ',' + format( self.y,'.3f') + ')' | |
def __str__(self): | |
return self.__repr__(); | |
def coord(self): | |
'''Return the point tuple (x,y)''' | |
return (self.x, self.y) | |
def length(self): | |
'''Vector length, Pythagoras theorem''' | |
return math.sqrt(self.x ** 2 + self.y ** 2) | |
def rot(self, angle): | |
'''Rotate vector [Origin,self] ''' | |
if not isinstance(angle, Angle): | |
try: angle = Angle(angle) | |
except: return NotImplemented | |
x = self.x * angle.cos - self.y * angle.sin | |
y = self.x * angle.sin + self.y * angle.cos | |
return Point(x,y) | |
class Angle: | |
'''Define a trigonometric angle [of a vector] ''' | |
def __init__(self, arg): | |
if isinstance(arg, numbers.Real): | |
# We precompute sin and cos for rotations | |
self.angle = arg | |
self.cos = math.cos(self.angle) | |
self.sin = math.sin(self.angle) | |
elif isinstance(arg, Point): | |
# Point angle is the trigonometric angle of the vector [origin, Point] | |
pt = arg | |
try: | |
self.cos = pt.x/pt.length() | |
self.sin = pt.y/pt.length() | |
except ZeroDivisionError: | |
self.cos = 1 | |
self.sin = 0 | |
self.angle = math.acos(self.cos) | |
if self.sin < 0: | |
self.angle = -self.angle | |
else: | |
raise TypeError("Angle is defined by a number or a Point") | |
def __neg__(self): | |
return Angle(Point(self.cos, -self.sin)) | |
class Line: | |
'''A line is an object defined by 2 points''' | |
def __init__(self, start, end): | |
self.start = start | |
self.end = end | |
def __str__(self): | |
return 'Line from ' + str(self.start) + ' to ' + str(self.end) | |
def segments(self, precision=0): | |
''' Line segments is simply the segment start -> end''' | |
return [self.start, self.end] | |
def length(self): | |
'''Line length, Pythagoras theorem''' | |
s = self.end - self.start | |
return math.sqrt(s.x ** 2 + s.y ** 2) | |
def pdistance(self, p): | |
'''Perpendicular distance between this Line and a given Point p''' | |
if not isinstance(p, Point): | |
return NotImplemented | |
if self.start == self.end: | |
# Distance from a Point to another Point is length of a line | |
return Line(self.start, p).length() | |
s = self.end - self.start | |
if s.x == 0: | |
# Vertical Line => pdistance is the difference of abscissa | |
return abs(self.start.x - p.x) | |
else: | |
# That's 2-D perpendicular distance formulae (ref: Wikipedia) | |
slope = s.y/s.x | |
# intercept: Crossing with ordinate y-axis | |
intercept = self.start.y - (slope * self.start.x) | |
return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1) | |
def bbox(self): | |
if self.start.x < self.end.x: | |
xmin = self.start.x | |
xmax = self.end.x | |
else: | |
xmin = self.end.x | |
xmax = self.start.x | |
if self.start.y < self.end.y: | |
ymin = self.start.y | |
ymax = self.end.y | |
else: | |
ymin = self.end.y | |
ymax = self.start.y | |
return (Point(xmin,ymin),Point(xmax,ymax)) | |
def transform(self, matrix): | |
self.start = matrix * self.start | |
self.end = matrix * self.end | |
def scale(self, ratio): | |
self.start *= ratio | |
self.end *= ratio | |
def translate(self, offset): | |
self.start += offset | |
self.end += offset | |
def rotate(self, angle): | |
self.start = self.start.rot(angle) | |
self.end = self.end.rot(angle) | |
class Bezier: | |
'''Bezier curve class | |
A Bezier curve is defined by its control points | |
Its dimension is equal to the number of control points | |
Note that SVG only support dimension 3 and 4 Bezier curve, respectively | |
Quadratic and Cubic Bezier curve''' | |
def __init__(self, pts): | |
self.pts = list(pts) | |
self.dimension = len(pts) | |
def __str__(self): | |
return 'Bezier' + str(self.dimension) + \ | |
' : ' + ", ".join([str(x) for x in self.pts]) | |
def control_point(self, n): | |
if n >= self.dimension: | |
raise LookupError('Index is larger than Bezier curve dimension') | |
else: | |
return self.pts[n] | |
def rlength(self): | |
'''Rough Bezier length: length of control point segments''' | |
pts = list(self.pts) | |
l = 0.0 | |
p1 = pts.pop() | |
while pts: | |
p2 = pts.pop() | |
l += Line(p1, p2).length() | |
p1 = p2 | |
return l | |
def bbox(self): | |
return self.rbbox() | |
def rbbox(self): | |
'''Rough bounding box: return the bounding box (P1,P2) of the Bezier | |
_control_ points''' | |
xmin = None | |
xmax = None | |
ymin = None | |
ymax = None | |
for pt in self.pts: | |
if xmin == None or pt.x < xmin: | |
xmin = pt.x | |
if ymin == None or pt.y < ymin: | |
ymin = pt.y | |
if xmax == None or pt.x > xmax: | |
xmax = pt.x | |
if ymax == None or pt.y > ymax: | |
ymax = pt.y | |
return (Point(xmin,ymin), Point(xmax,ymax)) | |
def segments(self, precision=0): | |
'''Return a polyline approximation ("segments") of the Bezier curve | |
precision is the minimum significative length of a segment''' | |
segments = [] | |
# n is the number of Bezier points to draw according to precision | |
if precision != 0: | |
n = int(self.rlength() / precision) + 1 | |
else: | |
n = 1000 | |
if n < 10: n = 10 | |
if n > 1000 : n = 1000 | |
for t in range(0, n): | |
segments.append(self._bezierN(float(t)/n)) | |
return segments | |
def _bezier1(self, p0, p1, t): | |
'''Bezier curve, one dimension | |
Compute the Point corresponding to a linear Bezier curve between | |
p0 and p1 at "time" t ''' | |
pt = p0 + t * (p1 - p0) | |
return pt | |
def _bezierN(self, t): | |
'''Bezier curve, Nth dimension | |
Compute the point of the Nth dimension Bezier curve at "time" t''' | |
# We reduce the N Bezier control points by computing the linear Bezier | |
# point of each control point segment, creating N-1 control points | |
# until we reach one single point | |
res = list(self.pts) | |
# We store the resulting Bezier points in res[], recursively | |
for n in range(self.dimension, 1, -1): | |
# For each control point of nth dimension, | |
# compute linear Bezier point a t | |
for i in range(0,n-1): | |
res[i] = self._bezier1(res[i], res[i+1], t) | |
return res[0] | |
def transform(self, matrix): | |
self.pts = [matrix * x for x in self.pts] | |
def scale(self, ratio): | |
self.pts = [x * ratio for x in self.pts] | |
def translate(self, offset): | |
self.pts = [x + offset for x in self.pts] | |
def rotate(self, angle): | |
self.pts = [x.rot(angle) for x in self.pts] | |
class MoveTo: | |
def __init__(self, dest): | |
self.dest = dest | |
def bbox(self): | |
return (self.dest, self.dest) | |
def transform(self, matrix): | |
self.dest = matrix * self.dest | |
def scale(self, ratio): | |
self.dest *= ratio | |
def translate(self, offset): | |
self.dest += offset | |
def rotate(self, angle): | |
self.dest = self.dest.rot(angle) | |
class Circle(Transformable): | |
'''SVG <circle>''' | |
def __init__(self, elt=None): | |
Transformable.__init__(self, elt) | |
if elt is not None: | |
self.center = Point(float(elt.get('cx')), float(elt.get('cy'))) | |
self.radius = float(elt.get('r')) | |
self.style = elt.get('style') | |
self.ident = elt.get('id') | |
def __repr__(self): | |
return 'circle id ' + self.ident | |
def bbox(self): | |
'''Bounding box''' | |
pmin = self.center - Point(self.radius, self.radius) | |
pmax = self.center + Point(self.radius, self.radius) | |
return (pmin, pmax) | |
def transform(self, matrix): | |
self.center = self.matrix * self.center | |
def scale(self, ratio): | |
self.center *= ratio | |
self.radius *= ratio | |
def translate(self, offset): | |
self.center += offset | |
def rotate(self, angle): | |
self.center = self.center.rot(angle) | |
def segments(self, precision=0): | |
return self | |
def simplify(self, precision): | |
return self | |
def simplify_segment(segment, epsilon): | |
'''Ramer-Douglas-Peucker algorithm''' | |
if len(segment) < 3 or epsilon <= 0: | |
return segment[:] | |
l = Line(segment[0], segment[-1]) # Longest line | |
# Find the furthest point from the line | |
maxDist = 0 | |
index = None | |
for i,p in enumerate(segment[1:]): | |
dist = l.pdistance(p) | |
if (dist > maxDist): | |
maxDist = dist | |
index = i+1 # enumerate starts at segment[1] | |
if maxDist > epsilon: | |
# Recursively call with segment splited in 2 on its furthest point | |
r1 = simplify_segment(segment[:index+1], epsilon) | |
r2 = simplify_segment(segment[index:], epsilon) | |
# Remove redundant 'middle' Point | |
return r1[:-1] + r2 | |
else: | |
return [segment[0], segment[-1]] | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys, os | |
import svg | |
import Image, ImageDraw | |
f = open(sys.argv[1]) | |
line = f.readline() | |
p = svg.Path(line) | |
im = Image.new("RGB", (800,800), "white") | |
draw = ImageDraw.Draw(im) | |
red = (255,0,0) | |
green = (0,255,0) | |
for l in p.segments(1): | |
draw.line([(1*x).coord() for x in l], fill=red) | |
#for l in p.simplify(5): | |
# draw.point([(1*x).coord() for x in l], fill=green) | |
draw.rectangle([pt.coord() for pt in p.bbox()], outline='blue') | |
#im.save(os.path.expanduser("~/public_html/bezier.png")) | |
im.show() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hello me,
It would be better to use Ramer–Douglas–Peucker algorithm for simplification.
See:
http://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
http://karthaus.nl/rdp/js/rdp.js
http://mourner.github.io/simplify-js/
https://github.com/omarestrella/simplify.py/blob/master/simplify.py