#!/usr/bin/env python """ Mainly written by Andras Prim github_at_primandras.hu Arc to bezier converting method is ported from: http://code.google.com/p/core-framework/source/browse/trunk/plugins/svg.js written by Angel Kostadinov, with MIT license """ try: from lxml import etree as ET except Exception: import xml.etree.ElementTree as ET import re import math def wrap(text, width): """ A word-wrap function that preserves existing line breaks """ retstr = "" for word in text.split(' '): if len(retstr)-retstr.rfind('\n')-1 + len(word.split('\n',1)[0]) >= width: retstr += ' \n' + word else: retstr += ' ' + word return retstr def css2dict(css): """returns a dictionary representing the given css string""" cssdict = {} if None == css: return cssdict for pair in css.split(';'): #TODO: what about escaped separators if pair.find(':') >= 0: key, value = pair.split(':') cssdict[ key.strip() ] = value.strip() return cssdict def cssColor2Eps(cssColor, colors='RGB'): """converts css color definition (a hexa code with leading # or 'rgb()') to eps color definition""" if '#' == cssColor[0]: r = float(int(cssColor[1:3],16)) / 255 g = float(int(cssColor[3:5],16)) / 255 b = float(int(cssColor[5:7],16)) / 255 else: # assume 'rgb()' color rgb = re.sub('[^0-9]+', ' ', cssColor).strip().split() r = float(int(rgb[0], 10)) / 255 g = float(int(rgb[1], 10)) / 255 b = float(int(rgb[2], 10)) / 255 if colors == 'RGB': return "%f %f %f" % (r, g, b) elif colors == 'CMYKRGB': if (r == 0) and (g == 0) and (b == 0): c = 0 m = 0 y = 0 k = 1 else: c = 1 - r m = 1 - g y = 1 - b # extract out k [0,1] min_cmy = min(c, m, y) c = (c - min_cmy) / (1 - min_cmy) m = (m - min_cmy) / (1 - min_cmy) y = (y - min_cmy) / (1 - min_cmy) k = min_cmy return "%f %f %f %f %f %f %f" % (c, m, y, k, r, g, b) class svg2eps: def __init__(self, filename=None): self.filename = filename self.svg = None self.rePathDSplit = re.compile('[^a-zA-Z0-9.-]+') self.reTransformFind = re.compile('([a-z]+)\\(([^)]+)\\)') self.reNumberFind = re.compile('[0-9.eE+-]+') # must update reNumberUnitFind, if e is a valid character in a unit self.reNumberUnitFind = re.compile('([0-9.eE+-]+)([a-z]*)') # px to pt conversion rate varies based on inkscape versions, it is added during parsing self.toPt = {'in': 72.0, 'pt': 1.0, 'mm': 2.8346456695, 'cm': 28.346456695, 'm': 2834.6456695, 'pc': 12.0} def unitConv(self, string, toUnit): match = self.reNumberUnitFind.search(string) number = float(match.group(1)) unit = match.group(2) if unit not in self.toPt: unit = 'uu' if unit == toUnit: return number else: return number * self.toPt[unit] / self.toPt[toUnit] def lengthConv(self, svgLength): """converts svgLength to eps length using the current transformation matrix""" matrix = self.matrices[-1] epsx = matrix[0] * svgLength epsy = matrix[1] * svgLength return math.sqrt(epsx*epsx + epsy*epsy) def coordConv(self, svgx, svgy, relative=False): """converts svgx, svgy coordinates to eps coordinates using the current transformation matrix""" if relative: svgx = float(svgx) + self.curPoint[0] svgy = float(svgy) + self.curPoint[1] else: svgx = float(svgx) svgy = float(svgy) matrix = self.matrices[-1] epsx = matrix[0] * svgx + matrix[2] * svgy + matrix[4] epsy = matrix[1] * svgx + matrix[3] * svgy + matrix[5] return (epsx, epsy) def matrixMul(self, matrix, matrix2): """multiplies matrix with matrix2""" matrix0 = matrix[:] matrix[0] = matrix0[0] * matrix2[0] + matrix0[2]*matrix2[1] # + matrix0[4]*0 matrix[1] = matrix0[1] * matrix2[0] + matrix0[3]*matrix2[1] # + matrix0[5]*0 matrix[2] = matrix0[0] * matrix2[2] + matrix0[2]*matrix2[3] # + matrix0[4]*0 matrix[3] = matrix0[1] * matrix2[2] + matrix0[3]*matrix2[3] # + matrix0[5]*0 matrix[4] = matrix0[0] * matrix2[4] + matrix0[2]*matrix2[5] + matrix0[4] matrix[5] = matrix0[1] * matrix2[4] + matrix0[3]*matrix2[5] + matrix0[5] def alert(self, string, elem): """adds an alert to the collection""" if not string in self.alerts: self.alerts[string] = set() elemId = elem.get('id') if elemId != None: self.alerts[string].add(elemId) def showAlerts(self): """show alerts collected by the alert() function""" for string, ids in self.alerts.iteritems(): idstring = ', '.join(ids) print(string, idstring) def elemSvg(self, elem): """handles the element""" # DPI changed in inkscape 0.92, so set the px-to-pt rate based on inkscape version self.toPt['px'] = 0.75 inkscapeVersionString = elem.get('{http://www.inkscape.org/namespaces/inkscape}version', '0.92.0') mobj = re.match(r'(\d+)\.(\d+)', inkscapeVersionString) if mobj != None: major = int(mobj.group(1)) minor = int(mobj.group(2)) if major == 0 and minor < 92: self.toPt['px'] = 0.8 # by default (without viewbox definition) user unit = pixel self.toPt['uu'] = self.toPt['px'] self.docWidth = self.unitConv(elem.get('width'), 'pt') self.docHeight = self.unitConv(elem.get('height'), 'pt') viewBoxString = elem.get('viewBox') if viewBoxString != None: viewBox = viewBoxString.split(' ') # theoretically width and height scaling factor could be different, # but this script does not support it widthUu = float(viewBox[2]) - float(viewBox[0]) self.toPt['uu'] = self.docWidth / widthUu # transform svg units to eps default pt scale = self.toPt['uu'] self.matrices = [ [scale, 0, 0, -scale, 0, self.docHeight] ] def gradientFill(self, elem, gradientId): """constructs a gradient instance definition in self.gradientOp""" if gradientId not in self.gradients: self.alert("fill gradient not defined: "+gradientId, elem ) return gradient = self.gradients[gradientId] transformGradient = gradient while 'href' in gradient: gradientId = gradient['href'] gradient = self.gradients[gradientId] if 'matrix' in transformGradient: self.matrices.append( self.matrices[-1][:] ) self.matrixMul(self.matrices[-1],transformGradient['matrix']) if 'linear' == transformGradient['type']: gradient['linUseCount'] += 1 x1, y1 = self.coordConv(transformGradient['x1'], transformGradient['y1']) x2, y2 = self.coordConv(transformGradient['x2'], transformGradient['y2']) deltax = x2 - x1 deltay = y2 - y1 length = math.sqrt( deltax*deltax + deltay*deltay ) angle = math.atan2(deltay, deltax)*180/math.pi elif 'radial' == transformGradient['type']: gradient['radUseCount'] += 1 cx, cy = self.coordConv(transformGradient['cx'], transformGradient['cy']) # fx, fy = self.coordConv(transformGradient['fx'], transformGradient['fy']) rx, ry = self.coordConv(transformGradient['cx'] + transformGradient['r'], transformGradient['cy']) r = math.sqrt( (rx-cx)*(rx-cx) + (ry-cy)*(ry-cy)) if 'matrix' in transformGradient: self.matrices.pop() if 'linear' == transformGradient['type']: #endPathSegment() will substitute appropriate closeOp in %%s self.gradientOp = "\nBb 1 (l_%s) %f %f %f %f 1 0 0 1 0 0 Bg %%s 0 BB" % \ (gradientId, x1, y1, angle, length) elif 'radial' == transformGradient['type']: self.gradientOp = "\nBb 1 (r_%s) %f %f 0 %f 1 0 0 1 0 0 Bg %%s 0 BB" % \ (gradientId, cx, cy, r) self.alert("radial gradients will appear circle shaped", elem) def pathStyle(self, elem): """handles the style attribute in svg element""" if self.clipPath: self.closeOp = 'h n' return css = self.cssStack[-1] if 'stroke' in css and css['stroke'] != 'none': self.closeOp = 's' self.pathCloseOp = 's' if '#' == css['stroke'][0] or 'rgb' == css['stroke'][0:3]: self.epspath += ' ' + cssColor2Eps(css['stroke']) + ' XA' elif 'url' == css['stroke'][0:3]: self.alert("gradient strokes not supported", elem) if 'fill' in css and css['fill'] != 'none': if self.closeOp == 's': self.closeOp = 'b' else: self.closeOp = 'f' if '#' == css['fill'][0] or 'rgb' == css['fill'][0:3]: self.epspath += ' ' + cssColor2Eps(css['fill']) + ' Xa' elif 'url' == css['fill'][0:3]: self.gradientFill(elem, css['fill'][5:-1]) if 'fill-rule' in css: if css['fill-rule'] == 'evenodd': self.epspath += " 1 XR" else: self.epspath += " 0 XR" if 'stroke-width' in css: self.epspath += " %f w" % (self.lengthConv(self.unitConv(css['stroke-width'], 'uu')), ) if 'stroke-linecap' in css: if css['stroke-linecap'] == 'butt': self.epspath += " 0 J" elif css['stroke-linecap'] == 'round': self.epspath += " 1 J" elif css['stroke-linecap'] == 'square': self.epspath += " 2 J" if 'stroke-linejoin' in css: if css['stroke-linejoin'] == 'miter': self.epspath += " 0 j" elif css['stroke-linejoin'] == 'round': self.epspath += " 1 j" elif css['stroke-linejoin'] == 'bevel': self.epspath += " 2 j" if 'stroke-miterlimit' in css: self.epspath += " " + css['stroke-miterlimit'] + " M" if 'stroke-dasharray' in css: phase = 0 if css['stroke-dasharray'] == 'none': dashArray = [] if css['stroke-dasharray'] != 'none': dashArrayIn = css['stroke-dasharray'].replace(',', ' ').split() dashArray = list(map(lambda x: "%f" % (x,), filter(lambda x: x > 0, map(lambda x: self.lengthConv(float(x)), dashArrayIn)))) if 'stroke-dashoffset' in css: phase = float(css['stroke-dashoffset']) self.epspath += ' [ %s ] %f d' % (' '.join(dashArray), phase) def endPathSegment(self, elem): """should be called when a path segment end is reached in a element""" if self.removeStrayPoints and self.segmentCommands <= 1: self.alert("removing stray point", elem) self.epspath = self.epspath[:self.segmentStartIndex] return if self.autoClose and (self.closeOp == 'f' or self.closeOp == 'b'): autoClose = True else: autoClose = False if self.pathExplicitClose or autoClose: closeOp = self.closeOp else: closeOp = self.closeOp.upper() if self.pathCurSegment == self.pathSegmentNum and self.gradientOp != None: closeOp = self.gradientOp % (closeOp,) if self.lastBegin != None: if (self.pathExplicitClose or autoClose): if abs(self.curPoint[0] - self.lastBegin[0]) + \ abs(self.curPoint[1] - self.lastBegin[1]) > self.closeDist: x, y = self.coordConv(self.lastBegin[0], self.lastBegin[1]) self.epspath += ' %f %f l' % (x, y) self.epspath += ' ' + closeOp + '\n' if self.pathExplicitClose: self.curPoint = self.lastBegin self.lastBegin = None def elemPath(self, elem, pathData=None): """handles svg element""" if None == pathData: pathData = elem.get('d') self.pathSegmentNum = pathData.count("m") + pathData.count("M") self.pathCurSegment = 0 self.epspath = '' self.segmentStartIndex = 0 # index in self.epspath of first character of current path segment self.segmentCommands = 0 # number of handled commands (including first moveto) in current paths segment self.closeOp = 'n' # pathStyle(elem) will modify this self.gradientOp = None self.pathExplicitClose = False if elem.get('id'): self.epspath += '\n%AI3_Note: ' + elem.get('id') + '\n' self.pathStyle(elem) tokens = self.rePathDSplit.split(pathData) i = 0 cmd = '' # path command self.curPoint = (0,0) self.lastBegin = None while i < len(tokens): token = tokens[i] if token in ['m', 'M', 'c', 'C', 'l', 'L', 'z', 'Z', 'a', 'A', 'q', 'Q', 'h', 'H', 'v', 'V']: cmd = token i += 1 elif token.isalpha(): self.alert('unhandled path command: %s' % (token,), elem) cmd = '' i += 1 else: # coordinates after a moveto are assumed to be lineto if 'm' == cmd: cmd = 'l' elif 'M' == cmd: cmd = 'L' if ('M' == cmd or 'm' == cmd) : if self.pathCurSegment > 0: self.endPathSegment(elem) self.pathCurSegment += 1 self.pathExplicitClose = False if 'M' == cmd or 'm' == cmd: if 'M' == cmd or ('m' == cmd and i == 1): self.curPoint = (float(tokens[i]), float(tokens[i+1])) else: self.curPoint = (self.curPoint[0] + float(tokens[i]), self.curPoint[1] + float(tokens[i+1])) self.segmentStartIndex = len(self.epspath) x, y = self.coordConv(self.curPoint[0], self.curPoint[1]) self.epspath += ' %f %f' % (x, y) i += 2 self.lastBegin = self.curPoint self.epspath += ' m' self.segmentCommands = 1 elif 'L' == cmd or 'l' == cmd: if 'L' == cmd: self.curPoint = (float(tokens[i]), float(tokens[i+1])) else: self.curPoint = (self.curPoint[0] + float(tokens[i]), self.curPoint[1] + float(tokens[i+1])) x, y = self.coordConv(self.curPoint[0], self.curPoint[1]) self.epspath += ' %f %f' % (x, y) i += 2 self.epspath += ' l' self.segmentCommands += 1 elif cmd in ['H', 'h', 'V', 'v']: if 'H' == cmd: self.curPoint = (float(tokens[i]), self.curPoint[1]) elif 'h' == cmd: self.curPoint = (self.curPoint[0] + float(tokens[i]), self.curPoint[1]) elif 'V' == cmd: self.curPoint = (self.curPoint[0], float(tokens[i])) elif 'v' == cmd: self.curPoint = (self.curPoint[0], self.curPoint[1] + float(tokens[i])) x, y = self.coordConv(self.curPoint[0], self.curPoint[1]) self.epspath += ' %f %f' % (x, y) i += 1 self.epspath += ' l' self.segmentCommands += 1 elif 'C' == cmd: for j in range(2): x, y = self.coordConv(tokens[i], tokens[i+1]) self.epspath += ' %f %f' % (x, y) i += 2 self.curPoint = (float(tokens[i]), float(tokens[i+1])) x, y = self.coordConv(self.curPoint[0], self.curPoint[1]) self.epspath += ' %f %f' % (x, y) i += 2 self.epspath += ' c' self.segmentCommands += 1 elif 'c' == cmd: for j in range(2): x, y = self.coordConv(self.curPoint[0] + float(tokens[i]), self.curPoint[1] +float(tokens[i+1])) self.epspath += ' %f %f' % (x, y) i += 2 self.curPoint = (self.curPoint[0] + float(tokens[i]), self.curPoint[1] + float(tokens[i+1])) x, y = self.coordConv(self.curPoint[0], self.curPoint[1]) self.epspath += ' %f %f' % (x, y) i += 2 self.epspath += ' c' self.segmentCommands += 1 elif 'Q' == cmd: #export quadratic Bezier as cubic qx0, qy0 = self.coordConv(self.curPoint[0], self.curPoint[1]) qx1, qy1 = self.coordConv(float(tokens[i]), float(tokens[i+1])) i += 2 self.curPoint = (float(tokens[i]), float(tokens[i+1])) qx2, qy2 = self.coordConv(self.curPoint[0], self.curPoint[1]) factor = 2.0 / 3.0 cx1 = qx0 + factor * (qx1 - qx0) cy1 = qy0 + factor * (qy1 - qy0) cx2 = qx2 - factor * (qx2 - qx1) cy2 = qy2 - factor * (qy2 - qy1) self.epspath += ' %f %f %f %f' % (cx1, cy1, cx2, cy2) self.epspath += ' %f %f' % (qx2, qy2) i += 2 self.epspath += ' c' self.segmentCommands += 1 elif 'q' == cmd: qx0, qy0 = self.coordConv(self.curPoint[0], self.curPoint[1]) qx1, qy1 = self.coordConv(self.curPoint[0] + float(tokens[i]), self.curPoint[1] + float(tokens[i+1])) i += 2 self.curPoint = (self.curPoint[0] + float(tokens[i]), self.curPoint[1] + float(tokens[i+1])) qx2, qy2 = self.coordConv(self.curPoint[0], self.curPoint[1]) factor = 2.0 / 3.0 cx1 = qx0 + factor * (qx1 - qx0) cy1 = qy0 + factor * (qy1 - qy0) cx2 = qx2 - factor * (qx2 - qx1) cy2 = qy2 - factor * (qy2 - qy1) self.epspath += ' %f %f %f %f' % (cx1, cy1, cx2, cy2) self.epspath += ' %f %f' % (qx2, qy2) i += 2 self.epspath += ' c' self.segmentCommands += 1 elif 'A' == cmd or 'a' == cmd: self.alert("elliptic arcs are converted to bezier curves", elem) # Angel Kostadinov begin r1 = abs(float(tokens[i])) r2 = abs(float(tokens[i+1])) psai = float(tokens[i+2]) largeArcFlag = int(tokens[i + 3]) fS = int(tokens[i+4]) rx = self.curPoint[0] ry = self.curPoint[1] if 'A' == cmd: cx, cy = (float(tokens[i+5]), float(tokens[i+6])) else: cx, cy = (self.curPoint[0] +float(tokens[i+5]), self.curPoint[1] +float(tokens[i+6])) if r1 > 0 and r2 > 0: ctx = (rx - cx) / 2 cty = (ry - cy) / 2 cpsi = math.cos(psai*math.pi/180) spsi = math.sin(psai*math.pi/180) rxd = cpsi*ctx + spsi*cty ryd = -1*spsi*ctx + cpsi*cty rxdd = rxd * rxd rydd = ryd * ryd r1x = r1 * r1 r2y = r2 * r2 lamda = rxdd/r1x + rydd/r2y if lamda > 1: r1 = math.sqrt(lamda) * r1 r2 = math.sqrt(lamda) * r2 sds = 0 else: seif = 1 if largeArcFlag == fS: seif = -1 sds = seif * math.sqrt((r1x*r2y - r1x*rydd - r2y*rxdd) / (r1x*rydd + r2y*rxdd)) txd = sds*r1*ryd / r2 tyd = -1 * sds*r2*rxd / r1 tx = cpsi*txd - spsi*tyd + (rx+cx)/2 ty = spsi*txd + cpsi*tyd + (ry+cy)/2 rad = math.atan2((ryd-tyd)/r2, (rxd-txd)/r1) - math.atan2(0, 1) if rad >= 0: s1 = rad else: s1 = 2 * math.pi + rad rad = math.atan2((-ryd-tyd)/r2, (-rxd-txd)/r1) - math.atan2((ryd-tyd)/r2, (rxd-txd)/r1) if rad >= 0: dr = rad else: dr = 2 * math.pi + rad if fS==0 and dr > 0: dr -= 2*math.pi elif fS==1 and dr < 0: dr += 2*math.pi sse = dr * 2 / math.pi if sse < 0: seg = math.ceil(-1*sse) else: seg = math.ceil(sse) segr = dr / seg t = 8.0/3.0 * math.sin(segr/4) * math.sin(segr/4) / math.sin(segr/2) cpsir1 = cpsi * r1 cpsir2 = cpsi * r2 spsir1 = spsi * r1 spsir2 = spsi * r2 mc = math.cos(s1) ms = math.sin(s1) x2 = rx - t * (cpsir1*ms + spsir2*mc) y2 = ry - t * (spsir1*ms - cpsir2*mc) for n in range(int(math.ceil(seg))): s1 += segr mc = math.cos(s1) ms = math.sin(s1) x3 = cpsir1*mc - spsir2*ms + tx y3 = spsir1*mc + cpsir2*ms + ty dx = -t * (cpsir1*ms + spsir2*mc) dy = -t * (spsir1*ms - cpsir2*mc) cx1, cy1 = self.coordConv(x2,y2) cx2, cy2 = self.coordConv(x3-dx,y3-dy) cx3, cy3 = self.coordConv(x3,y3) self.epspath += " %f %f %f %f %f %f c" % (cx1, cy1, cx2, cy2, cx3, cy3) x2 = x3 + dx y2 = y3 + dy else: # case when one radius is zero: this is a simple line x, y = self.coordConv(cx, cy) self.epspath += ' %f %f l' % (x, y) # Angel Kostadinov end self.segmentCommands += 1 i += 7 self.curPoint= (cx, cy) elif 'z' == cmd or 'Z' == cmd: self.pathExplicitClose = True cmd = '' else: i += 1 self.endPathSegment(elem) if self.pathSegmentNum > 1: self.epspath = " *u\n" + self.epspath + "\n*U " self.epsLayers += "\n" + wrap(self.epspath, 70) + "\n" def elemRect(self, elem): x = float(elem.get('x')) y = float(elem.get('y')) width = float(elem.get('width')) height = float(elem.get('height')) # construct an svg d attribute, and call self.elemPath() pathData = "" rx = elem.get('rx') ry = elem.get('ry') if None == rx and None == ry: rx = 0 ry = 0 else: # if only one radius is given, it means both are the same if None == rx: rx = float(ry) else: rx = float(rx) if None == ry: ry = float(rx) else: ry = float(ry) if rx == 0 and ry == 0: pathData = "M %f %f %f %f %f %f %f %f z" % (x,y, x+width,y, x+width, y+height, x, y+height) else: pathData = "M %f %f A %f %f 0 0 1 %f %f" % (x, y+ry, rx,ry, x+rx, y) pathData += " L %f %f A %f %f 0 0 1 %f %f" % (x+width-rx, y, rx,ry, x+width, y+ry) pathData += " L %f %f A %f %f 0 0 1 %f %f" % (x+width, y+height-ry, rx,ry, x+width-rx, y+height) pathData += " L %f %f A %f %f 0 0 1 %f %f z" % (x+rx, y+height, rx,ry, x, y+height-ry) self.elemPath(elem, pathData) def elemPolygon(self, elem): pathData = 'M ' + elem.get('points').replace(',', ' ').strip() + ' z' self.elemPath(elem, pathData) def elemCircle(self, elem): r = float(elem.get('r')) self.elemEllipseCircleCommon(elem, r, r) def elemEllipse(self, elem): rx = float(elem.get('rx')) ry = float(elem.get('ry')) self.elemEllipseCircleCommon(elem, rx, ry) def elemEllipseCircleCommon(self, elem, rx, ry): cx = float(elem.get('cx')) cy = float(elem.get('cy')) # todo: test whether PostScript arc is handled by AI and use that instead magic = 0.55228475 # I've read it on the internet controlx = rx * magic # x distance of control points from center controly = ry * magic # y distance of control points from center # construct an svg d attribute, and call self.elemPath() pathData = "M %f %f" % (cx - rx, cy) # leftmost point pathData += " C %f %f %f %f %f %f" % (cx - rx, cy - controly, cx - controlx, cy - ry, cx, cy - ry) # to top pathData += " C %f %f %f %f %f %f" % (cx + controlx, cy - ry, cx + rx, cy - controly, cx + rx, cy) # to right pathData += " C %f %f %f %f %f %f" % (cx + rx, cy + controly, cx + controlx, cy + ry, cx, cy + ry) # to bottom pathData += " C %f %f %f %f %f %f z" % (cx - controlx, cy + ry, cx - rx, cy + controly, cx - rx, cy) # back to left and close self.elemPath(elem, pathData) def attrTransform(self, matrix, transform): """transforms matrix using svg transform attribute""" for ttype, targs in self.reTransformFind.findall(transform): targs = list(map(lambda x: float(x), self.reNumberFind.findall(targs))) if ttype == 'matrix': newmatrix = [ targs[0], targs[1], targs[2], targs[3], targs[4], targs[5] ] self.matrixMul(matrix, newmatrix) elif ttype == 'translate': tx = targs[0] ty = targs[1] if len(targs) > 1 else 0 newmatrix = [ 1, 0, 0, 1, tx, ty ] self.matrixMul(matrix, newmatrix) elif ttype == 'scale': sx = targs[0] sy = targs[1] if len(targs) > 1 else sx newmatrix = [ sx, 0, 0, sy, 0, 0 ] self.matrixMul(matrix, newmatrix) elif ttype == 'rotate': if len(targs) == 1: alpha = targs[0] newmatrix = [ math.cos(alpha), math.sin(alpha), -math.sin(alpha), math.cos(alpha), 0, 0] self.matrixMul(matrix, newmatrix) else: alpha = targs[0] newmatrix = [ 1, 0, 0, 1, targs[1], targs[2] ] self.matrixMul(matrix, newmatrix) newmatrix = [ math.cos(alpha), math.sin(alpha), -math.sin(alpha), math.cos(alpha), 0, 0] self.matrixMul(matrix, newmatrix) newmatrix = [ 1, 0, 0, 1, -targs[1], -targs[2] ] self.matrixMul(matrix, newmatrix) elif ttype == 'skewX' or ttype == 'skewY': self.alert("skewX and skewY transformations are not supported", elem) else: print('unknown transform type: ', ttype) return matrix def elemGradient(self, elem, grType): """handles and svg elements""" elemId = elem.get('id') if elemId != None: self.curGradientId = elemId self.gradients[elemId] = {'stops': [], 'linUseCount': 0, 'radUseCount': 0, 'type': grType} if 'linear' == grType: x1 = elem.get('x1') if None != x1: self.gradients[elemId]['x1'] = float(x1) self.gradients[elemId]['y1'] = float(elem.get('y1')) self.gradients[elemId]['x2'] = float(elem.get('x2')) self.gradients[elemId]['y2'] = float(elem.get('y2')) elif 'radial' == grType: cx = elem.get('cx') if None != cx: self.gradients[elemId]['cx'] = float(cx) self.gradients[elemId]['cy'] = float(elem.get('cy')) self.gradients[elemId]['fx'] = float(elem.get('fx')) self.gradients[elemId]['fy'] = float(elem.get('fy')) self.gradients[elemId]['r'] = float(elem.get('r')) transform = elem.get('gradientTransform') if None != transform: self.gradients[elemId]['matrix'] = self.attrTransform([1, 0, 0, 1, 0, 0], transform) href = elem.get('{http://www.w3.org/1999/xlink}href') if None != href: self.gradients[elemId]['href'] = href[1:] def elemStop(self, elem): """handles (gradient stop) svg element""" stopColor = elem.get('stop-color') if not stopColor: style = css2dict(elem.get('style')) if 'stop-color' in style: stopColor = style['stop-color'] else: stopColor = '#000000' color = cssColor2Eps(stopColor, 'CMYKRGB') offsetString = elem.get('offset').strip() if offsetString[-1] == '%': offset = float(offsetString[:-1]) else: offset = float(offsetString) * 100 self.gradients[self.curGradientId]['stops'].append( (offset, color) ) def gradientSetup(self): """writes used gradient definitions into self.epsSetup""" gradientNum = 0 epsGradients = "" for gradientId, gradient in self.gradients.items(): if gradient['linUseCount'] > 0: gradientNum += 1 epsGradients += ("\n%%AI5_BeginGradient: (l_%s)" + \ "\n(l_%s) 0 %d Bd\n[\n") % \ (gradientId, gradientId, len(gradient['stops'])) gradient['stops'].sort(key=lambda x: x[0], reverse=True) for offset, color in gradient['stops']: epsGradients += "%s 2 50 %f %%_Bs\n" % (color, offset) epsGradients += "BD\n%AI5_EndGradient\n" if gradient['radUseCount'] > 0: gradientNum += 1 epsGradients += ("\n%%AI5_BeginGradient: (r_%s)" + \ "\n(r_%s) 1 %d Bd\n[\n") % \ (gradientId, gradientId, len(gradient['stops'])) gradient['stops'].sort(key=lambda x: x[0]) for offset, color in gradient['stops']: epsGradients += "%s 2 50 %f %%_Bs\n" % (color, offset) epsGradients += "BD\n%AI5_EndGradient\n" if gradientNum > 0: self.epsSetup += ("\n%d Bn\n" % gradientNum) + epsGradients def layerStart(self, elem): self.epsLayers += '\n\n%AI5_BeginLayer\n' layerName = elem.get('{http://www.inkscape.org/namespaces/inkscape}label') layerName = "".join(map(lambda x: '_' if ord(x)<32 or ord(x) > 127 else x, layerName)) self.epsLayers += '1 1 1 1 0 0 %d 0 0 0 Lb\n(%s) Ln\n' % \ (self.layerColor, layerName) self.layerColor = (self.layerColor + 1) % 27 def elemUse(self, elem): """handles a svg element""" x = self.unitConv(elem.get('x'), 'uu') if x == None: x = 0 y = self.unitConv(elem.get('y'), 'uu') if y == None: y = 0 if x != 0 or y != 0: self.matrices.append( self.matrices[-1][:] ) self.attrTransform(self.matrices[-1], "translate(%f %f)" % (x, y)) href = elem.get('{http://www.w3.org/1999/xlink}href') usedElem = self.root.find(".//*[@id='%s']" % (href[1:],)) if usedElem != None: self.walkElem(usedElem) else: self.alert("used Elem not found: " + href, elem) if x != 0 or y != 0: self.matrices.pop() # def elemNamedView(self, elem): # """handles a svg element""" # newDocumentUnit = elem.get('{http://www.inkscape.org/namespaces/inkscape}document-units') # if newDocumentUnit in self.toPt and newDocumentUnit != self.documentUnit: # if len(self.matrices) > 0: # # recalculate scaling transformation to new document unit # scale = self.toPt[newDocumentUnit] / self.toPt[self.documentUnit] # self.matrices[-1][0] = scale * self.matrices[-1][0] # self.matrices[-1][3] = scale * self.matrices[-1][3] # self.documentUnit = newDocumentUnit def walkElem(self, elem): if '}' in elem.tag: uri, shortTag = elem.tag.split('}') else: shortTag = elem.tag uri = '' transform = elem.get('transform') clipPath = elem.get('clip-path') cssNew = css2dict(elem.get('style')) css = self.cssStack[-1].copy() css.update(cssNew) self.cssStack.append(css) if self.removeInvisible: if 'visibility' in css and (css['visibility'] == 'hidden' or css['visibility'] == 'collapse'): return if 'display' in css and css['display'] == 'none': return if shortTag in ('path', 'rect', 'circle', 'ellipse', 'polygon'): if 'opacity' in css and css['opacity'] == '0': return stroke = False if 'stroke' in css and 'none' != css['stroke']: stroke = True if 'stroke-opacity' in css and css['stroke-opacity'] == '0': stroke = False if 'stroke-width' in css and css['stroke-width'] == '0': stroke = False fill = False if 'fill' in css and 'none' != css['fill']: fill = True if 'fill-opacity' in css and css['fill-opacity'] == '0': stroke = False if stroke == False and fill == False: return if transform != None: self.matrices.append( self.matrices[-1][:] ) self.attrTransform(self.matrices[-1], transform) if None != clipPath: clipId = clipPath[5:-1] clipElem = self.root.find(".//*[@id='%s']" % (clipId,)) if clipElem == None: self.alert('clipPath not found', elem) clipPath = None else: self.epsLayers += "\nq\n" clipPathSave= self.clipPath self.clipPath = True # output clip path even if it doesn't have visible style popRemoveInvisible = self.removeInvisible self.removeInvisible = False self.walkElem(clipElem) self.removeInvisible = popRemoveInvisible self.clipPath = clipPathSave self.epsLayers += ' W' if 'svg' == shortTag: self.elemSvg(elem) elif 'path' == shortTag: # do not output paths that are in defs # if they are referenced, they will be used there if self.section != 'defs': self.elemPath(elem) elif 'rect' == shortTag: if self.section != 'defs': self.elemRect(elem) elif 'circle' == shortTag: if self.section != 'defs': self.elemCircle(elem) elif 'ellipse' == shortTag: if self.section != 'defs': self.elemEllipse(elem) elif 'polygon' == shortTag: if self.section != 'defs': self.elemPolygon(elem) elif 'linearGradient' == shortTag: self.elemGradient(elem, 'linear') elif 'radialGradient' == shortTag: self.elemGradient(elem, 'radial') elif 'stop' == shortTag: self.elemStop(elem) elif 'g' == shortTag: if 'layer' == elem.get('{http://www.inkscape.org/namespaces/inkscape}groupmode'): self.layerStart(elem) elif None == clipPath: # clipping makes a group anyway self.epsLayers += '\nu\n' elif 'use' == shortTag: self.elemUse(elem) elif 'defs' == shortTag: self.section = shortTag elif 'namedview' == shortTag: self.section = shortTag else: self.alert("unhandled elem: " + shortTag, elem) for child in list(elem): self.walkElem(child) if None != clipPath: self.epsLayers += "\nQ\n" if 'g' == shortTag: if 'layer' == elem.get('{http://www.inkscape.org/namespaces/inkscape}groupmode'): self.epsLayers += '\nLB\n%AI5_EndLayer\n' elif None == clipPath: self.epsLayers += '\nU\n' elif shortTag in ('defs', 'namedview'): self.section = None if transform != None: self.matrices.pop() self.cssStack.pop() def convert(self, svg = None): self.alerts = {} if None != svg: self.svg = svg if None == self.svg and None != self.filename: fd = open(self.filename, 'rb') self.svg = fd.read() fd.close() self.autoClose = True # TODO: make it optional self.removeInvisible = True # TODO: make it optional self.removeStrayPoints = True # TODO: make it optional # if last point of a path is further from first point, then an explicit # 'lineto' is written to the first point before 'closepath' self.closeDist = 0.1 self.matrices = [[1, 0, 0, 1, 0, 0]] self.cssStack = [{}] self.gradients = {} self.docHeight = 400 self.docWidth = 400 self.layerColor = 0 self.section = None self.clipPath = False self.epsComments = """%!PS-Adobe-3.0 EPSF-3.0 %%Creator: tzunghaor svg2eps %%Pages: 1 %%DocumentData: Clean7Bit %%LanguageLevel: 3 %%DocumentNeededResources: procset Adobe_Illustrator_AI5 1.3 0 %AI5_FileFormat 3 """ # TODO: creation date, user etc self.epsProlog = """%%BeginProlog 100 dict begin /tzung_eps_state save def /dict_count countdictstack def /op_count count 1 sub def /Adobe_Illustrator_AI5 where { pop } { /tzung_strokergb [ 0 0 0 ] def /tzung_compound 0 def /tzung_closeop { S } def /tzung_fillrule 0 def /*u { /tzung_compound 1 def newpath /tzung_fillrule 0 def } bind def /*U { /tzung_compound 0 def tzung_closeop } bind def /u {} bind def /U {} bind def /q { clipsave } bind def /Q { cliprestore } bind def /W { clip } bind def /Lb { 10 {pop} repeat } bind def /Ln {pop} bind def /LB {} bind def /w { setlinewidth } bind def /J { setlinecap } bind def /j { setlinejoin } bind def /M { setmiterlimit } bind def /d { setdash } bind def /m { tzung_compound 0 eq { newpath /tzung_fillrule 0 def } if moveto } bind def /l { lineto } bind def /c { curveto } bind def /XR { /tzung_fillrule exch def } bind def /Xa { setrgbcolor } bind def /XA { 3 array astore /tzung_strokergb exch def } bind def /F { tzung_compound 0 eq { tzung_fillrule 0 eq { fill } { eofill } ifelse } { /tzung_closeop {F} def } ifelse } bind def /f { closepath F } bind def /S { tzung_compound 0 eq { tzung_strokergb aload pop setrgbcolor stroke } { /tzung_closeop {S} def } ifelse } bind def /s { closepath S } bind def /B { tzung_compound 0 eq { gsave tzung_fillrule 0 eq { fill } { eofill } ifelse grestore tzung_strokergb aload pop setrgbcolor stroke } { /tzung_closeop {B} def } ifelse } bind def /b { closepath B } bind def /H { tzung_compound 0 eq { }{ /tzung_closeop {H} def } ifelse} bind def /h { closepath } bind def /N { tzung_compound 0 eq { }{ /tzung_closeop {N} def } ifelse} bind def /n { closepath N } bind def /Bn { /dict_gradients exch dict def} bind def /Bd { /tmp_ngradstop exch def /tmp_shadingtype exch def } bind def %leaves gradient name in stack /BD { ] % this handles only stops that have CMYKRGB color definitions % linear gradient stops must be in reverse order, radials in normal order aload pop /tmp_boundaries tmp_ngradstop array def /tmp_colors tmp_ngradstop array def tmp_shadingtype 0 eq { 0 1 tmp_ngradstop 1 sub % for i=0; i<= number of gradient stops - 1; i++ } { tmp_ngradstop 1 sub -1 0 % for i=number of gradient stops - 1; i >= 0; i++ } ifelse { /loopvar exch def 100 div tmp_boundaries loopvar 3 -1 roll put % obj array i => array i obj pop % assume gradient middle is always 50 pop % assume color type is always 2 (CMYKRGB) 3 array astore tmp_colors loopvar 3 -1 roll put pop pop pop pop % drop CMYK values } for tmp_ngradstop 2 eq { /tmp_function 5 dict def tmp_boundaries 0 get tmp_boundaries 1 get 2 array astore tmp_function /Domain 3 -1 roll put tmp_function /FunctionType 2 put tmp_function /C0 tmp_colors 0 get put tmp_function /C1 tmp_colors 1 get put tmp_function /N 1 put } { /tmp_functions tmp_ngradstop 1 sub array def 0 1 tmp_ngradstop 2 sub { /loopvar exch def /tmp_function 5 dict def tmp_function /Domain [0 1] put tmp_function /FunctionType 2 put tmp_function /C0 tmp_colors loopvar get put tmp_function /C1 tmp_colors loopvar 1 add get put tmp_function /N 1 put tmp_functions loopvar tmp_function put } for /tmp_function 5 dict def tmp_boundaries 0 get tmp_boundaries tmp_ngradstop 1 sub get 2 array astore tmp_function /Domain 3 -1 roll put tmp_function /FunctionType 3 put tmp_boundaries aload pop tmp_ngradstop -1 roll pop pop % remove first and last bounds tmp_ngradstop 2 sub array astore tmp_function /Bounds 3 -1 roll put tmp_function /Functions tmp_functions put tmp_ngradstop 1 sub { 0 1 } repeat tmp_ngradstop 1 sub 2 mul array astore tmp_function /Encode 3 -1 roll put } ifelse /tmp_shading 6 dict def tmp_shadingtype 0 eq { tmp_shading /ShadingType 2 put tmp_shading /Coords [ 0 0 1 0 ] put } { tmp_shading /ShadingType 3 put tmp_shading /Coords [ 0 0 0 0 0 1 ] put } ifelse tmp_shading /ColorSpace /DeviceRGB put tmp_shading /Domain [0 1] put tmp_shading /Extend[ true true] put tmp_shading /Function tmp_function put /tmp_gradient 2 dict def tmp_gradient /PatternType 2 put tmp_gradient /Shading tmp_shading put dict_gradients exch tmp_gradient put % gradient's name is on the top of the stack from Bd operator } bind def /Lb { 10 { pop } repeat } bind def /Ln { pop } bind def /Bb { } bind def /Bg { 6 { pop } repeat gsave 4 2 roll translate exch rotate dup scale exch pop % remove Bg flag dict_gradients exch get % now gradient name is on top of the stack [ 1 0 0 1 0 0 ] makepattern /pattern_tmp exch def grestore pattern_tmp setpattern gsave % save for after pattern fil for possible stroke } def /BB { grestore 2 eq { s } if } bind def /LB { } bind def } ifelse """ self.epsSetup = """%%BeginSetup /Adobe_Illustrator_AI5 where { pop Adobe_Illustrator_AI5 /initialize get exec } if """ self.epsLayers = "" self.epsTrailer = """%%Trailer showpage count op_count sub {pop} repeat countdictstack dict_count sub {end} repeat tzung_eps_state restore end %%EOF """ self.root = ET.fromstring(self.svg) self.walkElem(self.root) self.gradientSetup() sizeComment = "%%%%BoundingBox: 0 0 %d %d\n" % (math.ceil(self.docWidth), math.ceil(self.docHeight)) sizeComment += "%%%%HiResBoundingBox: 0 0 %f %f\n" % (self.docWidth, self.docHeight) sizeComment += "%%AI5_ArtSize: %f %f\n" % (self.docWidth, self.docHeight) pagesetup = """%%%%Page: 1 1 %%%%BeginPageSetup %%%%PageBoundingBox: 0 0 %d %d %%%%EndPageSetup """ % (self.docWidth, self.docHeight) eps = self.epsComments + sizeComment + "%%EndComments\n\n" eps += self.epsProlog + "\n%%EndProlog\n\n" eps += self.epsSetup + "\n%%EndSetup\n\n" eps += pagesetup + self.epsLayers + "\n\n" eps += self.epsTrailer return eps import sys if len(sys.argv) < 2: raise NameError("missing filename") exit(1) converter = svg2eps(sys.argv[1]) print(converter.convert()) #TODO: show alerts in dialogbox #converter.showAlerts()