diff --git a/projectlayer.py b/projectlayer.py index a174c33..3e2f2a4 100644 --- a/projectlayer.py +++ b/projectlayer.py @@ -15,41 +15,62 @@ import xml.etree.ElementTree import wx -import time +import svg.document as wxpsvgdocument def parsesvg(name): et= xml.etree.ElementTree.ElementTree(file=name) + #xml.etree.ElementTree.dump(et) + slicer = 'Slic3r' if et.getroot().find('{http://www.w3.org/2000/svg}metadata') == None else 'Skeinforge' zlast=0 zdiff=0 ol=[] - for i in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"): - z=float(i.get('id').split("z:")[-1]) - zdiff=z-zlast - zlast=z - path=i.find('{http://www.w3.org/2000/svg}path') - ol+=[(path.get("d").split("z"))[:-1]] - return ol,zdiff + if (slicer == 'Slic3r'): + height = et.getroot().get('height') + width = et.getroot().get('width') + + for i in et.findall("{http://www.w3.org/2000/svg}g"): + z=float(i.get('{http://slic3r.org/namespaces/slic3r}z')) + zdiff=z-zlast + zlast=z + + svgSnippet = xml.etree.ElementTree.Element('{http://www.w3.org/2000/svg}svg') + svgSnippet.set('height', height+'mm') + svgSnippet.set('width', width+'mm') + svgSnippet.set('viewBox', '0 0 '+height+' '+width) + svgSnippet.append(i) + + ol+=[svgSnippet] + else : + for i in et.findall("{http://www.w3.org/2000/svg}g")[0].findall("{http://www.w3.org/2000/svg}g"): + z=float(i.get('id').split("z:")[-1]) + zdiff=z-zlast + zlast=z + path=i.find('{http://www.w3.org/2000/svg}path') + ol+=[(path.get("d").split("z"))[:-1]] + return ol,zdiff,slicer class dispframe(wx.Frame): - def __init__(self, parent, title, res=(1600,1200),printer=None): + def __init__(self, parent, title, res=(800,600),printer=None): wx.Frame.__init__(self, parent=parent, title=title) self.p=printer self.pic=wx.StaticBitmap(self) self.bitmap=wx.EmptyBitmap(*res) self.bbitmap=wx.EmptyBitmap(*res) + self.slicer='Skeinforge' dc=wx.MemoryDC() dc.SelectObject(self.bbitmap) dc.SetBackground(wx.Brush("black")) dc.Clear() dc.SelectObject(wx.NullBitmap) - + self.SetBackgroundColour("black") self.pic.Hide() self.pen=wx.Pen("white") self.brush=wx.Brush("white") self.SetDoubleBuffered(True) self.Show() + def drawlayer(self,svg): try: dc=wx.MemoryDC() @@ -58,18 +79,22 @@ class dispframe(wx.Frame): dc.Clear() dc.SetPen(self.pen) dc.SetBrush(self.brush) - for i in svg: - #print i - points=[wx.Point(*map(lambda x:int(round(float(x)*self.scale)),j.strip().split())) for j in i.strip().split("M")[1].split("L")] - dc.DrawPolygon(points,self.size[0]/2,self.size[1]/2) - - - dc.SelectObject(wx.NullBitmap) + + if self.slicer == 'Skeinforge': + for i in svg: + #print i + points=[wx.Point(*map(lambda x:int(round(float(x)*self.scale)),j.strip().split())) for j in i.strip().split("M")[1].split("L")] + dc.DrawPolygon(points,self.size[0]/2,self.size[1]/2) + else: + gc = wx.GraphicsContext_Create(dc) + gc.Translate(*self.offset) + gc.Scale(self.scale, self.scale) + wxpsvgdocument.SVGDocument(svg).render(gc) + self.pic.SetBitmap(self.bitmap) self.pic.Show() self.Refresh() - #self.pic.SetBitmap(self.bitmap) except: raise @@ -79,19 +104,18 @@ class dispframe(wx.Frame): self.drawlayer(image) self.pic.Show() self.Refresh() - # time.sleep(self.interval) - #self.pic.Hide() + self.Refresh() - if self.p!=None and self.p.online: + if self.p != None and self.p.online: self.p.send_now("G91") self.p.send_now("G1 Z%f F300"%(self.thickness,)) self.p.send_now("G90") def nextimg(self,event): - #print "b" + if self.index + + + Example triangle01- simple example of a 'path' + A path that draws a triangle + + +""" + +makePath = lambda: wx.GraphicsRenderer_GetDefaultRenderer().CreatePath() + +def attrAsFloat(node, attr, defaultValue="0"): + val = node.get(attr, defaultValue) + #TODO: process stuff like "inherit" by walking back up the nodes + #fast path optimization - if it's a valid float, don't + #try to parse it. + try: + return float(val) + except ValueError: + return valueToPixels(val) + +def valueToPixels(val, defaultUnits="px"): + #TODO manage default units + from pyparsing import ParseException + try: + val, unit = values.length.parseString(val) + except ParseException: + print "***", val + raise + #todo: unit conversion to something other than pixels + return val + + +def pathHandler(func): + """decorator for methods which return a path operation + Creates the path they will fill, + and generates the path operations for the node + """ + @wraps(func) + def inner(self, node): + #brush = self.getBrushFromState() + #pen = self.getPenFromState() + #if not (brush or pen): + # return None, [] + path = wx.GraphicsRenderer_GetDefaultRenderer().CreatePath() + func(self, node, path) + ops = self.generatePathOps(path) + return path, ops + return inner + + +class SVGDocument(object): + lastControl = None + brushCache = {} + penCache = {} + def __init__(self, element): + """ + Create an SVG document from an ElementTree node. + """ + self.handlers = { + '{http://www.w3.org/2000/svg}svg':self.addGroupToDocument, + '{http://www.w3.org/2000/svg}a':self.addGroupToDocument, + '{http://www.w3.org/2000/svg}g':self.addGroupToDocument, + '{http://www.w3.org/2000/svg}rect':self.addRectToDocument, + '{http://www.w3.org/2000/svg}circle': self.addCircleToDocument, + '{http://www.w3.org/2000/svg}ellipse': self.addEllipseToDocument, + '{http://www.w3.org/2000/svg}line': self.addLineToDocument, + '{http://www.w3.org/2000/svg}polyline': self.addPolyLineToDocument, + '{http://www.w3.org/2000/svg}polygon': self.addPolygonToDocument, + '{http://www.w3.org/2000/svg}path':self.addPathDataToDocument, + '{http://www.w3.org/2000/svg}text':self.addTextToDocument + } + + assert element.tag == '{http://www.w3.org/2000/svg}svg', 'Not an SVG fragment' + self.tree = element + self.paths = {} + self.stateStack = [{}] + path, ops = self.processElement(element) + self.ops = ops + + @property + def state(self): + """ Retrieve the current state, without popping""" + return self.stateStack[-1] + + def processElement(self, element): + """ Process one element of the XML tree. + Returns the path representing the node, + and an operation list for drawing the node. + + Parent nodes should return a path (for hittesting), but + no draw operations + """ + #copy the current state + current = dict(self.state) + current.update(element.items()) + current.update(css.inlineStyle(element.get("style", ""))) + self.stateStack.append(current) + handler = self.handlers.get(element.tag, lambda *any: (None, None)) + path, ops = handler(element) + self.paths[element] = path + self.stateStack.pop() + return path, ops + + def createTransformOpsFromNode(self, node): + """ Returns an oplist for transformations. + This applies to a node, not the current state because + the transform stack is saved in the wxGraphicsContext. + + This oplist does *not* include the push/pop state commands + """ + ops = [] + transform = node.get('transform') + #todo: replace this with a mapping list + if transform: + for transform, args in css.transformList.parseString(transform): + if transform == 'scale': + if len(args) == 1: + x = y = args[0] + else: + x, y = args + ops.append( + (wx.GraphicsContext.Scale, (x, y)) + ) + if transform == 'translate': + if len(args) == 1: + x = args[0] + y = 0 + else: + x, y = args + ops.append( + (wx.GraphicsContext.Translate, (x, y)) + ) + if transform == 'rotate': + if len(args) == 3: + angle, cx, cy = args + angle = math.radians(angle) + ops.extend([ + (wx.GraphicsContext.Translate, (cx, cy)), + (wx.GraphicsContext.Rotate, (angle,)), + (wx.GraphicsContext.Translate, (-cx, -cy)), + ]) + else: + angle = args[0] + angle = math.radians(angle) + ops.append( + (wx.GraphicsContext.Rotate, (angle,)) + ) + if transform == 'matrix': + matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix( + *args + ) + ops.append( + (wx.GraphicsContext.ConcatTransform, (matrix,)) + ) + if transform == 'skewX': + matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix( + 1,0,math.tan(math.radians(args[0])),1,0,0 + ) + ops.append( + (wx.GraphicsContext.ConcatTransform, (matrix,)) + ) + if transform == 'skewY': + matrix = wx.GraphicsRenderer_GetDefaultRenderer().CreateMatrix( + 1,math.tan(math.radians(args[0])),0,1,0,0 + ) + ops.append( + (wx.GraphicsContext.ConcatTransform, (matrix,)) + ) + return ops + + + def addGroupToDocument(self, node): + """ For parent elements: push on a state, + then process all child elements + """ + ops = [ + (wx.GraphicsContext.PushState, ()) + ] + + path = makePath() + ops.extend(self.createTransformOpsFromNode(node)) + for child in node.getchildren(): + cpath, cops = self.processElement(child) + if cpath: + path.AddPath(cpath) + if cops: + ops.extend(cops) + ops.append( + (wx.GraphicsContext.PopState, ()) + ) + return path, ops + + def getFontFromState(self): + font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT) + family = self.state.get("font-family") + if False:#family: + #print "setting font", family + font.SetFaceName(family) + size = self.state.get("font-size") + #I'm not sure if this is right or not + if size: + val, unit = values.length.parseString(size) + if '__WXMSW__' in wx.PlatformInfo: + i = int(val) + font.SetPixelSize((i, i)) + else: + font.SetPointSize(int(val)) + return font + + def addTextToDocument(self, node): + x, y = [attrAsFloat(node, attr) for attr in ('x', 'y')] + + def DoDrawText(context, text, x, y, brush=wx.NullGraphicsBrush): + #SVG spec appears to originate text from the bottom + #rather than the top as with our API. This function + #will measure and then re-orient the text as needed. + w, h = context.GetTextExtent(text) + y -= h + context.DrawText(text, x, y, brush) + font = self.getFontFromState() + brush = self.getBrushFromState() + if not (brush and brush.IsOk()): + brush = wx.BLACK_BRUSH + #normalize whitespace + #TODO: SVG probably has rules for this + text = ' '.join(node.text.split() if node.text else "") + if text is None: + return None, [] + ops = [ + (wx.GraphicsContext.SetFont, (font, brush.Colour)), + (DoDrawText, (text, x, y)) + ] + return None, ops + + @pathHandler + def addRectToDocument(self, node, path): + x, y, w, h = (attrAsFloat(node, attr) for attr in ['x', 'y', 'width', 'height']) + rx = node.get('rx') + ry = node.get('ry') + if not (w and h): + path.MoveToPoint(x,y) #keep the current point correct + return + if rx or ry: + if rx and ry: + rx, ry = float(rx), float(ry) + elif rx: + rx = ry = float(rx) + elif ry: + rx = ry = float(ry) + #value clamping as per spec section 9.2 + rx = min(rx, w/2) + ry = min(ry, h/2) + + #origin + path.MoveToPoint(x+rx, y) + path.AddLineToPoint(x+w-rx, y) + #top right + cx = rx * 2 + cy = ry * 2 + path.AddEllipticalArc( + x+w-cx, y, + cx, cy, + math.radians(270), math.radians(0), + True + ) + path.AddLineToPoint(x+w, y+h-ry) + #bottom right + path.AddEllipticalArc( + x+w-cx, y+h-cy, + cx, cy, + math.radians(0), math.radians(90), + True + ) + path.AddLineToPoint(x+rx, y+h) + #bottom left + path.AddEllipticalArc( + x, y+h-cy, + cx, cy, + math.radians(90), + math.radians(180), + True + ) + path.AddLineToPoint(x, y+ry) + #bottom right + path.AddEllipticalArc( + x, y, + cx, cy, + math.radians(180), + math.radians(270), + True + ) + path.CloseSubpath() + else: + path.AddRectangle( + x, y, w, h + ) + + @pathHandler + def addCircleToDocument(self, node, path): + cx, cy, r = [attrAsFloat(node, attr) for attr in ('cx', 'cy', 'r')] + path.AddCircle(cx, cy, r) + + @pathHandler + def addEllipseToDocument(self, node, path): + cx, cy, rx, ry = [float(node.get(attr, 0)) for attr in ('cx', 'cy', 'rx', 'ry')] + #cx, cy are centerpoint. + #rx, ry are radius. + #wxGC coords are the rect of the ellipse bounding box. + if rx <= 0 or ry <= 0: + return + x = cx - rx + y = cy - ry + path.AddEllipse(x, y, rx*2, ry*2) + + @pathHandler + def addLineToDocument(self, node, path): + x1, y1, x2, y2 = [attrAsFloat(node, attr) for attr in ('x1', 'y1', 'x2', 'y2')] + path.MoveToPoint(x1, y1) + path.AddLineToPoint(x2, y2) + + @pathHandler + def addPolyLineToDocument(self, node, path): + #translate to pathdata and render that + data = "M " + node.get("points") + self.addPathDataToPath(data, path) + + @pathHandler + def addPolygonToDocument(self, node, path): + #translate to pathdata and render that + data = "M " + node.get("points") + " Z" + self.addPathDataToPath(data, path) + + @pathHandler + def addPathDataToDocument(self, node, path): + self.addPathDataToPath(node.get('d', ''), path) + + def addPathDataToPath(self, data, path): + self.lastControl = None + self.lastControlQ = None + self.firstPoints = [] + def normalizeStrokes(parseResults): + """ The data comes from the parser in the + form of (command, [list of arguments]). + We translate that to [(command, args[0]), (command, args[1])] + via a generator. + + M is special cased because its subsequent arguments + become linetos. + """ + for command, arguments in parseResults: + if not arguments: + yield (command, ()) + else: + arguments = iter(arguments) + if command == 'm': + yield (command, arguments.next()) + command = "l" + elif command == "M": + yield (command, arguments.next()) + command = "L" + for arg in arguments: + yield (command, arg) + #print "data length", len(data) + import time + t = time.time() + parsed = pathdata.svg.parseString(data) + #print "parsed in", time.time()-t + for stroke in normalizeStrokes(parsed): + self.addStrokeToPath(path, stroke) + + + def generatePathOps(self, path): + """ Look at the current state and generate the + draw operations (fill, stroke, neither) for the path""" + ops = [] + brush = self.getBrushFromState(path) + fillRule = self.state.get('fill-rule', 'nonzero') + frMap = {'nonzero':wx.WINDING_RULE, 'evenodd': wx.ODDEVEN_RULE} + fr = frMap.get(fillRule, wx.ODDEVEN_RULE) + if brush: + ops.append( + (wx.GraphicsContext.SetBrush, (brush,)) + ) + ops.append( + (wx.GraphicsContext.FillPath, (path, fr)) + ) + pen = self.getPenFromState() + if pen: + ops.append( + (wx.GraphicsContext.SetPen, (pen,)) + ) + ops.append( + (wx.GraphicsContext.StrokePath, (path,)) + ) + return ops + + def getPenFromState(self): + pencolour = self.state.get('stroke', 'none') + if pencolour == 'currentColor': + pencolour = self.state.get('color', 'none') + if pencolour == 'transparent': + return wx.TRANSPARENT_PEN + if pencolour == 'none': + return wx.NullPen + type, value = colourValue.parseString(pencolour) + if type == 'URL': + warnings.warn("Color servers for stroking not implemented") + return wx.NullPen + else: + if value[:3] == (-1, -1, -1): + return wx.NullPen + pen = wx.Pen(wx.Colour(*value)) + width = self.state.get('stroke-width') + if width: + width, units = values.length.parseString(width) + pen.SetWidth(width) + capmap = { + 'butt':wx.CAP_BUTT, + 'round':wx.CAP_ROUND, + 'square':wx.CAP_PROJECTING + } + joinmap = { + 'miter':wx.JOIN_MITER, + 'round':wx.JOIN_ROUND, + 'bevel':wx.JOIN_BEVEL + } + pen.SetCap(capmap.get(self.state.get('stroke-linecap', None), wx.CAP_BUTT)) + pen.SetJoin(joinmap.get(self.state.get('stroke-linejoin', None), wx.JOIN_MITER)) + return wx.GraphicsRenderer_GetDefaultRenderer().CreatePen(pen) + + def getBrushFromState(self, path=None): + brushcolour = self.state.get('fill', 'black').strip() + type, details = paintValue.parseString(brushcolour) + if type == "URL": + url, fallback = details + element = self.resolveURL(url) + if not element: + if fallback: + type, details = fallback + else: + r, g, b, = 0, 0, 0 + else: + if element.tag == '{http://www.w3.org/2000/svg}linearGradient': + box = path.GetBox() + x, y, w, h = box.Get() + return wx.GraphicsRenderer.GetDefaultRenderer().CreateLinearGradientBrush( + x,y,x+w,y+h,wx.Colour(0,0,255,128), wx.RED + ) + elif element.tag == '{http://www.w3.org/2000/svg}radialGradient': + box = path.GetBox() + x, y, w, h = box.Get() + #print w + mx = wx.GraphicsRenderer.GetDefaultRenderer().CreateMatrix(x,y,w,h) + cx, cy = mx.TransformPoint(0.5, 0.5) + fx, fy = cx, cy + return wx.GraphicsRenderer.GetDefaultRenderer().CreateRadialGradientBrush( + cx,cy, + fx,fy, + (max(w,h))/2, + wx.Colour(0,0,255,128), wx.RED + ) + else: + #invlid gradient specified + return wx.NullBrush + r,g,b = 0,0,0 + if type == 'CURRENTCOLOR': + type, details = paintValue.parseString(self.state.get('color', 'none')) + if type == 'RGB': + r,g,b = details + elif type == "NONE": + return wx.NullBrush + opacity = self.state.get('fill-opacity', self.state.get('opacity', '1')) + opacity = float(opacity) + opacity = min(max(opacity, 0.0), 1.0) + a = 255 * opacity + #using try/except block instead of + #just setdefault because the wxBrush and wxColour would + #be created every time anyway in order to pass them, + #defeating the purpose of the cache + try: + return SVGDocument.brushCache[(r,g,b,a)] + except KeyError: + return SVGDocument.brushCache.setdefault((r,g,b,a), wx.Brush(wx.Colour(r,g,b,a))) + + + def resolveURL(self, urlData): + """ + Resolve a URL and return an elementTree Element from it. + + Return None if unresolvable + + """ + scheme, netloc, path, query, fragment = urlData + if scheme == netloc == path == '': + #horrible. There's got to be a better way? + if self.tree.get("id") == fragment: + return self.tree + else: + for child in self.tree.getiterator(): + #print child.get("id") + if child.get("id") == fragment: + return child + return None + else: + return self.resolveRemoteURL(urlData) + + def resolveRemoteURL(self, url): + return None + + def addStrokeToPath(self, path, stroke): + """ Given a stroke from a path command + (in the form (command, arguments)) create the path + commands that represent it. + + TODO: break out into (yet another) class/module, + especially so we can get O(1) dispatch on type? + """ + type, arg = stroke + relative = False + if type == type.lower(): + relative = True + ox, oy = path.GetCurrentPoint().Get() + else: + ox = oy = 0 + def normalizePoint(arg): + x, y = arg + return x+ox, y+oy + def reflectPoint(point, relativeTo): + x, y = point + a, b = relativeTo + return ((a*2)-x), ((b*2)-y) + type = type.upper() + if type == 'M': + pt = normalizePoint(arg) + self.firstPoints.append(pt) + path.MoveToPoint(pt) + elif type == 'L': + path.AddLineToPoint(normalizePoint(arg)) + elif type == 'C': + #control1, control2, endpoint = arg + control1, control2, endpoint = map( + normalizePoint, arg + ) + self.lastControl = control2 + path.AddCurveToPoint( + control1, + control2, + endpoint + ) + #~ cp = path.GetCurrentPoint() + #~ path.AddCircle(c1x, c1y, 5) + #~ path.AddCircle(c2x, c2y, 3) + #~ path.AddCircle(x,y, 7) + #~ path.MoveToPoint(cp) + #~ print "C", control1, control2, endpoint + + elif type == 'S': + #control2, endpoint = arg + control2, endpoint = map( + normalizePoint, arg + ) + if self.lastControl: + control1 = reflectPoint(self.lastControl, path.GetCurrentPoint()) + else: + control1 = path.GetCurrentPoint() + #~ print "S", self.lastControl,":",control1, control2, endpoint + self.lastControl = control2 + path.AddCurveToPoint( + control1, + control2, + endpoint + ) + elif type == "Q": + (cx, cy), (x,y) = map(normalizePoint, arg) + self.lastControlQ = (cx, cy) + path.AddQuadCurveToPoint(cx, cy, x, y) + elif type == "T": + x, y, = normalizePoint(arg) + if self.lastControlQ: + cx, cy = reflectPoint(self.lastControlQ, path.GetCurrentPoint()) + else: + cx, cy = path.GetCurrentPoint() + self.lastControlQ = (cx, cy) + path.AddQuadCurveToPoint(cx, cy, x, y) + + elif type == "V": + _, y = normalizePoint((0, arg)) + x, _ = path.GetCurrentPoint() + path.AddLineToPoint(x,y) + + elif type == "H": + x, _ = normalizePoint((arg, 0)) + _, y = path.GetCurrentPoint() + path.AddLineToPoint(x,y) + + elif type == "A": + #wxGC currently only supports circular arcs, + #not eliptical ones + + ( + (rx, ry), #radii of ellipse + angle, #angle of rotation on the ellipse in degrees + (fa, fs), #arc and stroke angle flags + (x, y) #endpoint on the arc + ) = arg + + x, y = normalizePoint((x,y)) + cx, cy = path.GetCurrentPoint() + if (cx, cy) == (x, y): + return #noop + + if (rx == 0 or ry == 0): + #no radius is effectively a line + path.AddLineToPoint(x,y) + return + + #find the center point for the ellipse + #translation via the angle + angle = angle % 360 + angle = math.radians(angle) + + #translated endpoint + xPrime = math.cos(angle) * ((cx-x)/2) + xPrime += math.sin(angle) * ((cx-x)/2) + yPrime = -(math.sin(angle)) * ((cy-y)/2) + yPrime += (math.cos(angle)) * ((cy-y)/2) + + + temp = ((rx**2) * (ry**2)) - ((rx**2) * (yPrime**2)) - ((ry**2) * (xPrime**2)) + temp /= ((rx**2) * (yPrime**2)) + ((ry**2)*(xPrime**2)) + temp = abs(temp) + try: + temp = math.sqrt(temp) + except ValueError: + import pdb + pdb.set_trace() + cxPrime = temp * ((rx * yPrime) / ry) + cyPrime = temp * -((ry * xPrime) / rx) + if fa == fs: + cxPrime, cyPrime = -cxPrime, -cyPrime + + #reflect backwards now for the origin + cnx = math.cos(angle) * cxPrime + cnx += math.sin(angle) * cxPrime + cny = -(math.sin(angle)) * cyPrime + cny += (math.cos(angle)) * cyPrime + cnx += ((cx+x)/2.0) + cny += ((cy+y)/2.0) + + #calculate the angle between the two endpoints + lastArc = wx.Point2D(x-cnx, y-cny).GetVectorAngle() + firstArc = wx.Point2D(cx-cnx, cy-cny).GetVectorAngle() + lastArc = math.radians(lastArc) + firstArc = math.radians(firstArc) + + + #aargh buggines. + #AddArc draws a straight line between + #the endpoints of the arc. + #putting it in a subpath makes the strokes come out + #correctly, but it still only fills the slice + #taking out the MoveToPoint fills correctly. + path.AddEllipse(cnx-rx, cny-ry, rx*2, ry*2) + path.MoveToPoint(x, y) + #~ npath = makePath() + #~ npath.AddEllipticalArc(cnx-rx, cny-ry, rx*2, ry*2, firstArc, lastArc, False) + #~ npath.MoveToPoint(x,y) + #~ path.AddPath(npath) + + elif type == 'Z': + #~ Bugginess: + #~ CloseSubpath() doesn't change the + #~ current point, as SVG spec requires. + #~ However, manually moving to the endpoint afterward opens a new subpath + #~ and (apparently) messes with stroked but not filled paths. + #~ This is possibly a bug in GDI+? + #~ Manually closing the path via AddLineTo gives incorrect line join + #~ results + #~ Manually closing the path *and* calling CloseSubpath() appears + #~ to give correct results on win32 + + pt = self.firstPoints.pop() + path.AddLineToPoint(pt) + path.CloseSubpath() + + def render(self, context): + if not hasattr(self, "ops"): + return + for op, args in self.ops: + op(context, *args) + +if __name__ == '__main__': + from tests.test_document import * + unittest.main() diff --git a/svg/pathdata.py b/svg/pathdata.py new file mode 100644 index 0000000..59d9255 --- /dev/null +++ b/svg/pathdata.py @@ -0,0 +1,193 @@ +""" + SVG path data parser + + + Usage: + steps = svg.parseString(pathdata) + for command, arguments in steps: + pass + +""" + +from pyparsing import (ParserElement, Literal, Word, CaselessLiteral, + Optional, Combine, Forward, ZeroOrMore, nums, oneOf, Group, ParseException, OneOrMore) + +#ParserElement.enablePackrat() + +def Command(char): + """ Case insensitive but case preserving""" + return CaselessPreservingLiteral(char) + +def Arguments(token): + return Group(token) + + +class CaselessPreservingLiteral(CaselessLiteral): + """ Like CaselessLiteral, but returns the match as found + instead of as defined. + """ + def __init__( self, matchString ): + super(CaselessPreservingLiteral,self).__init__( matchString.upper() ) + self.name = "'%s'" % matchString + self.errmsg = "Expected " + self.name + self.myException.msg = self.errmsg + + def parseImpl( self, instring, loc, doActions=True ): + test = instring[ loc:loc+self.matchLen ] + if test.upper() == self.match: + return loc+self.matchLen, test + #~ raise ParseException( instring, loc, self.errmsg ) + exc = self.myException + exc.loc = loc + exc.pstr = instring + raise exc + +def Sequence(token): + """ A sequence of the token""" + return OneOrMore(token+maybeComma) + +digit_sequence = Word(nums) + +sign = oneOf("+ -") + +def convertToFloat(s, loc, toks): + try: + return float(toks[0]) + except: + raise ParseException(loc, "invalid float format %s"%toks[0]) + +exponent = CaselessLiteral("e")+Optional(sign)+Word(nums) + +#note that almost all these fields are optional, +#and this can match almost anything. We rely on Pythons built-in +#float() function to clear out invalid values - loosely matching like this +#speeds up parsing quite a lot +floatingPointConstant = Combine( + Optional(sign) + + Optional(Word(nums)) + + Optional(Literal(".") + Optional(Word(nums)))+ + Optional(exponent) +) + +floatingPointConstant.setParseAction(convertToFloat) + +number = floatingPointConstant + +#same as FP constant but don't allow a - sign +nonnegativeNumber = Combine( + Optional(Word(nums)) + + Optional(Literal(".") + Optional(Word(nums)))+ + Optional(exponent) +) +nonnegativeNumber.setParseAction(convertToFloat) + +coordinate = number + +#comma or whitespace can seperate values all over the place in SVG +maybeComma = Optional(Literal(',')).suppress() + +coordinateSequence = Sequence(coordinate) + +coordinatePair = (coordinate + maybeComma + coordinate).setParseAction(lambda t: tuple(t)) +coordinatePairSequence = Sequence(coordinatePair) + +coordinatePairPair = coordinatePair + maybeComma + coordinatePair +coordinatePairPairSequence = Sequence(Group(coordinatePairPair)) + +coordinatePairTriple = coordinatePair + maybeComma + coordinatePair + maybeComma + coordinatePair +coordinatePairTripleSequence = Sequence(Group(coordinatePairTriple)) + +#commands +lineTo = Group(Command("L") + Arguments(coordinatePairSequence)) + +moveTo = Group(Command("M") + Arguments(coordinatePairSequence)) + +closePath = Group(Command("Z")).setParseAction(lambda t: ('Z', (None,))) + +flag = oneOf("1 0").setParseAction(lambda t: bool(int((t[0])))) + +arcRadius = ( + nonnegativeNumber + maybeComma + #rx + nonnegativeNumber #ry +).setParseAction(lambda t: tuple(t)) + +arcFlags = (flag + maybeComma + flag).setParseAction(lambda t: tuple(t)) + +ellipticalArcArgument = Group( + arcRadius + maybeComma + #rx, ry + number + maybeComma +#rotation + arcFlags + #large-arc-flag, sweep-flag + coordinatePair #(x,y) +) + + +ellipticalArc = Group(Command("A") + Arguments(Sequence(ellipticalArcArgument))) + +smoothQuadraticBezierCurveto = Group(Command("T") + Arguments(coordinatePairSequence)) + +quadraticBezierCurveto = Group(Command("Q") + Arguments(coordinatePairPairSequence)) + +smoothCurve = Group(Command("S") + Arguments(coordinatePairPairSequence)) + +curve = Group(Command("C") + Arguments(coordinatePairTripleSequence)) + +horizontalLine = Group(Command("H") + Arguments(coordinateSequence)) +verticalLine = Group(Command("V") + Arguments(coordinateSequence)) + +drawToCommand = ( + lineTo | moveTo | closePath | ellipticalArc | smoothQuadraticBezierCurveto | + quadraticBezierCurveto | smoothCurve | curve | horizontalLine | verticalLine + ) + +#~ number.debug = True +moveToDrawToCommands = moveTo + ZeroOrMore(drawToCommand) + +svg = ZeroOrMore(moveToDrawToCommands) +svg.keepTabs = True + +def profile(): + import cProfile + p = cProfile.Profile() + p.enable() + ptest() + ptest() + ptest() + p.disable() + p.print_stats() + +bpath = """M204.33 139.83 C196.33 133.33 206.68 132.82 206.58 132.58 C192.33 97.08 169.35 + 81.41 167.58 80.58 C162.12 78.02 159.48 78.26 160.45 76.97 C161.41 75.68 167.72 79.72 168.58 + 80.33 C193.83 98.33 207.58 132.33 207.58 132.33 C207.58 132.33 209.33 133.33 209.58 132.58 + C219.58 103.08 239.58 87.58 246.33 81.33 C253.08 75.08 256.63 74.47 247.33 81.58 C218.58 103.58 + 210.34 132.23 210.83 132.33 C222.33 134.83 211.33 140.33 211.83 139.83 C214.85 136.81 214.83 145.83 214.83 + 145.83 C214.83 145.83 231.83 110.83 298.33 66.33 C302.43 63.59 445.83 -14.67 395.83 80.83 C393.24 85.79 375.83 + 105.83 375.83 105.83 C375.83 105.83 377.33 114.33 371.33 121.33 C370.3 122.53 367.83 134.33 361.83 140.83 C360.14 142.67 + 361.81 139.25 361.83 140.83 C362.33 170.83 337.76 170.17 339.33 170.33 C348.83 171.33 350.19 183.66 350.33 183.83 C355.83 + 190.33 353.83 191.83 355.83 194.83 C366.63 211.02 355.24 210.05 356.83 212.83 C360.83 219.83 355.99 222.72 357.33 224.83 + C360.83 230.33 354.75 233.84 354.83 235.33 C355.33 243.83 349.67 240.73 349.83 244.33 C350.33 255.33 346.33 250.83 343.83 254.83 + C336.33 266.83 333.46 262.38 332.83 263.83 C329.83 270.83 325.81 269.15 324.33 270.83 C320.83 274.83 317.33 274.83 315.83 276.33 + C308.83 283.33 304.86 278.39 303.83 278.83 C287.83 285.83 280.33 280.17 277.83 280.33 C270.33 280.83 271.48 279.67 269.33 277.83 + C237.83 250.83 219.33 211.83 215.83 206.83 C214.4 204.79 211.35 193.12 212.33 195.83 C214.33 201.33 213.33 250.33 207.83 250.33 + C202.33 250.33 201.83 204.33 205.33 195.83 C206.43 193.16 204.4 203.72 201.79 206.83 C196.33 213.33 179.5 250.83 147.59 277.83 + C145.42 279.67 146.58 280.83 138.98 280.33 C136.46 280.17 128.85 285.83 112.65 278.83 C111.61 278.39 107.58 283.33 100.49 276.33 + C98.97 274.83 95.43 274.83 91.88 270.83 C90.39 269.15 86.31 270.83 83.27 263.83 C82.64 262.38 79.73 266.83 72.13 254.83 C69.6 250.83 + 65.54 255.33 66.05 244.33 C66.22 240.73 60.48 243.83 60.99 235.33 C61.08 233.84 54.91 230.33 58.45 224.83 C59.81 222.72 54.91 219.83 + 58.96 212.83 C60.57 210.05 49.04 211.02 59.97 194.83 C62 191.83 59.97 190.33 65.54 183.83 C65.69 183.66 67.06 171.33 76.69 170.33 + C78.28 170.17 53.39 170.83 53.9 140.83 C53.92 139.25 55.61 142.67 53.9 140.83 C47.82 134.33 45.32 122.53 44.27 121.33 C38.19 114.33 + 39.71 105.83 39.71 105.83 C39.71 105.83 22.08 85.79 19.46 80.83 C-31.19 -14.67 114.07 63.59 118.22 66.33 C185.58 110.83 202 145.83 + 202 145.83 C202 145.83 202.36 143.28 203 141.83 C203.64 140.39 204.56 140.02 204.33 139.83 z""" + +def ptest(): + svg.parseString(bpath) + + + + + +if __name__ == '__main__': + + #~ from tests.test_pathdata import * + #~ unittest.main() + profile() + + diff --git a/svg/testfiles/belt_pulley3.skeinforge.large.svg b/svg/testfiles/belt_pulley3.skeinforge.large.svg new file mode 100644 index 0000000..79a24cb --- /dev/null +++ b/svg/testfiles/belt_pulley3.skeinforge.large.svg @@ -0,0 +1,655 @@ + + + + + + + + belt_pulley3.stl - Slice Layers + + + + + + Layer 0, z:-1.8 + + + + + Layer 1, z:-1.4 + + + + + Layer 2, z:-1.0 + + + + + Layer 3, z:-0.6 + + + + + Layer 4, z:-0.2 + + + + + Layer 5, z:0.2 + + + + + Layer 6, z:0.6 + + + + + Layer 7, z:1.0 + + + + + Layer 8, z:1.4 + + + + + Layer 9, z:1.8 + + + + + Layer 10, z:2.2 + + + + + Layer 11, z:2.6 + + + + + Layer 12, z:3.0 + + + + + Layer 13, z:3.4 + + + + + Layer 14, z:3.8 + + + + + Layer 15, z:4.2 + + + + + Layer 16, z:4.6 + + + + + Layer 17, z:5.0 + + + + + Layer 18, z:5.4 + + + + + Layer 19, z:5.8 + + + + + Layer 20, z:6.2 + + + + + Layer 21, z:6.6 + + + + + Layer 22, z:7.0 + + + + + Layer 23, z:7.4 + + + + + Layer 24, z:7.8 + + + + + Layer 25, z:8.2 + + + + + Layer 26, z:8.6 + + + + + Layer 27, z:9.0 + + + + + Layer 28, z:9.4 + + + + + Layer 29, z:9.8 + + + + + + + + + + + + + Latitude + < + > + Longitude + < + > + Scale + 1 + < + > + + Min + X: -11.919 mm + Y: -11.919 mm + Z: -2.0 mm + + + Max + X: 11.919 mm + Y: 11.919 mm + Z: 10.0 mm + + + Dimension + X: 23.838 mm + Y: 23.838 mm + Z: 12.0 mm + + + Statistics + Layer Height: 0.4 mm + Number of Layers: 30 + Volume: 1.8836 cm3 + + + + + + + + Y + X + 0 + + + 1 + Layer + < + > + Scale + 1 + < + > + + Min + X: -11.919 mm + Y: -11.919 mm + Z: -2.0 mm + + + Max + X: 11.919 mm + Y: 11.919 mm + Z: 10.0 mm + + + Dimension + X: 23.838 mm + Y: 23.838 mm + Z: 12.0 mm + + + Statistics + Layer Height: 0.4 mm + Number of Layers: 30 + Volume: 1.8836 cm3 + + + + + + + Y + X + Scale + : 1 + < + > + + Min + X: -11.919 mm + Y: -11.919 mm + Z: -2.0 mm + + + Max + X: 11.919 mm + Y: 11.919 mm + Z: 10.0 mm + + + Dimension + X: 23.838 mm + Y: 23.838 mm + Z: 12.0 mm + + + Statistics + Layer Height: 0.4 mm + Number of Layers: 30 + Volume: 1.8836 cm3 + + + [Iso View] + Iso View + [Layer View] + Layer View + [Scroll View] + Scroll View + + diff --git a/svg/testfiles/belt_pulley3.skeinforge.small.svg b/svg/testfiles/belt_pulley3.skeinforge.small.svg new file mode 100644 index 0000000..9c57913 --- /dev/null +++ b/svg/testfiles/belt_pulley3.skeinforge.small.svg @@ -0,0 +1,515 @@ + + + + + + + + belt_pulley3.stl - Slice Layers + + + + + + Layer 0, z:-1.8 + + + + + Layer 1, z:-1.4 + + + + + + + + + + + + + Latitude + < + > + Longitude + < + > + Scale + 1 + < + > + + Min + X: -11.919 mm + Y: -11.919 mm + Z: -2.0 mm + + + Max + X: 11.919 mm + Y: 11.919 mm + Z: 10.0 mm + + + Dimension + X: 23.838 mm + Y: 23.838 mm + Z: 12.0 mm + + + Statistics + Layer Height: 0.4 mm + Number of Layers: 30 + Volume: 1.8836 cm3 + + + + + + + + Y + X + 0 + + + 1 + Layer + < + > + Scale + 1 + < + > + + Min + X: -11.919 mm + Y: -11.919 mm + Z: -2.0 mm + + + Max + X: 11.919 mm + Y: 11.919 mm + Z: 10.0 mm + + + Dimension + X: 23.838 mm + Y: 23.838 mm + Z: 12.0 mm + + + Statistics + Layer Height: 0.4 mm + Number of Layers: 30 + Volume: 1.8836 cm3 + + + + + + + Y + X + Scale + : 1 + < + > + + Min + X: -11.919 mm + Y: -11.919 mm + Z: -2.0 mm + + + Max + X: 11.919 mm + Y: 11.919 mm + Z: 10.0 mm + + + Dimension + X: 23.838 mm + Y: 23.838 mm + Z: 12.0 mm + + + Statistics + Layer Height: 0.4 mm + Number of Layers: 30 + Volume: 1.8836 cm3 + + + [Iso View] + Iso View + [Layer View] + Layer View + [Scroll View] + Scroll View + + diff --git a/svg/testfiles/belt_pulley3.slic3r.large.svg b/svg/testfiles/belt_pulley3.slic3r.large.svg new file mode 100644 index 0000000..d1acd94 --- /dev/null +++ b/svg/testfiles/belt_pulley3.slic3r.large.svg @@ -0,0 +1,488 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/svg/testfiles/belt_pulley3.slic3r.small.svg b/svg/testfiles/belt_pulley3.slic3r.small.svg new file mode 100644 index 0000000..9a219b5 --- /dev/null +++ b/svg/testfiles/belt_pulley3.slic3r.small.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + +