Newer
Older
GB_Printer / Dump / inkscape / share / extensions / ink2canvas / svg.py
#!/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)