Source code for svg.geomsvg

#-----------------------------------------------------------------------------
# Copyright 2012-2016 Claude Zervas
# email: claude@utlco.com
#-----------------------------------------------------------------------------
"""
Methods for converting SVG shape elements to geometry objects.
"""
# Python 3 compatibility boilerplate
from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
from future_builtins import *

import math
import logging

import geom

from geom import transform2d
from geom import bezier

from . import svg

logger = logging.getLogger(__name__)


[docs]def svg_to_geometry(svg_elements, parent_transform=None): """Convert the SVG shape elements to Line, Arc, and/or CubicBezier segments, and apply node/parent transforms. The coordinates of the segments will be absolute with respect to the parent container. Args: svg_elements: An iterable collection of 2-tuples consisting of SVG Element node and transform matrix. parent_transform: An optional parent transform to apply to all nodes. Default is None. Returns: A list of paths, where a path is a list of one or more segments made of Line, Arc, or CubicBezier objects. """ path_list = [] for element, element_transform in svg_elements: transformed_paths = svg_element_to_geometry(element, element_transform, parent_transform) if transformed_paths: path_list.extend(transformed_paths) return path_list
[docs]def svg_element_to_geometry(element, element_transform=None, parent_transform=None): """Convert the SVG shape element to a list of one or more Line, Arc, and/or CubicBezier segments, and apply node/parent transforms. The coordinates of the segments will be absolute with respect to the parent container. Args: element: An SVG Element shape node. element_transform: An optional transform to apply to the element. Default is None. parent_transform: An optional parent transform to apply to the element. Default is None. Returns: A list of zero or more paths. A path being a list of zero or more Line, Arc, EllipticalArc, or CubicBezier objects. """ # Convert the element to a list of subpaths subpath_list = [] tag = svg.strip_ns(element.tag) # tag stripped of namespace part if tag == 'path': d = element.get('d') if d is not None and d: subpath_list = parse_path_geom(d, ellipse_to_bezier=True) else: subpath = [] if tag == 'line': subpath = convert_line(element) elif tag == 'ellipse': ellipse = convert_ellipse(element) subpath = bezier.bezier_ellipse(ellipse) elif tag == 'rect': subpath = convert_rect(element) elif tag == 'circle': subpath = convert_circle(element) elif tag == 'polyline': subpath = convert_polyline(element) elif tag == 'polygon': subpath = convert_polygon(element) if subpath: subpath_list = [subpath, ] if subpath_list: # Create a transform matrix that is composed of the # parent transform and the element transform # so that control points are in absolute coordinates. if parent_transform is not None: element_transform = transform2d.compose_transform(parent_transform, element_transform) if element_transform is not None: x_subpath_list = [] for subpath in subpath_list: x_subpath = [] for segment in subpath: # Skip zero-length segments. if not segment.p1 == segment.p2: segment = segment.transform(element_transform) x_subpath.append(segment) x_subpath_list.append(x_subpath) return x_subpath_list return subpath_list
[docs]def parse_path_geom(path_data, ellipse_to_bezier=False): """ Parse SVG path data and convert to geometry objects. Args: path_data: The `d` attribute value of an SVG path element. ellipse_to_bezier: Convert elliptical arcs to bezier curves if True. Default is False. Returns: A list of zero or more subpaths. A subpath being a list of zero or more Line, Arc, EllipticalArc, or CubicBezier objects. """ subpath = [] subpath_list = [] p1 = (0.0, 0.0) for cmd, params in svg.parse_path(path_data): p2 = (params[-2], params[-1]) if cmd == 'M': # Start of path or sub-path if subpath: subpath_list.append(subpath) subpath = [] elif cmd == 'L': subpath.append(geom.Line(p1, p2)) elif cmd == 'A': rx = params[0] ry = params[1] phi = params[2] large_arc = params[3] sweep_flag = params[4] elliptical_arc = geom.ellipse.EllipticalArc.from_endpoints( p1, p2, rx, ry, large_arc, sweep_flag, phi) if elliptical_arc is None: # Parameters must be degenerate... # Try just making a line logger = logging.getLogger(__name__) logger.debug('Degenerate arc...') subpath.append(geom.Line(p1, p2)) elif geom.float_eq(rx, ry): # If it's a circular arc then create one using # the previously computed ellipse parameters. segment = geom.Arc(p1, p2, rx, elliptical_arc.sweep_angle, elliptical_arc.center) subpath.append(segment) elif ellipse_to_bezier: # Convert the elliptical arc to cubic Beziers subpath.extend(bezier.bezier_ellipse(elliptical_arc)) else: subpath.append(elliptical_arc) elif cmd == 'C': c1 = (params[0], params[1]) c2 = (params[2], params[3]) subpath.append(bezier.CubicBezier(p1, c1, c2, p2)) elif cmd == 'Q': c1 = (params[0], params[1]) subpath.append(bezier.CubicBezier.from_quadratic(p1, c1, p2)) p1 = p2 if subpath: subpath_list.append(subpath) return subpath_list
[docs]def convert_rect(element): """Convert an SVG rect shape element to four geom.Line segments. Args: element: An SVG 'rect' element of the form <rect x='X' y='Y' width='W' height='H'/> Returns: A clockwise wound polygon as a list of geom.Line segments. """ # Convert to a clockwise wound polygon x1 = float(element.get('x', 0)) y1 = float(element.get('y', 0)) x2 = x1 + float(element.get('width', 0)) y2 = y1 + float(element.get('height', 0)) p1 = (x1, y1) p2 = (x1, y2) p3 = (x2, y2) p4 = (x2, y1) return [geom.Line(p1, p2), geom.Line(p2, p3), geom.Line(p3, p4), geom.Line(p4, p1)]
[docs]def convert_line(element): """Convert an SVG line shape element to a geom.Line. Args: element: An SVG 'line' element of the form: <line x1='X1' y1='Y1' x2='X2' y2='Y2/> Returns: A line segment: geom.Line((x1, y1), (x2, y2)) """ x1 = float(element.get('x1', 0)) y1 = float(element.get('y1', 0)) x2 = float(element.get('x2', 0)) y2 = float(element.get('y2', 0)) return geom.Line((x1, y1), (x2, y2))
[docs]def convert_circle(element): """Convert an SVG circle shape element to four circular arc segments. Args: element: An SVG 'circle' element of the form: <circle r='RX' cx='X' cy='Y'/> Returns: A counter-clockwise wound list of four circular geom.Arc segments. """ # Convert to four arcs. CCW winding. r = abs(float(element.get('r', 0))) cx = float(element.get('cx', 0)) cy = float(element.get('cy', 0)) center = (cx, cy) p1 = (cx + r, cy) p2 = (cx, cy + r) p3 = (cx - r, cy) p4 = (cx, cy - r) a1 = geom.Arc(p1, p2, r, math.pi / 2, center) a2 = geom.Arc(p2, p3, r, math.pi / 2, center) a3 = geom.Arc(p3, p4, r, math.pi / 2, center) a4 = geom.Arc(p4, p1, r, math.pi / 2, center) return [a1, a2, a3, a4]
[docs]def convert_ellipse(element): """Convert an SVG ellipse shape element to a geom.Ellipse. Args: element: An SVG 'ellipse' element of the form: <ellipse rx='RX' ry='RY' cx='X' cy='Y'/> Returns: A geom.Ellipse. """ rx = float(element.get('rx', 0)) ry = float(element.get('ry', 0)) cx = float(element.get('cx', 0)) cy = float(element.get('cy', 0)) return geom.ellipse.Ellipse((cx, cy), rx, ry)
[docs]def convert_polyline(element): """Convert an SVG `polyline` shape element to a list of line segments. Args: element: An SVG 'polyline' element of the form: <polyline points='x1,y1 x2,y2 x3,y3 [...]'/> Returns: A list of geom.Line segments. """ segments = [] points = element.get('points', '').split() sx, sy = points[0].split(',') start_p = geom.P(float(sx), float(sy)) prev_p = start_p for point in points[1:]: sx, sy = point.split(',') p = geom.P(float(sx), float(sy)) segments.append(geom.Line(prev_p, p)) prev_p = p return segments
[docs]def convert_polygon(element): """Convert an SVG `polygon` shape element to a list line segments. Args: element: An SVG 'polygon' element of the form: <polygon points='x1,y1 x2,y2 x3,y3 [...]'/> Returns: A list of geom.Line segments. The polygon will be closed. """ segments = convert_polyline(element) # Close the polygon if not already so if len(segments) > 1 and segments[-1] != segments[0]: segments.append(geom.Line(segments[-1], segments[0])) return segments