Source code for petrify.formats.svg

"""
**Experimental** SVG read support.

This currently only works with SVG paths without certain complications:

- usage of fill-rule resulting in a single non-terminated (with the `Z` command)
  path resulting in unfilled areas.
- motion commands that move the "pen" without terminating the path.

"""
from svg.path import parse_path, Line, CubicBezier, QuadraticBezier, Arc
from .. import units
from ..geometry import valid_scalar
from ..plane import Matrix, Point, Polygon, ComplexPolygon
from ..decompose import trapezoidal
from ..solid import Basis, Node, PlanarPolygon, PolygonExtrusion, Union
import re
import xml.sax

u = units.u

exp = re.compile('(.*)\((.*)\)')
def parse_transform(s):
    # see https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/transform
    # especially for the matrix type
    name, params = exp.match(s).groups()
    params = [float(v) for v in params.split(',')]
    if name == 'matrix':
        a, b, c, d, e, f = params
        # a c e
        # b d f
        # 0 0 1
        return Matrix.from_values(a, b, 0, c, d, 0, e, f, 1)
    elif name == 'translate':
        return Matrix.translate(*params)
    elif name == 'scale':
        if len(params):
            param, = params
            return Matrix.scale(param, param)
        else:
            return Matrix.scale(*params)
    elif name == 'rotate':
        if len(params) == 1:
            return Matrix.rotate(*params)
        else:
            angle, x, y = params
            return Matrix.translate(-x, -y) * Matrix.rotate(angle) * Matrix.rotate(x, y)

def from_complex(v):
    return Point(v.real, v.imag)

lines = [Line, CubicBezier, QuadraticBezier, Arc]
[docs]class Path: """ An individual path object within a SVG file. """ def __init__(self, transforms, data): self.transforms = transforms self.data = data self.transform = Matrix.scale(1, -1) for transform in self.transforms: self.transform *= transform def __mul__(self, f): if not valid_scalar(f): return NotImplemented return Path([Matrix.scale(f, f), *self.transforms], self.data) __rmul__ = __mul__ def parse(self): return parse_path(self.data) def t(self, point): return self.transform * point
[docs] def polygons(self, min_length = 1.0 * u.file): """ Returns all the simple :class:`~petrify.plane.Polygon` objects formed from this path: >>> from petrify import u >>> paths = SVG.read('tests/fixtures/example.svg', u.inches / (90 * u.file)) >>> box = paths['rect'].m_as(u.inches) >>> len(box.polygons()) 2 >>> len(box.polygons()[0].segments()) 4 Converts all curves to lines. `min_length` : The length used to linearize all curves. For example, a curve with a length of 4.5 file units and a `min_length` of `2 * u.file` would be broken into three line segments. """ parsed = self.parse() polygons = [] current = [] for ix in range(len(parsed)): command = parsed[ix] if any(isinstance(command, T) for T in lines): if not isinstance(command, Line): l = command.length(error=1e-5) points = l / min_length.m_as(u.file) for ix in range(0, max(0, int(points) - 1)): subpoint = from_complex(command.point(ix / points)) current.append(self.t(subpoint)) current.append(self.t(from_complex(command.end))) else: if current: polygons.append(current) current = [self.t(from_complex(command.start))] if current: polygons.append(current) polygons = (Polygon(p).simplify() for p in polygons) return [p for p in polygons if p is not None]
[docs] def polygon(self, min_length = 1.0 * u.file): """ Returns a :class:`~petrify.plane.ComplexPolygon` formed from this path: >>> from petrify import u >>> paths = SVG.read('tests/fixtures/example.svg', u.inches / (90 * u.file)) >>> box = paths['rect'].m_as(u.inches) >>> len(box.polygon().segments()) 8 >>> len(box.polygon().exterior) 1 >>> len(box.polygon().interior) 1 Converts all curves to lines. `min_length` : The length used to linearize all curves. For example, a curve with a length of 4.5 file units and a `min_length` of `2 * u.file` would be broken into three line segments. """ return ComplexPolygon(self.polygons(min_length))
class Handler(xml.sax.ContentHandler): def __init__(self, scale): self.stack = [] self.scale = units.conversion(scale) self.paths = {} def startElement(self, tag, attributes): transform = None if 'transform' in attributes: transform = attributes['transform'] if tag == 'g': self.stack.append(transform) if tag == 'path': transforms = list(t for t in self.stack if t is not None) if transform: transforms.append(transform) transforms = [parse_transform(t) for t in transforms] path = Path(transforms, attributes['d']) x = path * units.u.file self.paths[attributes['id']] = path * units.u.file * self.scale def endElement(self, tag): if tag == 'g': self.stack.pop()
[docs]class SVG: """ Basic reader for the SVG file format: >>> from petrify import u >>> paths = SVG.read('tests/fixtures/example.svg', u.inches / (90 * u.file)) >>> box = paths['rect'].m_as(u.inches) """ @classmethod def read(cls, path, scale): parser = xml.sax.make_parser() svg = Handler(scale) parser.setContentHandler(svg) parser.parse(path) return svg.paths