Sat, 07 Nov 2015 13:33:12 +0100
added basic svg parser library
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svg2gcode/svg/__init__.py Sat Nov 07 13:33:12 2015 +0100 @@ -0,0 +1,8 @@ +#__all__ = ['geometry', 'svg'] + +from .svg import * + +def parse(filename): + f = svg.Svg(filename) + return f +
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svg2gcode/svg/geometry.py Sat Nov 07 13:33:12 2015 +0100 @@ -0,0 +1,343 @@ +# Copyright (C) 2013 -- CJlano < cjlano @ free.fr > + +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +''' +This module contains all the geometric classes and functions not directly +related to SVG parsing. It can be reused outside the scope of SVG. +''' + +import math +import numbers +import operator + +class Point: + def __init__(self, x=None, y=None): + '''A Point is defined either by a tuple/list of length 2 or + by 2 coordinates + >>> Point(1,2) + (1.000,2.000) + >>> Point((1,2)) + (1.000,2.000) + >>> Point([1,2]) + (1.000,2.000) + >>> Point('1', '2') + (1.000,2.000) + >>> Point(('1', None)) + (1.000,0.000) + ''' + if (isinstance(x, tuple) or isinstance(x, list)) and len(x) == 2: + x,y = x + + # Handle empty parameter(s) which should be interpreted as 0 + if x is None: x = 0 + if y is None: y = 0 + + try: + self.x = float(x) + self.y = float(y) + except: + raise TypeError("A Point is defined by 2 numbers or a tuple") + + def __add__(self, other): + '''Add 2 points by adding coordinates. + Try to convert other to Point if necessary + >>> Point(1,2) + Point(3,2) + (4.000,4.000) + >>> Point(1,2) + (3,2) + (4.000,4.000)''' + if not isinstance(other, Point): + try: other = Point(other) + except: return NotImplemented + return Point(self.x + other.x, self.y + other.y) + + def __sub__(self, other): + '''Substract two Points. + >>> Point(1,2) - Point(3,2) + (-2.000,0.000) + ''' + if not isinstance(other, Point): + try: other = Point(other) + except: return NotImplemented + return Point(self.x - other.x, self.y - other.y) + + def __mul__(self, other): + '''Multiply a Point with a constant. + >>> 2 * Point(1,2) + (2.000,4.000) + >>> Point(1,2) * Point(1,2) #doctest:+IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError: + ''' + if not isinstance(other, numbers.Real): + return NotImplemented + return Point(self.x * other, self.y * other) + def __rmul__(self, other): + return self.__mul__(other) + + def __eq__(self, other): + '''Test equality + >>> Point(1,2) == (1,2) + True + >>> Point(1,2) == Point(2,1) + False + ''' + if not isinstance(other, Point): + try: other = Point(other) + except: return NotImplemented + return (self.x == other.x) and (self.y == other.y) + + def __repr__(self): + return '(' + format(self.x,'.3f') + ',' + format( self.y,'.3f') + ')' + + def __str__(self): + return self.__repr__(); + + def coord(self): + '''Return the point tuple (x,y)''' + return (self.x, self.y) + + def length(self): + '''Vector length, Pythagoras theorem''' + return math.sqrt(self.x ** 2 + self.y ** 2) + + def rot(self, angle): + '''Rotate vector [Origin,self] ''' + if not isinstance(angle, Angle): + try: angle = Angle(angle) + except: return NotImplemented + x = self.x * angle.cos - self.y * angle.sin + y = self.x * angle.sin + self.y * angle.cos + return Point(x,y) + + +class Angle: + '''Define a trigonometric angle [of a vector] ''' + def __init__(self, arg): + if isinstance(arg, numbers.Real): + # We precompute sin and cos for rotations + self.angle = arg + self.cos = math.cos(self.angle) + self.sin = math.sin(self.angle) + elif isinstance(arg, Point): + # Point angle is the trigonometric angle of the vector [origin, Point] + pt = arg + try: + self.cos = pt.x/pt.length() + self.sin = pt.y/pt.length() + except ZeroDivisionError: + self.cos = 1 + self.sin = 0 + + self.angle = math.acos(self.cos) + if self.sin < 0: + self.angle = -self.angle + else: + raise TypeError("Angle is defined by a number or a Point") + + def __neg__(self): + return Angle(Point(self.cos, -self.sin)) + +class Segment: + '''A segment is an object defined by 2 points''' + def __init__(self, start, end): + self.start = start + self.end = end + + def __str__(self): + return 'Segment from ' + str(self.start) + ' to ' + str(self.end) + + def segments(self, precision=0): + ''' Segments is simply the segment start -> end''' + return [self.start, self.end] + + def length(self): + '''Segment length, Pythagoras theorem''' + s = self.end - self.start + return math.sqrt(s.x ** 2 + s.y ** 2) + + def pdistance(self, p): + '''Perpendicular distance between this Segment and a given Point p''' + if not isinstance(p, Point): + return NotImplemented + + if self.start == self.end: + # Distance from a Point to another Point is length of a segment + return Segment(self.start, p).length() + + s = self.end - self.start + if s.x == 0: + # Vertical Segment => pdistance is the difference of abscissa + return abs(self.start.x - p.x) + else: + # That's 2-D perpendicular distance formulae (ref: Wikipedia) + slope = s.y/s.x + # intercept: Crossing with ordinate y-axis + intercept = self.start.y - (slope * self.start.x) + return abs(slope * p.x - p.y + intercept) / math.sqrt(slope ** 2 + 1) + + + def bbox(self): + xmin = min(self.start.x, self.end.x) + xmax = max(self.start.x, self.end.x) + ymin = min(self.start.y, self.end.y) + ymax = max(self.start.y, self.end.y) + + return (Point(xmin,ymin),Point(xmax,ymax)) + + def transform(self, matrix): + self.start = matrix * self.start + self.end = matrix * self.end + + def scale(self, ratio): + self.start *= ratio + self.end *= ratio + def translate(self, offset): + self.start += offset + self.end += offset + def rotate(self, angle): + self.start = self.start.rot(angle) + self.end = self.end.rot(angle) + +class Bezier: + '''Bezier curve class + A Bezier curve is defined by its control points + Its dimension is equal to the number of control points + Note that SVG only support dimension 3 and 4 Bezier curve, respectively + Quadratic and Cubic Bezier curve''' + def __init__(self, pts): + self.pts = list(pts) + self.dimension = len(pts) + + def __str__(self): + return 'Bezier' + str(self.dimension) + \ + ' : ' + ", ".join([str(x) for x in self.pts]) + + def control_point(self, n): + if n >= self.dimension: + raise LookupError('Index is larger than Bezier curve dimension') + else: + return self.pts[n] + + def rlength(self): + '''Rough Bezier length: length of control point segments''' + pts = list(self.pts) + l = 0.0 + p1 = pts.pop() + while pts: + p2 = pts.pop() + l += Segment(p1, p2).length() + p1 = p2 + return l + + def bbox(self): + return self.rbbox() + + def rbbox(self): + '''Rough bounding box: return the bounding box (P1,P2) of the Bezier + _control_ points''' + xmin = min([p.x for p in self.pts]) + xmax = max([p.x for p in self.pts]) + ymin = min([p.y for p in self.pts]) + ymax = max([p.y for p in self.pts]) + + return (Point(xmin,ymin), Point(xmax,ymax)) + + def segments(self, precision=0): + '''Return a polyline approximation ("segments") of the Bezier curve + precision is the minimum significative length of a segment''' + segments = [] + # n is the number of Bezier points to draw according to precision + if precision != 0: + n = int(self.rlength() / precision) + 1 + else: + n = 1000 + if n < 10: n = 10 + if n > 1000 : n = 1000 + + for t in range(0, n+1): + segments.append(self._bezierN(float(t)/n)) + return segments + + def _bezier1(self, p0, p1, t): + '''Bezier curve, one dimension + Compute the Point corresponding to a linear Bezier curve between + p0 and p1 at "time" t ''' + pt = p0 + t * (p1 - p0) + return pt + + def _bezierN(self, t): + '''Bezier curve, Nth dimension + Compute the point of the Nth dimension Bezier curve at "time" t''' + # We reduce the N Bezier control points by computing the linear Bezier + # point of each control point segment, creating N-1 control points + # until we reach one single point + res = list(self.pts) + # We store the resulting Bezier points in res[], recursively + for n in range(self.dimension, 1, -1): + # For each control point of nth dimension, + # compute linear Bezier point a t + for i in range(0,n-1): + res[i] = self._bezier1(res[i], res[i+1], t) + return res[0] + + def transform(self, matrix): + self.pts = [matrix * x for x in self.pts] + + def scale(self, ratio): + self.pts = [x * ratio for x in self.pts] + def translate(self, offset): + self.pts = [x + offset for x in self.pts] + def rotate(self, angle): + self.pts = [x.rot(angle) for x in self.pts] + +class MoveTo: + def __init__(self, dest): + self.dest = dest + + def bbox(self): + return (self.dest, self.dest) + + def transform(self, matrix): + self.dest = matrix * self.dest + + def scale(self, ratio): + self.dest *= ratio + def translate(self, offset): + self.dest += offset + def rotate(self, angle): + self.dest = self.dest.rot(angle) + + +def simplify_segment(segment, epsilon): + '''Ramer-Douglas-Peucker algorithm''' + if len(segment) < 3 or epsilon <= 0: + return segment[:] + + l = Segment(segment[0], segment[-1]) # Longest segment + + # Find the furthest point from the segment + index, maxDist = max([(i, l.pdistance(p)) for i,p in enumerate(segment)], + key=operator.itemgetter(1)) + + if maxDist > epsilon: + # Recursively call with segment splited in 2 on its furthest point + r1 = simplify_segment(segment[:index+1], epsilon) + r2 = simplify_segment(segment[index:], epsilon) + # Remove redundant 'middle' Point + return r1[:-1] + r2 + else: + return [segment[0], segment[-1]]
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/svg2gcode/svg/svg.py Sat Nov 07 13:33:12 2015 +0100 @@ -0,0 +1,680 @@ +# SVG parser in Python + +# Copyright (C) 2013 -- CJlano < cjlano @ free.fr > + +# 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., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +from __future__ import absolute_import +import sys +import os +import copy +import re +import xml.etree.ElementTree as etree +import itertools +import operator +import json +from .geometry import * + +svg_ns = '{http://www.w3.org/2000/svg}' + +# Regex commonly used +number_re = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?' +unit_re = r'em|ex|px|in|cm|mm|pt|pc|%' + +# Unit converter +unit_convert = { + None: 1, # Default unit (same as pixel) + 'px': 1, # px: pixel. Default SVG unit + 'em': 10, # 1 em = 10 px FIXME + 'ex': 5, # 1 ex = 5 px FIXME + 'in': 96, # 1 in = 96 px + 'cm': 96 / 2.54, # 1 cm = 1/2.54 in + 'mm': 96 / 25.4, # 1 mm = 1/25.4 in + 'pt': 96 / 72.0, # 1 pt = 1/72 in + 'pc': 96 / 6.0, # 1 pc = 1/6 in + '%' : 1 / 100.0 # 1 percent + } + +class Transformable: + '''Abstract class for objects that can be geometrically drawn & transformed''' + def __init__(self, elt=None): + # a 'Transformable' is represented as a list of Transformable items + self.items = [] + self.id = hex(id(self)) + # Unit transformation matrix on init + self.matrix = Matrix() + self.viewport = Point(800, 600) # default viewport is 800x600 + if elt is not None: + self.id = elt.get('id', self.id) + # Parse transform attibute to update self.matrix + self.getTransformations(elt) + + def bbox(self): + '''Bounding box''' + bboxes = [x.bbox() for x in self.items] + xmin = min([b[0].x for b in bboxes]) + xmax = max([b[1].x for b in bboxes]) + ymin = min([b[0].y for b in bboxes]) + ymax = max([b[1].y for b in bboxes]) + + return (Point(xmin,ymin), Point(xmax,ymax)) + + # Parse transform field + def getTransformations(self, elt): + t = elt.get('transform') + if t is None: return + + svg_transforms = [ + 'matrix', 'translate', 'scale', 'rotate', 'skewX', 'skewY'] + + # match any SVG transformation with its parameter (until final parenthese) + # [^)]* == anything but a closing parenthese + # '|'.join == OR-list of SVG transformations + transforms = re.findall( + '|'.join([x + '[^)]*\)' for x in svg_transforms]), t) + + for t in transforms: + op, arg = t.split('(') + op = op.strip() + # Keep only numbers + arg = [float(x) for x in re.findall(number_re, arg)] + print('transform: ' + op + ' '+ str(arg)) + + if op == 'matrix': + self.matrix *= Matrix(arg) + + if op == 'translate': + tx = arg[0] + if len(arg) == 1: ty = 0 + else: ty = arg[1] + self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) + + if op == 'scale': + sx = arg[0] + if len(arg) == 1: sy = sx + else: sy = arg[1] + self.matrix *= Matrix([sx, 0, 0, sy, 0, 0]) + + if op == 'rotate': + cosa = math.cos(math.radians(arg[0])) + sina = math.sin(math.radians(arg[0])) + if len(arg) != 1: + tx, ty = arg[1:3] + self.matrix *= Matrix([1, 0, 0, 1, tx, ty]) + self.matrix *= Matrix([cosa, sina, -sina, cosa, 0, 0]) + if len(arg) != 1: + self.matrix *= Matrix([1, 0, 0, 1, -tx, -ty]) + + if op == 'skewX': + tana = math.tan(math.radians(arg[0])) + self.matrix *= Matrix([1, 0, tana, 1, 0, 0]) + + if op == 'skewY': + tana = math.tan(math.radians(arg[0])) + self.matrix *= Matrix([1, tana, 0, 1, 0, 0]) + + def transform(self, matrix=None): + for x in self.items: + x.transform(self.matrix) + + def length(self, v, mode='xy'): + # Handle empty (non-existing) length element + if v is None: + return 0 + + # Get length value + m = re.search(number_re, v) + if m: value = m.group(0) + else: raise TypeError(v + 'is not a valid length') + + # Get length unit + m = re.search(unit_re, v) + if m: unit = m.group(0) + else: unit = None + + if unit == '%': + if mode == 'x': + return float(value) * unit_convert[unit] * self.viewport.x + if mode == 'y': + return float(value) * unit_convert[unit] * self.viewport.y + if mode == 'xy': + return float(value) * unit_convert[unit] * self.viewport.x # FIXME + + return float(value) * unit_convert[unit] + + def xlength(self, x): + return self.length(x, 'x') + def ylength(self, y): + return self.length(y, 'y') + + def flatten(self): + '''Flatten the SVG objects nested list into a flat (1-D) list, + removing Groups''' + # http://rightfootin.blogspot.fr/2006/09/more-on-python-flatten.html + # Assigning a slice a[i:i+1] with a list actually replaces the a[i] + # element with the content of the assigned list + i = 0 + flat = copy.deepcopy(self.items) + while i < len(flat): + while isinstance(flat[i], Group): + flat[i:i+1] = flat[i].items + i += 1 + return flat + + def scale(self, ratio): + for x in self.items: + x.scale(ratio) + return self + + def translate(self, offset): + for x in self.items: + x.translate(offset) + return self + + def rotate(self, angle): + for x in self.items: + x.rotate(angle) + return self + +class Svg(Transformable): + '''SVG class: use parse to parse a file''' + # class Svg handles the <svg> tag + # tag = 'svg' + + def __init__(self, filename=None): + Transformable.__init__(self) + if filename: + self.parse(filename) + + def parse(self, filename): + self.filename = filename + tree = etree.parse(filename) + self.root = tree.getroot() + if self.root.tag != svg_ns + 'svg': + raise TypeError('file %s does not seem to be a valid SVG file', filename) + + # Create a top Group to group all other items (useful for viewBox elt) + top_group = Group() + self.items.append(top_group) + + # SVG dimension + width = self.xlength(self.root.get('width')) + height = self.ylength(self.root.get('height')) + # update viewport + top_group.viewport = Point(width, height) + + # viewBox + if self.root.get('viewBox') is not None: + viewBox = re.findall(number_re, self.root.get('viewBox')) + sx = width / float(viewBox[2]) + sy = height / float(viewBox[3]) + tx = -float(viewBox[0]) + ty = -float(viewBox[1]) + top_group.matrix = Matrix([sx, 0, 0, sy, tx, ty]) + + # Parse XML elements hierarchically with groups <g> + top_group.append(self.root) + + self.transform() + + def title(self): + t = self.root.find(svg_ns + 'title') + if t is not None: + return t + else: + return os.path.splitext(os.path.basename(self.filename))[0] + + def json(self): + return self.items + + +class Group(Transformable): + '''Handle svg <g> elements''' + # class Group handles the <g> tag + tag = 'g' + + def __init__(self, elt=None): + Transformable.__init__(self, elt) + + def append(self, element): + for elt in element: + elt_class = svgClass.get(elt.tag, None) + if elt_class is None: + print('No handler for element %s' % elt.tag) + continue + # instanciate elt associated class (e.g. <path>: item = Path(elt) + item = elt_class(elt) + # Apply group matrix to the newly created object + item.matrix = self.matrix * item.matrix + item.viewport = self.viewport + + self.items.append(item) + # Recursively append if elt is a <g> (group) + if elt.tag == svg_ns + 'g': + item.append(elt) + + def __repr__(self): + return '<Group ' + self.id + '>: ' + repr(self.items) + + def json(self): + return {'Group ' + self.id : self.items} + +class Matrix: + ''' SVG transformation matrix and its operations + a SVG matrix is represented as a list of 6 values [a, b, c, d, e, f] + (named vect hereafter) which represent the 3x3 matrix + ((a, c, e) + (b, d, f) + (0, 0, 1)) + see http://www.w3.org/TR/SVG/coords.html#EstablishingANewUserSpace ''' + + def __init__(self, vect=[1, 0, 0, 1, 0, 0]): + # Unit transformation vect by default + if len(vect) != 6: + raise ValueError("Bad vect size %d" % len(vect)) + self.vect = list(vect) + + def __mul__(self, other): + '''Matrix multiplication''' + if isinstance(other, Matrix): + a = self.vect[0] * other.vect[0] + self.vect[2] * other.vect[1] + b = self.vect[1] * other.vect[0] + self.vect[3] * other.vect[1] + c = self.vect[0] * other.vect[2] + self.vect[2] * other.vect[3] + d = self.vect[1] * other.vect[2] + self.vect[3] * other.vect[3] + e = self.vect[0] * other.vect[4] + self.vect[2] * other.vect[5] \ + + self.vect[4] + f = self.vect[1] * other.vect[4] + self.vect[3] * other.vect[5] \ + + self.vect[5] + return Matrix([a, b, c, d, e, f]) + + elif isinstance(other, Point): + x = other.x * self.vect[0] + other.y * self.vect[2] + self.vect[4] + y = other.x * self.vect[1] + other.y * self.vect[3] + self.vect[5] + return Point(x,y) + + else: + return NotImplemented + + def __str__(self): + return str(self.vect) + + def xlength(self, x): + return x * self.vect[0] + def ylength(self, y): + return y * self.vect[3] + + +COMMANDS = 'MmZzLlHhVvCcSsQqTtAa' + +class Path(Transformable): + '''SVG <path>''' + # class Path handles the <path> tag + tag = 'path' + + def __init__(self, elt=None): + Transformable.__init__(self, elt) + if elt is not None: + self.style = elt.get('style') + self.parse(elt.get('d')) + + def parse(self, pathstr): + """Parse path string and build elements list""" + + pathlst = re.findall(number_re + r"|\ *[%s]\ *" % COMMANDS, pathstr) + + pathlst.reverse() + + command = None + current_pt = Point(0,0) + start_pt = None + + while pathlst: + if pathlst[-1].strip() in COMMANDS: + last_command = command + command = pathlst.pop().strip() + absolute = (command == command.upper()) + command = command.upper() + else: + if command is None: + raise ValueError("No command found at %d" % len(pathlst)) + + if command == 'M': + # MoveTo + x = pathlst.pop() + y = pathlst.pop() + pt = Point(x, y) + if absolute: + current_pt = pt + else: + current_pt += pt + start_pt = current_pt + + self.items.append(MoveTo(current_pt)) + + # MoveTo with multiple coordinates means LineTo + command = 'L' + + elif command == 'Z': + # Close Path + l = Segment(current_pt, start_pt) + self.items.append(l) + + + elif command in 'LHV': + # LineTo, Horizontal & Vertical line + # extra coord for H,V + if absolute: + x,y = current_pt.coord() + else: + x,y = (0,0) + + if command in 'LH': + x = pathlst.pop() + if command in 'LV': + y = pathlst.pop() + + pt = Point(x, y) + if not absolute: + pt += current_pt + + self.items.append(Segment(current_pt, pt)) + current_pt = pt + + elif command in 'CQ': + dimension = {'Q':3, 'C':4} + bezier_pts = [] + bezier_pts.append(current_pt) + for i in range(1,dimension[command]): + x = pathlst.pop() + y = pathlst.pop() + pt = Point(x, y) + if not absolute: + pt += current_pt + bezier_pts.append(pt) + + self.items.append(Bezier(bezier_pts)) + current_pt = pt + + elif command in 'TS': + # number of points to read + nbpts = {'T':1, 'S':2} + # the control point, from previous Bezier to mirror + ctrlpt = {'T':1, 'S':2} + # last command control + last = {'T': 'QT', 'S':'CS'} + + bezier_pts = [] + bezier_pts.append(current_pt) + + if last_command in last[command]: + pt0 = self.items[-1].control_point(ctrlpt[command]) + else: + pt0 = current_pt + pt1 = current_pt + # Symetrical of pt1 against pt0 + bezier_pts.append(pt1 + pt1 - pt0) + + for i in range(0,nbpts[command]): + x = pathlst.pop() + y = pathlst.pop() + pt = Point(x, y) + if not absolute: + pt += current_pt + bezier_pts.append(pt) + + self.items.append(Bezier(bezier_pts)) + current_pt = pt + + elif command == 'A': + rx = pathlst.pop() + ry = pathlst.pop() + xrot = pathlst.pop() + # Arc flags are not necesarily sepatated numbers + flags = pathlst.pop().strip() + large_arc_flag = flags[0] + if large_arc_flag not in '01': + print('Arc parsing failure') + break + + if len(flags) > 1: flags = flags[1:].strip() + else: flags = pathlst.pop().strip() + sweep_flag = flags[0] + if sweep_flag not in '01': + print('Arc parsing failure') + break + + if len(flags) > 1: x = flags[1:] + else: x = pathlst.pop() + y = pathlst.pop() + # TODO + print('ARC: ' + + ', '.join([rx, ry, xrot, large_arc_flag, sweep_flag, x, y])) +# self.items.append( +# Arc(rx, ry, xrot, large_arc_flag, sweep_flag, Point(x, y))) + + else: + pathlst.pop() + + def __str__(self): + return '\n'.join(str(x) for x in self.items) + + def __repr__(self): + return '<Path ' + self.id + '>' + + def segments(self, precision=0): + '''Return a list of segments, each segment is ended by a MoveTo. + A segment is a list of Points''' + ret = [] + # group items separated by MoveTo + for moveTo, group in itertools.groupby(self.items, + lambda x: isinstance(x, MoveTo)): + # Use only non MoveTo item + if not moveTo: + # Generate segments for each relevant item + seg = [x.segments(precision) for x in group] + # Merge all segments into one + ret.append(list(itertools.chain.from_iterable(seg))) + + return ret + + def simplify(self, precision): + '''Simplify segment with precision: + Remove any point which are ~aligned''' + ret = [] + for seg in self.segments(precision): + ret.append(simplify_segment(seg, precision)) + + return ret + +class Ellipse(Transformable): + '''SVG <ellipse>''' + # class Ellipse handles the <ellipse> tag + tag = 'ellipse' + + def __init__(self, elt=None): + Transformable.__init__(self, elt) + if elt is not None: + self.center = Point(self.xlength(elt.get('cx')), + self.ylength(elt.get('cy'))) + self.rx = self.length(elt.get('rx')) + self.ry = self.length(elt.get('ry')) + self.style = elt.get('style') + + def __repr__(self): + return '<Ellipse ' + self.id + '>' + + def bbox(self): + '''Bounding box''' + pmin = self.center - Point(self.rx, self.ry) + pmax = self.center + Point(self.rx, self.ry) + return (pmin, pmax) + + def transform(self, matrix): + self.center = self.matrix * self.center + self.rx = self.matrix.xlength(self.rx) + self.ry = self.matrix.ylength(self.ry) + + def scale(self, ratio): + self.center *= ratio + self.rx *= ratio + self.ry *= ratio + def translate(self, offset): + self.center += offset + def rotate(self, angle): + self.center = self.center.rot(angle) + + def P(self, t): + '''Return a Point on the Ellipse for t in [0..1]''' + x = self.center.x + self.rx * math.cos(2 * math.pi * t) + y = self.center.y + self.ry * math.sin(2 * math.pi * t) + return Point(x,y) + + def segments(self, precision=0): + if max(self.rx, self.ry) < precision: + return [[self.center]] + + p = [(0,self.P(0)), (1, self.P(1))] + d = 2 * max(self.rx, self.ry) + + while d > precision: + for (t1,p1),(t2,p2) in zip(p[:-1],p[1:]): + t = t1 + (t2 - t1)/2. + d = Segment(p1, p2).pdistance(self.P(t)) + p.append((t, self.P(t))) + p.sort(key=operator.itemgetter(0)) + + ret = [x for t,x in p] + return [ret] + + def simplify(self, precision): + return self + +# A circle is a special type of ellipse where rx = ry = radius +class Circle(Ellipse): + '''SVG <circle>''' + # class Circle handles the <circle> tag + tag = 'circle' + + def __init__(self, elt=None): + if elt is not None: + elt.set('rx', elt.get('r')) + elt.set('ry', elt.get('r')) + Ellipse.__init__(self, elt) + + def __repr__(self): + return '<Circle ' + self.id + '>' + +class Rect(Transformable): + '''SVG <rect>''' + # class Rect handles the <rect> tag + tag = 'rect' + + def __init__(self, elt=None): + Transformable.__init__(self, elt) + if elt is not None: + self.P1 = Point(self.xlength(elt.get('x')), + self.ylength(elt.get('y'))) + + self.P2 = Point(self.P1.x + self.xlength(elt.get('width')), + self.P1.y + self.ylength(elt.get('height'))) + + def __repr__(self): + return '<Rect ' + self.id + '>' + + def bbox(self): + '''Bounding box''' + xmin = min([p.x for p in (self.P1, self.P2)]) + xmax = max([p.x for p in (self.P1, self.P2)]) + ymin = min([p.y for p in (self.P1, self.P2)]) + ymax = max([p.y for p in (self.P1, self.P2)]) + + return (Point(xmin,ymin), Point(xmax,ymax)) + + def transform(self, matrix): + self.P1 = self.matrix * self.P1 + self.P2 = self.matrix * self.P2 + + def segments(self, precision=0): + # A rectangle is built with a segment going thru 4 points + ret = [] + Pa = Point(self.P1.x, self.P2.y) + Pb = Point(self.P2.x, self.P1.y) + + ret.append([self.P1, Pa, self.P2, Pb, self.P1]) + return ret + + def simplify(self, precision): + return self.segments(precision) + +class Line(Transformable): + '''SVG <line>''' + # class Line handles the <line> tag + tag = 'line' + + def __init__(self, elt=None): + Transformable.__init__(self, elt) + if elt is not None: + self.P1 = Point(self.xlength(elt.get('x1')), + self.ylength(elt.get('y1'))) + self.P2 = Point(self.xlength(elt.get('x2')), + self.ylength(elt.get('y2'))) + self.segment = Segment(self.P1, self.P2) + + def __repr__(self): + return '<Line ' + self.id + '>' + + def bbox(self): + '''Bounding box''' + xmin = min([p.x for p in (self.P1, self.P2)]) + xmax = max([p.x for p in (self.P1, self.P2)]) + ymin = min([p.y for p in (self.P1, self.P2)]) + ymax = max([p.y for p in (self.P1, self.P2)]) + + return (Point(xmin,ymin), Point(xmax,ymax)) + + def transform(self, matrix): + self.P1 = self.matrix * self.P1 + self.P2 = self.matrix * self.P2 + self.segment = Segment(self.P1, self.P2) + + def segments(self, precision=0): + return [self.segment.segments()] + + def simplify(self, precision): + return self.segments(precision) + +# overwrite JSONEncoder for svg classes which have defined a .json() method +class JSONEncoder(json.JSONEncoder): + def default(self, obj): + if not isinstance(obj, tuple(svgClass.values() + [Svg])): + return json.JSONEncoder.default(self, obj) + + if not hasattr(obj, 'json'): + return repr(obj) + + return obj.json() + +## Code executed on module load ## + +# SVG tag handler classes are initialized here +# (classes must be defined before) +import inspect +svgClass = {} +# Register all classes with attribute 'tag' in svgClass dict +for name, cls in inspect.getmembers(sys.modules[__name__], inspect.isclass): + tag = getattr(cls, 'tag', None) + if tag: + svgClass[svg_ns + tag] = cls +