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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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