From: Hector Martin Date: Wed, 8 Dec 2010 12:20:21 +0000 (+0100) Subject: Many svg2ild improvements X-Git-Url: https://gitweb.tyo.aptx.org/?a=commitdiff_plain;h=061700118bd553080ca5d3e376d31ae3280fffa6;p=openlase.git Many svg2ild improvements - Modularized, can be imported now - Added lots of SVG primitives that were missing - Support elliptical arcs - Added basic invisible object detection - Auto centering and scaling down if required - Support viewBox properly --- diff --git a/tools/svg2ild.py b/tools/svg2ild.py index 75040b2..a8dc3c8 100644 --- a/tools/svg2ild.py +++ b/tools/svg2ild.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- import os, sys, math +import struct import xml.sax, xml.sax.handler import re @@ -399,6 +400,83 @@ class SVGPath(object): 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] @@ -475,28 +553,104 @@ class SVGPath(object): 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': @@ -504,9 +658,18 @@ class SVGReader(xml.sax.handler.ContentHandler): 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 @@ -525,8 +688,8 @@ class SVGReader(xml.sax.handler.ContentHandler): 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) @@ -538,18 +701,22 @@ class SVGReader(xml.sax.handler.ContentHandler): 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) @@ -557,7 +724,10 @@ class SVGReader(xml.sax.handler.ContentHandler): 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 @@ -581,86 +751,160 @@ class SVGReader(xml.sax.handler.ContentHandler): 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))