]> Some of my projects - openlase-old.git/commitdiff
Many svg2ild improvements
authorHector Martin <hector@marcansoft.com>
Wed, 8 Dec 2010 12:20:21 +0000 (13:20 +0100)
committerHector Martin <hector@marcansoft.com>
Wed, 8 Dec 2010 12:20:21 +0000 (13:20 +0100)
- 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

tools/svg2ild.py

index 75040b27df4959b85bee9a50bb28236aa3cbdc34..a8dc3c823020264bcdc43a3ac6918e233c8da787 100644 (file)
@@ -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))