Skip to content

Instantly share code, notes, and snippets.

@cjlano
Forked from anonymous/bezier.py.txt
Last active December 14, 2015 15:39
Show Gist options
  • Save cjlano/5109445 to your computer and use it in GitHub Desktop.
Save cjlano/5109445 to your computer and use it in GitHub Desktop.
plot bezier curve from SVG
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()
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
Display the source blob
Display the rendered blob
Raw
<?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>
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
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()
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
#!/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)
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]]
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()
@cjlano
Copy link
Author

cjlano commented Jul 8, 2013

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment