# -*- coding: utf-8 -*-
import os, sys, math
+import struct
import xml.sax, xml.sax.handler
import re
px,py = point
return (2*cx-px, 2*cy-py)
+ def angle(self, u, v):
+ # calculate the angle between two vectors
+ ux, uy = u
+ vx, vy = v
+ dot = ux*vx + uy*vy
+ ul = math.sqrt(ux**2 + uy**2)
+ vl = math.sqrt(vx**2 + vy**2)
+ a = math.acos(dot/(ul*vl))
+ return math.copysign(a, ux*vy - uy*vx)
+ def arc_eval(self, cx, cy, rx, ry, phi, w):
+ # evaluate a point on an arc
+ x = rx * math.cos(w)
+ y = ry * math.sin(w)
+ x, y = x*math.cos(phi) - y*math.sin(phi), math.sin(phi)*x + math.cos(phi)*y
+ x += cx
+ y += cy
+ return (x,y)
+ def arc_deriv(self, rx, ry, phi, w):
+ # evaluate the derivative of an arc
+ x = -rx * math.sin(w)
+ y = ry * math.cos(w)
+ x, y = x*math.cos(phi) - y*math.sin(phi), math.sin(phi)*x + math.cos(phi)*y
+ return (x,y)
+ def arc_to_beziers(self, cx, cy, rx, ry, phi, w1, dw):
+ # convert an SVG arc to 1-4 bezier segments
+ segcnt = min(4,int(abs(dw) / (math.pi/2) - 0.00000001) + 1)
+ beziers = []
+ for i in range(segcnt):
+ sw1 = w1 + dw / segcnt * i
+ sdw = dw / segcnt
+ p0 = self.arc_eval(cx, cy, rx, ry, phi, sw1)
+ p3 = self.arc_eval(cx, cy, rx, ry, phi, sw1+sdw)
+ a = math.sin(sdw)*(math.sqrt(4+3*math.tan(sdw/2)**2)-1)/3
+ d1 = self.arc_deriv(rx, ry, phi, sw1)
+ d2 = self.arc_deriv(rx, ry, phi, sw1+sdw)
+ p1 = p0[0] + a*d1[0], p0[1] + a*d1[1]
+ p2 = p3[0] - a*d2[0], p3[1] - a*d2[1]
+ beziers.append(PathBezier4(p0, p1, p2, p3))
+ return beziers
+ def svg_arc_to_beziers(self, start, end, rx, ry, phi, fa, fs):
+ # first convert endpoint format to center-and-radii format
+ rx, ry = abs(rx), abs(ry)
+ phi = phi % (2*math.pi)
+
+ x1, y1 = start
+ x2, y2 = end
+ x1p = (x1 - x2) / 2
+ y1p = (y1 - y2) / 2
+ psin = math.sin(phi)
+ pcos = math.cos(phi)
+ x1p, y1p = pcos*x1p + psin*y1p, -psin*x1p + pcos*y1p
+ foo = x1p**2 / rx**2 + y1p**2 / ry**2
+ sr = ((rx**2 * ry**2 - rx**2 * y1p**2 - ry**2 * x1p**2) /
+ (rx**2 * y1p**2 + ry**2 * x1p**2))
+ if foo > 1 or sr < 0:
+ #print "fixup!",foo,sr
+ rx = math.sqrt(foo)*rx
+ ry = math.sqrt(foo)*ry
+ sr = 0
+
+ srt = math.sqrt(sr)
+ if fa == fs:
+ srt = -srt
+ cxp, cyp = srt * rx * y1p / ry, -srt * ry * x1p / rx
+ cx, cy = pcos*cxp + -psin*cyp, psin*cxp + pcos*cyp
+ cx, cy = cx + (x1+x2)/2, cy + (y1+y2)/2
+
+ va = ((x1p - cxp)/rx, (y1p - cyp)/ry)
+ vb = ((-x1p - cxp)/rx, (-y1p - cyp)/ry)
+ w1 = self.angle((1,0), va)
+ dw = self.angle(va, vb) % (2*math.pi)
+ if not fs:
+ dw -= 2*math.pi
+
+ # then do the actual approximation
+ return self.arc_to_beziers(cx, cy, rx, ry, phi, w1, dw)
+
def parse(self, data):
ds = re.split(r"[ \r\n\t]*([-+]?\d+\.\d+[eE][+-]?\d+|[-+]?\d+\.\d+|[-+]?\.\d+[eE][+-]?\d+|[-+]?\.\d+|[-+]?\d+\.?[eE][+-]?\d+|[-+]?\d+\.?|[MmZzLlHhVvCcSsQqTtAa])[, \r\n\t]*", data)
tokens = ds[1::2]
end = self.popcoord(rel, cur)
subpath.add(PathBezier3(cur, cp, end))
cur = curcpc = end
- elif ucmd in 'Aa':
- raise ValueError("Arcs not implemented, biatch")
+ elif ucmd == 'A':
+ rx, ry = self.popcoord()
+ phi = self.popnum() / 180.0 * math.pi
+ fa = self.popnum() != 0
+ fs = self.popnum() != 0
+ end = self.popcoord(rel, cur)
+
+ if cur == end:
+ cur = curcpc = curcpq = end
+ continue
+ if rx == 0 or ry == 0:
+ subpath.add(PathLine(cur, end))
+ cur = curcpc = curcpq = end
+ continue
+
+ subpath.segments += self.svg_arc_to_beziers(cur, end, rx, ry, phi, fa, fs)
+ cur = curcpc = curcpq = end
if subpath.segments:
self.subpaths.append(subpath)
+class SVGPolyline(SVGPath):
+ def __init__(self, data=None, close=False):
+ self.subpaths = []
+ if data:
+ self.parse(data, close)
+ def parse(self, data, close=False):
+ ds = re.split(r"[ \r\n\t]*([-+]?\d+\.\d+[eE][+-]?\d+|[-+]?\d+\.\d+|[-+]?\.\d+[eE][+-]?\d+|[-+]?\.\d+|[-+]?\d+\.?[eE][+-]?\d+|[-+]?\d+\.?)[, \r\n\t]*", data)
+ tokens = ds[1::2]
+ if any(ds[::2]) or not all(ds[1::2]):
+ raise ValueError("Invalid SVG path expression: %r"%data)
+
+ self.tokens = tokens
+ cur = None
+ first = None
+ subpath = LaserPath()
+ while tokens:
+ pt = self.popcoord()
+ if first is None:
+ first = pt
+ if cur is not None:
+ subpath.add(PathLine(cur, pt))
+ cur = pt
+ if close:
+ subpath.add(PathLine(cur, first))
+ self.subpaths.append(subpath)
+
class SVGReader(xml.sax.handler.ContentHandler):
def doctype(self, name, pubid, system):
print name,pubid,system
def startDocument(self):
self.frame = LaserFrame()
self.matrix_stack = [(1,0,0,1,0,0)]
+ self.style_stack = []
+ self.defsdepth = 0
def endDocument(self):
self.frame.transform(self.tc)
def startElement(self, name, attrs):
if name == "svg":
- self.width = float(attrs['width'].replace("px",""))
- self.height = float(attrs['height'].replace("px",""))
+ self.dx = self.dy = 0
+ if 'viewBox' in attrs.keys():
+ self.dx, self.dy, self.width, self.height = map(float, attrs['viewBox'].split())
+ else:
+ ws = attrs['width']
+ hs = attrs['height']
+ for r in ('px','pt','mm','in','cm'):
+ hs = hs.replace(r,"")
+ ws = ws.replace(r,"")
+ self.width = float(ws)
+ self.height = float(hs)
elif name == "path":
if 'transform' in attrs.keys():
self.transform(attrs['transform'])
- self.addPath(attrs['d'])
+ if self.defsdepth == 0 and self.isvisible(attrs):
+ self.addPath(attrs['d'])
+ if 'transform' in attrs.keys():
+ self.popmatrix()
+ elif name in ("polyline","polygon"):
+ if 'transform' in attrs.keys():
+ self.transform(attrs['transform'])
+ if self.defsdepth == 0 and self.isvisible(attrs):
+ self.addPolyline(attrs['points'], name == "polygon")
+ if 'transform' in attrs.keys():
+ self.popmatrix()
+ elif name == "line":
+ if 'transform' in attrs.keys():
+ self.transform(attrs['transform'])
+ if self.defsdepth == 0 and self.isvisible(attrs):
+ x1, y1, x2, y2 = [float(attrs[x]) for x in ('x1','y1','x2','y2')]
+ self.addLine(x1, y1, x2, y2)
+ if 'transform' in attrs.keys():
+ self.popmatrix()
+ elif name == "rect":
+ if 'transform' in attrs.keys():
+ self.transform(attrs['transform'])
+ if self.defsdepth == 0 and self.isvisible(attrs):
+ x1, y1, w, h = [float(attrs[x]) for x in ('x','y','width','height')]
+ self.addRect(x1, y1, x1+w, y1+h)
if 'transform' in attrs.keys():
self.popmatrix()
elif name == 'g':
self.transform(attrs['transform'])
else:
self.pushmatrix((1,0,0,1,0,0))
+ if 'style' in attrs.keys():
+ self.style_stack.append(attrs['style'])
+ else:
+ self.style_stack.append("")
+ elif name in ('defs','clipPath'):
+ self.defsdepth += 1
def endElement(self, name):
if name == 'g':
self.popmatrix()
+ self.style_stack.pop()
+ elif name in ('defs','clipPath'):
+ self.defsdepth -= 1
def mmul(self, m1, m2):
a1,b1,c1,d1,e1,f1 = m1
a2,b2,c2,d2,e2,f2 = m2
def tc(self,coord):
vw = vh = max(self.width, self.height) / 2.0
x,y = coord
- x -= self.width / 2.0
- y -= self.height / 2.0
+ x -= self.width / 2.0 + self.dx
+ y -= self.height / 2.0 + self.dy
x = x / vw
y = y / vh
return (x,y)
return (nx,ny)
def transform(self, data):
ds = re.split(r"[ \r\n\t]*([a-z]+\([^)]+\)|,)[ \r\n\t]*", data)
- tokens = ds[1::2]
- if any(ds[::2]) or not all(ds[1::2]):
- raise ValueError("Invalid SVG transform expression: %r"%data)
- if not all([x == ',' for x in tokens[1::2]]):
- raise ValueError("Invalid SVG transform expression: %r"%data)
- transforms = tokens[::2]
+ tokens = []
+ for v in ds:
+ if v == ',':
+ continue
+ if v == '':
+ continue
+ if not re.match(r"[a-z]+\([^)]+\)", v):
+ raise ValueError("Invalid SVG transform expression: %r (%r)"%(data,v))
+ tokens.append(v)
+ transforms = tokens
mat = (1,0,0,1,0,0)
for t in transforms:
name,rest = t.split("(")
if rest[-1] != ")":
- raise ValueError("Invalid SVG transform expression: %r"%data)
+ raise ValueError("Invalid SVG transform expression: %r (%r)"%(data,rest))
args = map(float,rest[:-1].split(","))
if name == 'matrix':
mat = self.mmul(mat, args)
tx,ty = args
mat = self.mmul(mat, (1,0,0,1,tx,ty))
elif name == 'scale':
- sx,sy = args
+ if len(args) == 1:
+ sx,sy = args[0],args[0]
+ else:
+ sx,sy = args
mat = self.mmul(mat, (sx,0,0,sy,0,0))
elif name == 'rotate':
a = args[0] / 180.0 * math.pi
for path in p.subpaths:
path.transform(self.ts)
self.frame.add(path)
+ def addPolyline(self, data, close=False):
+ p = SVGPolyline(data, close)
+ for path in p.subpaths:
+ path.transform(self.ts)
+ self.frame.add(path)
+ def addLine(self, x1, y1, x2, y2):
+ path = LaserPath()
+ path.add(PathLine((x1,y1), (x2,y2)))
+ path.transform(self.ts)
+ self.frame.add(path)
+ def addRect(self, x1, y1, x2, y2):
+ path = LaserPath()
+ path.add(PathLine((x1,y1), (x2,y1)))
+ path.add(PathLine((x2,y1), (x2,y2)))
+ path.add(PathLine((x2,y2), (x1,y2)))
+ path.add(PathLine((x1,y2), (x1,y1)))
+ path.transform(self.ts)
+ self.frame.add(path)
+ def isvisible(self, attrs):
+ # skip elements with no stroke or fill
+ # hacky but gets rid of some gunk
+ style = ' '.join(self.style_stack)
+ if 'style' in attrs.keys():
+ style += " %s"%attrs['style']
+ if 'fill' in attrs.keys():
+ return True
+ style = re.sub(r'fill:\s*none\s*(;?)','', style)
+ style = re.sub(r'stroke:\s*none\s*(;?)','', style)
+ if 'stroke' not in style and 'fill' not in style:
+ return False
+ if re.match(r'display:\s*none', style):
+ return False
+ return True
-optimize = True
-params = RenderParameters()
-
-if sys.argv[1] == "-noopt":
- optimize = False
- sys.argv = [sys.argv[0]] + sys.argv[2:]
-
-if sys.argv[1] == "-cfg":
- params.load(sys.argv[2])
- sys.argv = [sys.argv[0]] + sys.argv[3:]
-
-handler = SVGReader()
-print "Parse"
-parser = xml.sax.make_parser()
-parser.setContentHandler(handler)
-parser.setFeature(xml.sax.handler.feature_external_ges, False)
-parser.parse(sys.argv[1])
-print "Parsed"
-
-frame = handler.frame
-#frame.showinfo()
-if optimize:
- frame.sort()
-
-print "Render"
-rframe = frame.render(params)
-print "Done"
-
-import struct
-
-for i,sample in enumerate(rframe):
- if sample.on:
- rframe = rframe[:i] + [sample]*params.extra_first_dwell + rframe[i+1:]
- break
-
-fout = open(sys.argv[2], "wb")
-
-dout = struct.pack(">4s3xB8s8sHHHBx", "ILDA", 1, "svg2ilda", "", len(rframe), 1, 1, 0)
-for i,sample in enumerate(rframe):
- x,y = sample.coord
- mode = 0
- if i == len(rframe):
- mode |= 0x80
- if params.invert:
- sample.on = not sample.on
- if params.force:
- sample.on = True
- if not sample.on:
- mode |= 0x40
- if abs(x) > params.width :
- raise ValueError("X out of bounds")
- if abs(y) > params.height :
- raise ValueError("Y out of bounds")
- dout += struct.pack(">hhBB",x,-y,mode,0x00)
-
-frame_time = len(rframe) / float(params.rate)
-
-if (frame_time*2) < params.time:
- count = int(params.time / frame_time)
- dout = dout * count
-
-fout.write(dout)
-
-print "Statistics:"
-print " Objects: %d"%params.objects
-print " Subpaths: %d"%params.subpaths
-print " Bezier subdivisions:"
-print " Due to rate: %d"%params.rate_divs
-print " Due to flatness: %d"%params.flatness_divs
-print " Points: %d"%params.points
-print " Trip: %d"%params.points_trip
-print " Line: %d"%params.points_line
-print " Bezier: %d"%params.points_bezier
-print " Start dwell: %d"%params.points_dwell_start
-print " Curve dwell: %d"%params.points_dwell_curve
-print " Corner dwell: %d"%params.points_dwell_corner
-print " End dwell: %d"%params.points_dwell_end
-print " Switch dwell: %d"%params.points_dwell_switch
-print " Total on: %d"%params.points_on
-print " Total off: %d"%(params.points - params.points_on)
-print " Efficiency: %.3f"%(params.points_on/float(params.points))
-print " Framerate: %.3f"%(params.rate/float(params.points))
+def load_svg(path):
+ handler = SVGReader()
+ parser = xml.sax.make_parser()
+ parser.setContentHandler(handler)
+ parser.setFeature(xml.sax.handler.feature_external_ges, False)
+ parser.parse(path)
+ return handler.frame
+
+def write_ild(params, rframe, path):
+ min_x = min_y = max_x = max_y = None
+ for i,sample in enumerate(rframe):
+ x,y = sample.coord
+ if min_x is None or min_x > x:
+ min_x = x
+ if min_y is None or min_y > y:
+ min_y = y
+ if max_x is None or max_x < x:
+ max_x = x
+ if max_y is None or max_y < y:
+ max_y = y
+
+ for i,sample in enumerate(rframe):
+ if sample.on:
+ rframe = rframe[:i] + [sample]*params.extra_first_dwell + rframe[i+1:]
+ break
+
+ if len(rframe) == 0:
+ raise ValueError("No points rendered")
+
+ # center image
+ offx = -(min_x + max_x)/2
+ offy = -(min_y + max_y)/2
+ width = max_x - min_x
+ height = max_y - min_y
+ scale = 1
+
+ if width > 65534 or height > 65534:
+ smax = max(width, height)
+ scale = 65534.0/smax
+ print "Scaling to %.02f%% due to overflow"%(scale*100)
+
+ if len(rframe) >= 65535:
+ raise ValueError("Too many points (%d, max 65535)"%len(rframe))
+
+ fout = open(path, "wb")
+
+ dout = struct.pack(">4s3xB8s8sHHHBx", "ILDA", 1, "svg2ilda", "", len(rframe), 1, 1, 0)
+ for i,sample in enumerate(rframe):
+ x,y = sample.coord
+ x += offx
+ y += offy
+ x *= scale
+ y *= scale
+ x = int(x)
+ y = int(y)
+ mode = 0
+ if i == len(rframe):
+ mode |= 0x80
+ if params.invert:
+ sample.on = not sample.on
+ if params.force:
+ sample.on = True
+ if not sample.on:
+ mode |= 0x40
+ if abs(x) > 32767:
+ raise ValueError("X out of bounds: %d"%x)
+ if abs(y) > 32767:
+ raise ValueError("Y out of bounds: %d"%y)
+ dout += struct.pack(">hhBB",x,-y,mode,0x00)
+
+ frame_time = len(rframe) / float(params.rate)
+
+ if (frame_time*2) < params.time:
+ count = int(params.time / frame_time)
+ dout = dout * count
+
+ fout.write(dout)
+ fout.close()
+
+if __name__ == "__main__":
+ optimize = True
+ params = RenderParameters()
+
+ if sys.argv[1] == "-noopt":
+ optimize = False
+ sys.argv = [sys.argv[0]] + sys.argv[2:]
+
+ if sys.argv[1] == "-cfg":
+ params.load(sys.argv[2])
+ sys.argv = [sys.argv[0]] + sys.argv[3:]
+
+ print "Parse"
+ frame = load_svg(sys.argv[1])
+ print "Done"
+
+ if optimize:
+ frame.sort()
+
+ print "Render"
+ rframe = frame.render(params)
+ print "Done"
+
+ write_ild(params, rframe, sys.argv[2])
+
+ print "Statistics:"
+ print " Objects: %d"%params.objects
+ print " Subpaths: %d"%params.subpaths
+ print " Bezier subdivisions:"
+ print " Due to rate: %d"%params.rate_divs
+ print " Due to flatness: %d"%params.flatness_divs
+ print " Points: %d"%params.points
+ print " Trip: %d"%params.points_trip
+ print " Line: %d"%params.points_line
+ print " Bezier: %d"%params.points_bezier
+ print " Start dwell: %d"%params.points_dwell_start
+ print " Curve dwell: %d"%params.points_dwell_curve
+ print " Corner dwell: %d"%params.points_dwell_corner
+ print " End dwell: %d"%params.points_dwell_end
+ print " Switch dwell: %d"%params.points_dwell_switch
+ print " Total on: %d"%params.points_on
+ print " Total off: %d"%(params.points - params.points_on)
+ print " Efficiency: %.3f"%(params.points_on/float(params.points))
+ print " Framerate: %.3f"%(params.rate/float(params.points))