#!/usr/bin/env python ''' Copyright (C) 2011 Karlisson Bezerra <contact@hacktoon.com> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ''' import inkex import simplestyle from simplepath import parsePath from simpletransform import parseTransform class Element: def attr(self, val, ns=""): if ns: val = inkex.addNS(val, ns) try: attr = float(self.node.get(val)) except: attr = self.node.get(val) return attr class GradientDef(Element): def __init__(self, node, stops): self.node = node self.stops = stops class LinearGradientDef(GradientDef): def get_data(self): x1 = self.attr("x1") y1 = self.attr("y1") x2 = self.attr("x2") y2 = self.attr("y2") #self.createLinearGradient(href, x1, y1, x2, y2) def draw(self): pass class RadialGradientDef(GradientDef): def get_data(self): cx = self.attr("cx") cy = self.attr("cy") r = self.attr("r") #self.createRadialGradient(href, cx, cy, r, cx, cy, r) def draw(self): pass class AbstractShape(Element): def __init__(self, command, node, ctx): self.node = node self.command = command self.ctx = ctx def get_data(self): return def get_style(self): style = simplestyle.parseStyle(self.attr("style")) #remove any trailing space in dict keys/values style = dict([(str.strip(k), str.strip(v)) for k,v in style.items()]) return style def set_style(self, style): """Translates style properties names into method calls""" self.ctx.style = style for key in style: tmp_list = map(str.capitalize, key.split("-")) method = "set" + "".join(tmp_list) if hasattr(self.ctx, method) and style[key] != "none": getattr(self.ctx, method)(style[key]) #saves style to compare in next iteration self.ctx.style_cache = style def has_transform(self): return bool(self.attr("transform")) def get_transform(self): data = self.node.get("transform") if not data: return matrix = parseTransform(data) m11, m21, dx = matrix[0] m12, m22, dy = matrix[1] return m11, m12, m21, m22, dx, dy def has_gradient(self): style = self.get_style() if "fill" in style: fill = style["fill"] return fill.startswith("url(#linear") or \ fill.startswith("url(#radial") return False def get_gradient_href(self): style = self.get_style() if "fill" in style: return style["fill"][5:-1] return def has_clip(self): return bool(self.attr("clip-path")) def start(self, gradient): self.gradient = gradient self.ctx.write("\n// #%s" % self.attr("id")) if self.has_transform() or self.has_clip(): self.ctx.save() def draw(self): data = self.get_data() style = self.get_style() self.ctx.beginPath() if self.has_transform(): trans_matrix = self.get_transform() self.ctx.transform(*trans_matrix) # unpacks argument list if self.has_gradient(): self.gradient.draw() self.set_style(style) # unpacks "data" in parameters to given method getattr(self.ctx, self.command)(*data) self.ctx.closePath() def end(self): if self.has_transform() or self.has_clip(): self.ctx.restore() class G(AbstractShape): def draw(self): #get layer label, if exists gtype = self.attr("groupmode", "inkscape") or "group" if self.has_transform(): trans_matrix = self.get_transform() self.ctx.transform(*trans_matrix) class Rect(AbstractShape): def get_data(self): x = self.attr("x") y = self.attr("y") w = self.attr("width") h = self.attr("height") rx = self.attr("rx") or 0 ry = self.attr("ry") or 0 return x, y, w, h, rx, ry class Circle(AbstractShape): def __init__(self, command, node, ctx): AbstractShape.__init__(self, command, node, ctx) self.command = "arc" def get_data(self): import math cx = self.attr("cx") cy = self.attr("cy") r = self.attr("r") return cx, cy, r, 0, math.pi * 2, True class Ellipse(AbstractShape): def get_data(self): cx = self.attr("cx") cy = self.attr("cy") rx = self.attr("rx") ry = self.attr("ry") return cx, cy, rx, ry def draw(self): import math cx, cy, rx, ry = self.get_data() style = self.get_style() self.ctx.beginPath() if self.has_transform(): trans_matrix = self.get_transform() self.ctx.transform(*trans_matrix) # unpacks argument list self.set_style(style) KAPPA = 4 * ((math.sqrt(2) - 1) / 3) self.ctx.moveTo(cx, cy - ry) self.ctx.bezierCurveTo(cx + (KAPPA * rx), cy - ry, cx + rx, cy - (KAPPA * ry), cx + rx, cy) self.ctx.bezierCurveTo(cx + rx, cy + (KAPPA * ry), cx + (KAPPA * rx), cy + ry, cx, cy + ry) self.ctx.bezierCurveTo(cx - (KAPPA * rx), cy + ry, cx - rx, cy + (KAPPA * ry), cx - rx, cy) self.ctx.bezierCurveTo(cx - rx, cy - (KAPPA * ry), cx - (KAPPA * rx), cy - ry, cx, cy - ry) self.ctx.closePath() class Path(AbstractShape): def get_data(self): #path data is already converted to float return parsePath(self.attr("d")) def pathMoveTo(self, data): self.ctx.moveTo(data[0], data[1]) self.currentPosition = data[0], data[1] def pathLineTo(self, data): self.ctx.lineTo(data[0], data[1]) self.currentPosition = data[0], data[1] def pathCurveTo(self, data): x1, y1, x2, y2 = data[0], data[1], data[2], data[3] x, y = data[4], data[5] self.ctx.bezierCurveTo(x1, y1, x2, y2, x, y) self.currentPosition = x, y def pathArcTo(self, data): #http://www.w3.org/TR/SVG11/implnote.html#ArcImplementationNotes # code adapted from http://code.google.com/p/canvg/ import math x1 = self.currentPosition[0] y1 = self.currentPosition[1] x2 = data[5] y2 = data[6] rx = data[0] ry = data[1] angle = data[2] * (math.pi / 180.0) arcflag = data[3] sweepflag = data[4] #compute (x1', y1') _x1 = math.cos(angle) * (x1 - x2) / 2.0 + math.sin(angle) * (y1 - y2) / 2.0 _y1 = -math.sin(angle) * (x1 - x2) / 2.0 + math.cos(angle) * (y1 - y2) / 2.0 #adjust radii l = _x1**2 / rx**2 + _y1**2 / ry**2 if l > 1: rx *= math.sqrt(l) ry *= math.sqrt(l) #compute (cx', cy') numr = (rx**2 * ry**2) - (rx**2 * _y1**2) - (ry**2 * _x1**2) demr = (rx**2 * _y1**2) + (ry**2 * _x1**2) sig = -1 if arcflag == sweepflag else 1 sig = sig * math.sqrt(numr / demr) if math.isnan(sig): sig = 0; _cx = sig * rx * _y1 / ry _cy = sig * -ry * _x1 / rx #compute (cx, cy) from (cx', cy') cx = (x1 + x2) / 2.0 + math.cos(angle) * _cx - math.sin(angle) * _cy cy = (y1 + y2) / 2.0 + math.sin(angle) * _cx + math.cos(angle) * _cy #compute startAngle & endAngle #vector magnitude m = lambda v: math.sqrt(v[0]**2 + v[1]**2) #ratio between two vectors r = lambda u, v: (u[0] * v[0] + u[1] * v[1]) / (m(u) * m(v)) #angle between two vectors a = lambda u, v: (-1 if u[0]*v[1] < u[1]*v[0] else 1) * math.acos(r(u,v)) #initial angle a1 = a([1,0], [(_x1 - _cx) / rx, (_y1 - _cy)/ry]) #angle delta u = [(_x1 - _cx) / rx, (_y1 - _cy) / ry] v = [(-_x1 - _cx) / rx, (-_y1 - _cy) / ry] ad = a(u, v) if r(u,v) <= -1: ad = math.pi if r(u,v) >= 1: ad = 0 if sweepflag == 0 and ad > 0: ad = ad - 2 * math.pi; if sweepflag == 1 and ad < 0: ad = ad + 2 * math.pi; r = rx if rx > ry else ry sx = 1 if rx > ry else rx / ry sy = ry / rx if rx > ry else 1 self.ctx.translate(cx, cy) self.ctx.rotate(angle) self.ctx.scale(sx, sy) self.ctx.arc(0, 0, r, a1, a1 + ad, 1 - sweepflag) self.ctx.scale(1/sx, 1/sy) self.ctx.rotate(-angle) self.ctx.translate(-cx, -cy) self.currentPosition = x2, y2 def draw(self): path = self.get_data() style = self.get_style() """Gets the node type and calls the given method""" self.ctx.beginPath() if self.has_transform(): trans_matrix = self.get_transform() self.ctx.transform(*trans_matrix) # unpacks argument list self.set_style(style) """Draws path commands""" path_command = {"M": self.pathMoveTo, "L": self.pathLineTo, "C": self.pathCurveTo, "A": self.pathArcTo} for pt in path: comm, data = pt if comm in path_command: path_command[comm](data) self.ctx.closePath() class Line(Path): def get_data(self): x1 = self.attr("x1") y1 = self.attr("y1") x2 = self.attr("x2") y2 = self.attr("y2") return (("M", (x1, y1)), ("L", (x2, y2))) class Polygon(Path): def get_data(self): points = self.attr("points").strip().split(" ") points = map(lambda x: x.split(","), points) comm = [] for pt in points: # creating path command similar pt = map(float, pt) comm.append(["L", pt]) comm[0][0] = "M" # first command must be a 'M' => moveTo return comm class Polyline(Polygon): pass class Text(AbstractShape): def text_helper(self, tspan): if not len(tspan): return unicode(tspan.text) for ts in tspan: return ts.text + self.text_helper(ts) + ts.tail def set_text_style(self, style): keys = ("font-style", "font-weight", "font-size", "font-family") text = [] for key in keys: if key in style: text.append(style[key]) self.ctx.setFont(" ".join(text)) def get_data(self): x = self.attr("x") y = self.attr("y") return x, y def draw(self): x, y = self.get_data() style = self.get_style() if self.has_transform(): trans_matrix = self.get_transform() self.ctx.transform(*trans_matrix) # unpacks argument list self.set_style(style) self.set_text_style(style) for tspan in self.node: text = self.text_helper(tspan) _x = float(tspan.get("x")) _y = float(tspan.get("y")) self.ctx.fillText(text, _x, _y)