commit
acb4085fb2
|
@ -7,7 +7,6 @@ Author: Andy Port
|
||||||
Author-email: AndyAPort@gmail.com
|
Author-email: AndyAPort@gmail.com
|
||||||
License: MIT
|
License: MIT
|
||||||
Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.2
|
Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.2
|
||||||
Description-Content-Type: UNKNOWN
|
|
||||||
Description:
|
Description:
|
||||||
svgpathtools
|
svgpathtools
|
||||||
============
|
============
|
||||||
|
@ -595,9 +594,8 @@ Description:
|
||||||
of the 'parallel' offset curve."""
|
of the 'parallel' offset curve."""
|
||||||
nls = []
|
nls = []
|
||||||
for seg in path:
|
for seg in path:
|
||||||
ct = 1
|
|
||||||
for k in range(steps):
|
for k in range(steps):
|
||||||
t = k / steps
|
t = k / float(steps)
|
||||||
offset_vector = offset_distance * seg.normal(t)
|
offset_vector = offset_distance * seg.normal(t)
|
||||||
nl = Line(seg.point(t), seg.point(t) + offset_vector)
|
nl = Line(seg.point(t), seg.point(t) + offset_vector)
|
||||||
nls.append(nl)
|
nls.append(nl)
|
||||||
|
|
|
@ -15,6 +15,7 @@ test.svg
|
||||||
vectorframes.svg
|
vectorframes.svg
|
||||||
svgpathtools/__init__.py
|
svgpathtools/__init__.py
|
||||||
svgpathtools/bezier.py
|
svgpathtools/bezier.py
|
||||||
|
svgpathtools/document.py
|
||||||
svgpathtools/misctools.py
|
svgpathtools/misctools.py
|
||||||
svgpathtools/parser.py
|
svgpathtools/parser.py
|
||||||
svgpathtools/path.py
|
svgpathtools/path.py
|
||||||
|
@ -29,11 +30,13 @@ svgpathtools.egg-info/requires.txt
|
||||||
svgpathtools.egg-info/top_level.txt
|
svgpathtools.egg-info/top_level.txt
|
||||||
test/circle.svg
|
test/circle.svg
|
||||||
test/ellipse.svg
|
test/ellipse.svg
|
||||||
|
test/groups.svg
|
||||||
test/polygons.svg
|
test/polygons.svg
|
||||||
test/rects.svg
|
test/rects.svg
|
||||||
test/test.svg
|
test/test.svg
|
||||||
test/test_bezier.py
|
test/test_bezier.py
|
||||||
test/test_generation.py
|
test/test_generation.py
|
||||||
|
test/test_groups.py
|
||||||
test/test_parsing.py
|
test/test_parsing.py
|
||||||
test/test_path.py
|
test/test_path.py
|
||||||
test/test_polytools.py
|
test/test_polytools.py
|
||||||
|
|
|
@ -12,8 +12,9 @@ from .paths2svg import disvg, wsvg
|
||||||
from .polytools import polyroots, polyroots01, rational_limit, real, imag
|
from .polytools import polyroots, polyroots01, rational_limit, real, imag
|
||||||
from .misctools import hex2rgb, rgb2hex
|
from .misctools import hex2rgb, rgb2hex
|
||||||
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
|
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
|
||||||
|
from .document import Document
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .svg2paths import svg2paths, svg2paths2
|
from .svg2paths import svg2paths, svg2paths2
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -51,45 +51,143 @@ A Big Problem:
|
||||||
# External dependencies
|
# External dependencies
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import os
|
import os
|
||||||
import xml.etree.cElementTree as etree
|
import collections
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
from xml.etree.ElementTree import Element, SubElement, register_namespace, _namespace_map
|
||||||
|
import warnings
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .parser import parse_path
|
from .parser import parse_path
|
||||||
from .svg2paths import (ellipse2pathd, line2pathd, polyline2pathd,
|
from .parser import parse_transform
|
||||||
|
from .svg2paths import (path2pathd, ellipse2pathd, line2pathd, polyline2pathd,
|
||||||
polygon2pathd, rect2pathd)
|
polygon2pathd, rect2pathd)
|
||||||
from .misctools import open_in_browser
|
from .misctools import open_in_browser
|
||||||
|
from .path import *
|
||||||
|
|
||||||
# THESE MUST BE WRAPPED TO OUPUT ElementTree.element objects
|
# Let xml.etree.ElementTree know about the SVG namespace
|
||||||
CONVERSIONS = {'circle': ellipse2pathd,
|
SVG_NAMESPACE = {'svg': 'http://www.w3.org/2000/svg'}
|
||||||
|
register_namespace('svg', 'http://www.w3.org/2000/svg')
|
||||||
|
|
||||||
|
# THESE MUST BE WRAPPED TO OUTPUT ElementTree.element objects
|
||||||
|
CONVERSIONS = {'path': path2pathd,
|
||||||
|
'circle': ellipse2pathd,
|
||||||
'ellipse': ellipse2pathd,
|
'ellipse': ellipse2pathd,
|
||||||
'line': line2pathd,
|
'line': line2pathd,
|
||||||
'polyline': polyline2pathd,
|
'polyline': polyline2pathd,
|
||||||
'polygon': polygon2pathd,
|
'polygon': polygon2pathd,
|
||||||
'rect': rect2pathd}
|
'rect': rect2pathd}
|
||||||
|
|
||||||
|
CONVERT_ONLY_PATHS = {'path': path2pathd}
|
||||||
|
|
||||||
|
SVG_GROUP_TAG = 'svg:g'
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_all_paths(
|
||||||
|
group,
|
||||||
|
group_filter=lambda x: True,
|
||||||
|
path_filter=lambda x: True,
|
||||||
|
path_conversions=CONVERSIONS,
|
||||||
|
group_search_xpath=SVG_GROUP_TAG):
|
||||||
|
"""Returns the paths inside a group (recursively), expressing the paths in the base coordinates.
|
||||||
|
|
||||||
|
Note that if the group being passed in is nested inside some parent group(s), we cannot take the parent group(s)
|
||||||
|
into account, because xml.etree.Element has no pointer to its parent. You should use Document.flatten_group(group)
|
||||||
|
to flatten a specific nested group into the root coordinates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
group is an Element
|
||||||
|
path_conversions (dict): A dictionary to convert from an SVG element to a path data string. Any element tags
|
||||||
|
that are not included in this dictionary will be ignored (including the `path` tag).
|
||||||
|
To only convert explicit path elements, pass in path_conversions=CONVERT_ONLY_PATHS.
|
||||||
|
"""
|
||||||
|
if not isinstance(group, Element):
|
||||||
|
raise TypeError('Must provide an xml.etree.Element object. Instead you provided {0}'.format(type(group)))
|
||||||
|
|
||||||
|
# Stop right away if the group_selector rejects this group
|
||||||
|
if not group_filter(group):
|
||||||
|
return []
|
||||||
|
|
||||||
|
# To handle the transforms efficiently, we'll traverse the tree of groups depth-first using a stack of tuples.
|
||||||
|
# The first entry in the tuple is a group element and the second entry is its transform. As we pop each entry in
|
||||||
|
# the stack, we will add all its child group elements to the stack.
|
||||||
|
StackElement = collections.namedtuple('StackElement', ['group', 'transform'])
|
||||||
|
|
||||||
|
def new_stack_element(element, last_tf):
|
||||||
|
return StackElement(element, last_tf.dot(parse_transform(element.get('transform'))))
|
||||||
|
|
||||||
|
def get_relevant_children(parent, last_tf):
|
||||||
|
children = []
|
||||||
|
for elem in filter(group_filter, parent.iterfind(group_search_xpath, SVG_NAMESPACE)):
|
||||||
|
children.append(new_stack_element(elem, last_tf))
|
||||||
|
return children
|
||||||
|
|
||||||
|
stack = [new_stack_element(group, np.identity(3))]
|
||||||
|
|
||||||
|
FlattenedPath = collections.namedtuple('FlattenedPath', ['path', 'element', 'transform'])
|
||||||
|
paths = []
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
top = stack.pop()
|
||||||
|
|
||||||
|
# For each element type that we know how to convert into path data, parse the element after confirming that
|
||||||
|
# the path_filter accepts it.
|
||||||
|
for key, converter in path_conversions.iteritems():
|
||||||
|
for path_elem in filter(path_filter, top.group.iterfind('svg:'+key, SVG_NAMESPACE)):
|
||||||
|
path_tf = top.transform.dot(parse_transform(path_elem.get('transform')))
|
||||||
|
path = transform(parse_path(converter(path_elem)), path_tf)
|
||||||
|
paths.append(FlattenedPath(path, path_elem, path_tf))
|
||||||
|
|
||||||
|
stack.extend(get_relevant_children(top.group, top.transform))
|
||||||
|
|
||||||
|
return paths
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_group(
|
||||||
|
group_to_flatten,
|
||||||
|
root,
|
||||||
|
recursive=True,
|
||||||
|
group_filter=lambda x: True,
|
||||||
|
path_filter=lambda x: True,
|
||||||
|
path_conversions=CONVERSIONS,
|
||||||
|
group_search_xpath=SVG_GROUP_TAG):
|
||||||
|
"""Flatten all the paths in a specific group.
|
||||||
|
|
||||||
|
The paths will be flattened into the 'root' frame. Note that root needs to be
|
||||||
|
an ancestor of the group that is being flattened. Otherwise, no paths will be returned."""
|
||||||
|
|
||||||
|
if not any(group_to_flatten is descendant for descendant in root.iter()):
|
||||||
|
warnings.warn('The requested group_to_flatten is not a descendant of root')
|
||||||
|
# We will shortcut here, because it is impossible for any paths to be returned anyhow.
|
||||||
|
return []
|
||||||
|
|
||||||
|
# We create a set of the unique IDs of each element that we wish to flatten, if those elements are groups.
|
||||||
|
# Any groups outside of this set will be skipped while we flatten the paths.
|
||||||
|
desired_groups = set()
|
||||||
|
if recursive:
|
||||||
|
for group in group_to_flatten.iter():
|
||||||
|
desired_groups.add(id(group))
|
||||||
|
else:
|
||||||
|
desired_groups.add(id(group_to_flatten))
|
||||||
|
|
||||||
|
def desired_group_filter(x):
|
||||||
|
return (id(x) in desired_groups) and group_filter(x)
|
||||||
|
|
||||||
|
return flatten_all_paths(root, desired_group_filter, path_filter, path_conversions, group_search_xpath)
|
||||||
|
|
||||||
|
|
||||||
class Document:
|
class Document:
|
||||||
def __init__(self, filename, conversions=False, transform_paths=True):
|
def __init__(self, filename):
|
||||||
"""(EXPERIMENTAL) A container for a DOM-style document.
|
"""A container for a DOM-style SVG document.
|
||||||
|
|
||||||
The `Document` class provides a simple interface to modify and analyze
|
The `Document` class provides a simple interface to modify and analyze
|
||||||
the path elements in a DOM-style document. The DOM-style document is
|
the path elements in a DOM-style document. The DOM-style document is
|
||||||
parsed into an ElementTree object (stored in the `tree` attribute and
|
parsed into an ElementTree object (stored in the `tree` attribute).
|
||||||
all SVG-Path (and, optionally, Path-like) elements are extracted into a
|
|
||||||
list of svgpathtools Path objects. For more information on "Path-like"
|
This class provides functions for extracting SVG data into Path objects.
|
||||||
objects, see the below explanation of the `conversions` argument.
|
The Path output objects will be transformed based on their parent groups.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
merge_transforms (object):
|
|
||||||
filename (str): The filename of the DOM-style object.
|
filename (str): The filename of the DOM-style object.
|
||||||
conversions (bool or dict): If true, automatically converts
|
|
||||||
circle, ellipse, line, polyline, polygon, and rect elements
|
|
||||||
into path elements. These changes are saved in the ElementTree
|
|
||||||
object. For custom conversions, a dictionary can be passed in instead whose
|
|
||||||
keys are the element tags that are to be converted and whose values
|
|
||||||
are the corresponding conversion functions. Conversion
|
|
||||||
functions should both take in and return an ElementTree.element
|
|
||||||
object.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# remember location of original svg file
|
# remember location of original svg file
|
||||||
|
@ -102,15 +200,26 @@ class Document:
|
||||||
self.tree = etree.parse(filename)
|
self.tree = etree.parse(filename)
|
||||||
self.root = self.tree.getroot()
|
self.root = self.tree.getroot()
|
||||||
|
|
||||||
# get URI namespace (only necessary in OS X?)
|
def flatten_all_paths(self,
|
||||||
root_tag = self.tree.getroot().tag
|
group_filter=lambda x: True,
|
||||||
if root_tag[0] == "{":
|
path_filter=lambda x: True,
|
||||||
self._prefix = root_tag[:root_tag.find('}') + 1]
|
path_conversions=CONVERSIONS):
|
||||||
else:
|
return flatten_all_paths(self.tree.getroot(), group_filter, path_filter, path_conversions)
|
||||||
self._prefix = ''
|
|
||||||
# etree.register_namespace('', prefix)
|
|
||||||
|
|
||||||
self.paths = self._get_paths(conversions)
|
def flatten_group(self,
|
||||||
|
group,
|
||||||
|
recursive=True,
|
||||||
|
group_filter=lambda x: True,
|
||||||
|
path_filter=lambda x: True,
|
||||||
|
path_conversions=CONVERSIONS):
|
||||||
|
if all(isinstance(s, basestring) for s in group):
|
||||||
|
# If we're given a list of strings, assume it represents a nested sequence
|
||||||
|
group = self.get_or_add_group(group)
|
||||||
|
elif not isinstance(group, Element):
|
||||||
|
raise TypeError('Must provide a list of strings that represent a nested group name, '
|
||||||
|
'or provide an xml.etree.Element object. Instead you provided {0}'.format(group))
|
||||||
|
|
||||||
|
return flatten_group(group, self.tree.getroot(), recursive, group_filter, path_filter, path_conversions)
|
||||||
|
|
||||||
def get_elements_by_tag(self, tag):
|
def get_elements_by_tag(self, tag):
|
||||||
"""Returns a generator of all elements with the given tag.
|
"""Returns a generator of all elements with the given tag.
|
||||||
|
@ -120,68 +229,115 @@ class Document:
|
||||||
"""
|
"""
|
||||||
return self.tree.iter(tag=self._prefix + tag)
|
return self.tree.iter(tag=self._prefix + tag)
|
||||||
|
|
||||||
def _get_paths(self, conversions):
|
|
||||||
paths = []
|
|
||||||
|
|
||||||
# Get d-strings for SVG-Path elements
|
|
||||||
paths += [el.attrib for el in self.get_elements_by_tag('path')]
|
|
||||||
d_strings = [el['d'] for el in paths]
|
|
||||||
attribute_dictionary_list = paths
|
|
||||||
|
|
||||||
# Convert path-like elements to d-strings and attribute dicts
|
|
||||||
if conversions:
|
|
||||||
for tag, fcn in conversions.items():
|
|
||||||
attributes = [l.attrib for l in self.get_elements_by_tag(tag)]
|
|
||||||
d_strings += [fcn(d) for d in attributes]
|
|
||||||
|
|
||||||
path_list = [parse_path(d) for d in d_strings]
|
|
||||||
return path_list
|
|
||||||
|
|
||||||
def convert_pathlike_elements_to_paths(self, conversions=CONVERSIONS):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def get_svg_attributes(self):
|
def get_svg_attributes(self):
|
||||||
"""To help with backwards compatibility."""
|
"""To help with backwards compatibility."""
|
||||||
return self.get_elements_by_tag('svg')[0].attrib
|
return self.get_elements_by_tag('svg')[0].attrib
|
||||||
|
|
||||||
def get_path_attributes(self):
|
def get_path_attributes(self):
|
||||||
"""To help with backwards compatibility."""
|
"""To help with backwards compatibility."""
|
||||||
return [p.tree_element.attrib for p in self.paths]
|
return [p.tree_element.attrib for p in self.tree.getroot().iter('path')]
|
||||||
|
|
||||||
def add(self, path, attribs={}, parent=None):
|
def add_path(self, path, attribs=None, group=None):
|
||||||
"""Add a new path to the SVG."""
|
"""Add a new path to the SVG."""
|
||||||
if parent is None:
|
|
||||||
parent = self.tree.getroot()
|
|
||||||
# just get root
|
|
||||||
# then add new path
|
|
||||||
# then record element_tree object in path
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def add_group(self, group_attribs={}, parent=None):
|
# If we are not given a parent, assume that the path does not have a group
|
||||||
|
if group is None:
|
||||||
|
group = self.tree.getroot()
|
||||||
|
|
||||||
|
# If we are given a list of strings (one or more), assume it represents a sequence of nested group names
|
||||||
|
elif all(isinstance(elem, basestring) for elem in group):
|
||||||
|
group = self.get_or_add_group(group)
|
||||||
|
|
||||||
|
elif not isinstance(group, Element):
|
||||||
|
raise TypeError('Must provide a list of strings or an xml.etree.Element object. '
|
||||||
|
'Instead you provided {0}'.format(group))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Make sure that the group belongs to this Document object
|
||||||
|
if not self.contains_group(group):
|
||||||
|
warnings.warn('The requested group does not belong to this Document')
|
||||||
|
|
||||||
|
if isinstance(path, Path):
|
||||||
|
path_svg = path.d()
|
||||||
|
elif is_path_segment(path):
|
||||||
|
path_svg = Path(path).d()
|
||||||
|
elif isinstance(path, basestring):
|
||||||
|
# Assume this is a valid d-string. TODO: Should we sanity check the input string?
|
||||||
|
path_svg = path
|
||||||
|
else:
|
||||||
|
raise TypeError('Must provide a Path, a path segment type, or a valid SVG path d-string. '
|
||||||
|
'Instead you provided {0}'.format(path))
|
||||||
|
|
||||||
|
if attribs is None:
|
||||||
|
attribs = {}
|
||||||
|
else:
|
||||||
|
attribs = attribs.copy()
|
||||||
|
|
||||||
|
attribs['d'] = path_svg
|
||||||
|
|
||||||
|
return SubElement(group, 'path', attribs)
|
||||||
|
|
||||||
|
def contains_group(self, group):
|
||||||
|
return any(group is owned for owned in self.tree.iter())
|
||||||
|
|
||||||
|
def get_or_add_group(self, nested_names):
|
||||||
|
"""Get a group from the tree, or add a new one with the given name structure.
|
||||||
|
|
||||||
|
*nested_names* is a list of strings which represent group names. Each group name will be nested inside of the
|
||||||
|
previous group name.
|
||||||
|
|
||||||
|
Returns the requested group. If the requested group did not exist, this function will create it, as well as all
|
||||||
|
parent groups that it requires. All created groups will be left with blank attributes.
|
||||||
|
|
||||||
|
"""
|
||||||
|
group = self.tree.getroot()
|
||||||
|
# Drill down through the names until we find the desired group
|
||||||
|
while nested_names:
|
||||||
|
prev_group = group
|
||||||
|
next_name = nested_names.pop(0)
|
||||||
|
for elem in group.iterfind('svg:g', SVG_NAMESPACE):
|
||||||
|
if elem.get('id') == next_name:
|
||||||
|
group = elem
|
||||||
|
break
|
||||||
|
|
||||||
|
if prev_group is group:
|
||||||
|
# The group we're looking for does not exist, so let's create the group structure
|
||||||
|
nested_names.insert(0, next_name)
|
||||||
|
|
||||||
|
while nested_names:
|
||||||
|
next_name = nested_names.pop(0)
|
||||||
|
group = self.add_group({'id': next_name}, group)
|
||||||
|
|
||||||
|
# Now nested_names will be empty, so the topmost while-loop will end
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
|
def add_group(self, group_attribs=None, parent=None):
|
||||||
"""Add an empty group element to the SVG."""
|
"""Add an empty group element to the SVG."""
|
||||||
if parent is None:
|
if parent is None:
|
||||||
parent = self.tree.getroot()
|
parent = self.tree.getroot()
|
||||||
raise NotImplementedError
|
elif not self.contains_group(parent):
|
||||||
|
warnings.warn('The requested group {0} does not belong to this Document'.format(parent))
|
||||||
|
|
||||||
def update_tree(self):
|
if group_attribs is None:
|
||||||
"""Rewrite d-string's for each path in the `tree` attribute."""
|
group_attribs = {}
|
||||||
raise NotImplementedError
|
else:
|
||||||
|
group_attribs = group_attribs.copy()
|
||||||
|
|
||||||
def save(self, filename, update=True):
|
return SubElement(parent, 'g', group_attribs)
|
||||||
"""Write to svg to a file."""
|
|
||||||
if update:
|
def save(self, filename=None):
|
||||||
self.update_tree()
|
if filename is None:
|
||||||
|
filename = self.original_filename
|
||||||
|
|
||||||
with open(filename, 'w') as output_svg:
|
with open(filename, 'w') as output_svg:
|
||||||
output_svg.write(etree.tostring(self.tree.getroot()))
|
output_svg.write(etree.tostring(self.tree.getroot()))
|
||||||
|
|
||||||
def display(self, filename=None, update=True):
|
def display(self, filename=None):
|
||||||
"""Displays/opens the doc using the OS's default application."""
|
"""Displays/opens the doc using the OS's default application."""
|
||||||
if update:
|
|
||||||
self.update_tree()
|
|
||||||
|
|
||||||
if filename is None:
|
if filename is None:
|
||||||
raise NotImplementedError
|
filename = self.original_filename
|
||||||
|
|
||||||
# write to a (by default temporary) file
|
# write to a (by default temporary) file
|
||||||
with open(filename, 'w') as output_svg:
|
with open(filename, 'w') as output_svg:
|
||||||
|
|
|
@ -5,6 +5,8 @@ Note: This file was taken (nearly) as is from the svg.path module (v 2.0)."""
|
||||||
# External dependencies
|
# External dependencies
|
||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import re
|
import re
|
||||||
|
import numpy as np
|
||||||
|
import warnings
|
||||||
|
|
||||||
# Internal dependencies
|
# Internal dependencies
|
||||||
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
|
from .path import Path, Line, QuadraticBezier, CubicBezier, Arc
|
||||||
|
@ -197,3 +199,98 @@ def parse_path(pathdef, current_pos=0j, tree_element=None):
|
||||||
current_pos = end
|
current_pos = end
|
||||||
|
|
||||||
return segments
|
return segments
|
||||||
|
|
||||||
|
|
||||||
|
def _check_num_parsed_values(values, allowed):
|
||||||
|
if not any(num == len(values) for num in allowed):
|
||||||
|
if len(allowed) > 1:
|
||||||
|
warnings.warn('Expected one of the following number of values {0}, found {1}: {2}'
|
||||||
|
.format(allowed, len(values), values))
|
||||||
|
elif allowed[0] != 1:
|
||||||
|
warnings.warn('Expected {0} values, found {1}: {2}'.format(allowed[0], len(values), values))
|
||||||
|
else:
|
||||||
|
warnings.warn('Expected 1 value, found {0}: {1}'.format(len(values), values))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_transform_substr(transform_substr):
|
||||||
|
|
||||||
|
type_str, value_str = transform_substr.split('(')
|
||||||
|
value_str = value_str.replace(',', ' ')
|
||||||
|
values = list(map(float, filter(None, value_str.split(' '))))
|
||||||
|
|
||||||
|
transform = np.identity(3)
|
||||||
|
if 'matrix' in type_str:
|
||||||
|
if not _check_num_parsed_values(values, [6]):
|
||||||
|
return transform
|
||||||
|
|
||||||
|
transform[0:2, 0:3] = np.matrix([values[0:6:2], values[1:6:2]])
|
||||||
|
|
||||||
|
elif 'translate' in transform_substr:
|
||||||
|
if not _check_num_parsed_values(values, [1, 2]):
|
||||||
|
return transform
|
||||||
|
|
||||||
|
transform[0, 2] = values[0]
|
||||||
|
if len(values) > 1:
|
||||||
|
transform[1, 2] = values[1]
|
||||||
|
|
||||||
|
elif 'scale' in transform_substr:
|
||||||
|
if not _check_num_parsed_values(values, [1, 2]):
|
||||||
|
return transform
|
||||||
|
|
||||||
|
x_scale = values[0]
|
||||||
|
y_scale = values[1] if (len(values) > 1) else x_scale
|
||||||
|
transform[0, 0] = x_scale
|
||||||
|
transform[1, 1] = y_scale
|
||||||
|
|
||||||
|
elif 'rotate' in transform_substr:
|
||||||
|
if not _check_num_parsed_values(values, [1, 3]):
|
||||||
|
return transform
|
||||||
|
|
||||||
|
angle = values[0] * np.pi / 180.0
|
||||||
|
if len(values) == 3:
|
||||||
|
offset = values[1:3]
|
||||||
|
else:
|
||||||
|
offset = (0, 0)
|
||||||
|
tf_offset = np.identity(3)
|
||||||
|
tf_offset[0:2, 2:3] = np.matrix([[offset[0]], [offset[1]]])
|
||||||
|
tf_rotate = np.identity(3)
|
||||||
|
tf_rotate[0:2, 0:2] = np.matrix([[np.cos(angle), -np.sin(angle)], [np.sin(angle), np.cos(angle)]])
|
||||||
|
tf_offset_neg = np.identity(3)
|
||||||
|
tf_offset_neg[0:2, 2:3] = np.matrix([[-offset[0]], [-offset[1]]])
|
||||||
|
|
||||||
|
transform = tf_offset.dot(tf_rotate).dot(tf_offset_neg)
|
||||||
|
|
||||||
|
elif 'skewX' in transform_substr:
|
||||||
|
if not _check_num_parsed_values(values, [1]):
|
||||||
|
return transform
|
||||||
|
|
||||||
|
transform[0, 1] = np.tan(values[0] * np.pi / 180.0)
|
||||||
|
|
||||||
|
elif 'skewY' in transform_substr:
|
||||||
|
if not _check_num_parsed_values(values, [1]):
|
||||||
|
return transform
|
||||||
|
|
||||||
|
transform[1, 0] = np.tan(values[0] * np.pi / 180.0)
|
||||||
|
else:
|
||||||
|
# Return an identity matrix if the type of transform is unknown, and warn the user
|
||||||
|
warnings.warn('Unknown SVG transform type: {0}'.format(type_str))
|
||||||
|
|
||||||
|
return transform
|
||||||
|
|
||||||
|
|
||||||
|
def parse_transform(transform_str):
|
||||||
|
"""Converts a valid SVG transformation string into a 3x3 matrix.
|
||||||
|
If the string is empty or null, this returns a 3x3 identity matrix"""
|
||||||
|
if not transform_str:
|
||||||
|
return np.identity(3)
|
||||||
|
elif not isinstance(transform_str, basestring):
|
||||||
|
raise TypeError('Must provide a string to parse')
|
||||||
|
|
||||||
|
total_transform = np.identity(3)
|
||||||
|
transform_substrs = transform_str.split(')')[:-1] # Skip the last element, because it should be empty
|
||||||
|
for substr in transform_substrs:
|
||||||
|
total_transform = total_transform.dot(_parse_transform_substr(substr))
|
||||||
|
|
||||||
|
return total_transform
|
||||||
|
|
|
@ -207,6 +207,32 @@ def translate(curve, z0):
|
||||||
"QuadraticBezier, CubicBezier, or Arc object.")
|
"QuadraticBezier, CubicBezier, or Arc object.")
|
||||||
|
|
||||||
|
|
||||||
|
def transform(curve, tf):
|
||||||
|
"""Transforms the curve by the homogeneous transformation matrix tf"""
|
||||||
|
def to_point(p):
|
||||||
|
return np.matrix([[p.real], [p.imag], [1.0]])
|
||||||
|
|
||||||
|
def to_vector(v):
|
||||||
|
return np.matrix([[v.real], [v.imag], [0.0]])
|
||||||
|
|
||||||
|
def to_complex(z):
|
||||||
|
return z[0] + 1j * z[1]
|
||||||
|
|
||||||
|
if isinstance(curve, Path):
|
||||||
|
return Path(*[transform(segment, tf) for segment in curve])
|
||||||
|
elif is_bezier_segment(curve):
|
||||||
|
return bpoints2bezier([to_complex(tf*to_point(p)) for p in curve.bpoints()])
|
||||||
|
elif isinstance(curve, Arc):
|
||||||
|
new_start = to_complex(tf * to_point(curve.start))
|
||||||
|
new_end = to_complex(tf * to_point(curve.end))
|
||||||
|
new_radius = to_complex(tf * to_vector(curve.radius))
|
||||||
|
return Arc(new_start, radius=new_radius, rotation=curve.rotation,
|
||||||
|
large_arc=curve.large_arc, sweep=curve.sweep, end=new_end)
|
||||||
|
else:
|
||||||
|
raise TypeError("Input `curve` should be a Path, Line, "
|
||||||
|
"QuadraticBezier, CubicBezier, or Arc object.")
|
||||||
|
|
||||||
|
|
||||||
def bezier_unit_tangent(seg, t):
|
def bezier_unit_tangent(seg, t):
|
||||||
"""Returns the unit tangent of the segment at t.
|
"""Returns the unit tangent of the segment at t.
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,8 @@ COORD_PAIR_TMPLT = re.compile(
|
||||||
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)'
|
r'([\+-]?\d*[\.\d]\d*[eE][\+-]?\d+|[\+-]?\d*[\.\d]\d*)'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def path2pathd(path):
|
||||||
|
return path.get('d', '')
|
||||||
|
|
||||||
def ellipse2pathd(ellipse):
|
def ellipse2pathd(ellipse):
|
||||||
"""converts the parameters from an ellipse or a circle to a string for a
|
"""converts the parameters from an ellipse or a circle to a string for a
|
||||||
|
@ -88,6 +90,8 @@ def rect2pathd(rect):
|
||||||
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
|
"".format(x0, y0, x1, y1, x2, y2, x3, y3))
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
def line2pathd(l):
|
||||||
|
return 'M' + l['x1'] + ' ' + l['y1'] + 'L' + l['x2'] + ' ' + l['y2']
|
||||||
|
|
||||||
def svg2paths(svg_file_location,
|
def svg2paths(svg_file_location,
|
||||||
return_svg_attributes=False,
|
return_svg_attributes=False,
|
||||||
|
|
|
@ -0,0 +1,161 @@
|
||||||
|
<?xml version="1.0" ?>
|
||||||
|
<svg
|
||||||
|
baseProfile="full"
|
||||||
|
version="1.1"
|
||||||
|
viewBox="0 0 365 365"
|
||||||
|
height="100%"
|
||||||
|
width="100%"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:test="some://testuri">
|
||||||
|
<defs/>
|
||||||
|
<g
|
||||||
|
id="matrix group"
|
||||||
|
transform="matrix(1.5 0.0 0.0 0.5 -40.0 20.0)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 183,183 l 0,-50"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path00"/>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="scale group"
|
||||||
|
transform="scale(1.25)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 122,320 l -50,0"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path01"/>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="nested group - empty transform"
|
||||||
|
transform="">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 150,200 l -50,25"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path02"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="nested group - no transform">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 150,200 l -50,25"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path03"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="nested group - translate"
|
||||||
|
transform="translate(20)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 150,200 l -50,25"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path04"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="nested group - translate xy"
|
||||||
|
transform="translate(20, 30)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 150,200 l -50,25"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path05"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="scale xy group"
|
||||||
|
transform="scale(0.5 1.5)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 122,320 l -50,0"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path06"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="rotate group"
|
||||||
|
transform="rotate(20)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 183,183 l 0,30"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path07"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="rotate xy group"
|
||||||
|
transform="rotate(45 183 183)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 183,183 l 0,30"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path08"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="skew x group"
|
||||||
|
transform="skewX(5)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 183,183 l 40,40"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path09"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
id="skew y group"
|
||||||
|
transform="skewY(5)">
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 183,183 l 40,40"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="3"
|
||||||
|
test:name="path10"/>
|
||||||
|
|
||||||
|
<path
|
||||||
|
d="M 180,20 l -70,80"
|
||||||
|
fill="black"
|
||||||
|
stroke="black"
|
||||||
|
stroke-width="4"
|
||||||
|
transform="rotate(-40, 100, 100)"
|
||||||
|
test:name="path11"/>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,135 @@
|
||||||
|
from __future__ import division, absolute_import, print_function
|
||||||
|
import unittest
|
||||||
|
from svgpathtools import *
|
||||||
|
from os.path import join, dirname
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def get_desired_path(name, paths):
|
||||||
|
return next(p for p in paths if p.element.get('{some://testuri}name') == name)
|
||||||
|
|
||||||
|
|
||||||
|
def column_vector(values):
|
||||||
|
input = []
|
||||||
|
for value in values:
|
||||||
|
input.append([value])
|
||||||
|
return np.matrix(input)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGroups(unittest.TestCase):
|
||||||
|
|
||||||
|
def check_values(self, v, z):
|
||||||
|
# Check that the components of 2D vector v match the components of complex number z
|
||||||
|
self.assertAlmostEqual(v[0], z.real)
|
||||||
|
self.assertAlmostEqual(v[1], z.imag)
|
||||||
|
|
||||||
|
def check_line(self, tf, v_s_vals, v_e_relative_vals, name, paths):
|
||||||
|
# Check that the endpoints of the line have been correctly transformed.
|
||||||
|
# * tf is the transform that should have been applied.
|
||||||
|
# * v_s_vals is a 2D list of the values of the line's start point
|
||||||
|
# * v_e_relative_vals is a 2D list of the values of the line's end point relative to the start point
|
||||||
|
# * name is the path name (value of the test:name attribute in the SVG document)
|
||||||
|
# * paths is the output of doc.flatten_all_paths()
|
||||||
|
v_s_vals.append(1.0)
|
||||||
|
v_e_relative_vals.append(0.0)
|
||||||
|
v_s = column_vector(v_s_vals)
|
||||||
|
v_e = v_s + column_vector(v_e_relative_vals)
|
||||||
|
|
||||||
|
actual = get_desired_path(name, paths)
|
||||||
|
|
||||||
|
self.check_values(tf.dot(v_s), actual.path.start)
|
||||||
|
self.check_values(tf.dot(v_e), actual.path.end)
|
||||||
|
|
||||||
|
def test_group_flatten(self):
|
||||||
|
# Test the Document.flatten_all_paths() function against the groups.svg test file.
|
||||||
|
# There are 12 paths in that file, with various levels of being nested inside of group transforms.
|
||||||
|
# The check_line function is used to reduce the boilerplate, since all the tests are very similar.
|
||||||
|
# This test covers each of the different types of transforms that are specified by the SVG standard.
|
||||||
|
doc = Document(join(dirname(__file__), 'groups.svg'))
|
||||||
|
|
||||||
|
result = doc.flatten_all_paths()
|
||||||
|
self.assertEqual(12, len(result))
|
||||||
|
|
||||||
|
tf_matrix_group = np.matrix([[1.5, 0.0, -40.0], [0.0, 0.5, 20.0], [0.0, 0.0, 1.0]])
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group,
|
||||||
|
[183, 183], [0.0, -50],
|
||||||
|
'path00', result)
|
||||||
|
|
||||||
|
tf_scale_group = np.matrix([[1.25, 0.0, 0.0], [0.0, 1.25, 0.0], [0.0, 0.0, 1.0]])
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_scale_group),
|
||||||
|
[122, 320], [-50.0, 0.0],
|
||||||
|
'path01', result)
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_scale_group),
|
||||||
|
[150, 200], [-50, 25],
|
||||||
|
'path02', result)
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_scale_group),
|
||||||
|
[150, 200], [-50, 25],
|
||||||
|
'path03', result)
|
||||||
|
|
||||||
|
tf_nested_translate_group = np.matrix([[1, 0, 20], [0, 1, 0], [0, 0, 1]])
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_scale_group).dot(tf_nested_translate_group),
|
||||||
|
[150, 200], [-50, 25],
|
||||||
|
'path04', result)
|
||||||
|
|
||||||
|
tf_nested_translate_xy_group = np.matrix([[1, 0, 20], [0, 1, 30], [0, 0, 1]])
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_scale_group).dot(tf_nested_translate_xy_group),
|
||||||
|
[150, 200], [-50, 25],
|
||||||
|
'path05', result)
|
||||||
|
|
||||||
|
tf_scale_xy_group = np.matrix([[0.5, 0, 0], [0, 1.5, 0.0], [0, 0, 1]])
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_scale_xy_group),
|
||||||
|
[122, 320], [-50, 0],
|
||||||
|
'path06', result)
|
||||||
|
|
||||||
|
a_07 = 20.0*np.pi/180.0
|
||||||
|
tf_rotate_group = np.matrix([[np.cos(a_07), -np.sin(a_07), 0],
|
||||||
|
[np.sin(a_07), np.cos(a_07), 0],
|
||||||
|
[0, 0, 1]])
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_rotate_group),
|
||||||
|
[183, 183], [0, 30],
|
||||||
|
'path07', result)
|
||||||
|
|
||||||
|
a_08 = 45.0*np.pi/180.0
|
||||||
|
tf_rotate_xy_group_R = np.matrix([[np.cos(a_08), -np.sin(a_08), 0],
|
||||||
|
[np.sin(a_08), np.cos(a_08), 0],
|
||||||
|
[0, 0, 1]])
|
||||||
|
tf_rotate_xy_group_T = np.matrix([[1, 0, 183], [0, 1, 183], [0, 0, 1]])
|
||||||
|
tf_rotate_xy_group = tf_rotate_xy_group_T.dot(tf_rotate_xy_group_R).dot(np.linalg.inv(tf_rotate_xy_group_T))
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_rotate_xy_group),
|
||||||
|
[183, 183], [0, 30],
|
||||||
|
'path08', result)
|
||||||
|
|
||||||
|
a_09 = 5.0*np.pi/180.0
|
||||||
|
tf_skew_x_group = np.matrix([[1, np.tan(a_09), 0], [0, 1, 0], [0, 0, 1]])
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_skew_x_group),
|
||||||
|
[183, 183], [40, 40],
|
||||||
|
'path09', result)
|
||||||
|
|
||||||
|
a_10 = 5.0*np.pi/180.0
|
||||||
|
tf_skew_y_group = np.matrix([[1, 0, 0], [np.tan(a_10), 1, 0], [0, 0, 1]])
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_skew_y_group),
|
||||||
|
[183, 183], [40, 40],
|
||||||
|
'path10', result)
|
||||||
|
|
||||||
|
# This last test is for handling transforms that are defined as attributes of a <path> element.
|
||||||
|
a_11 = -40*np.pi/180.0
|
||||||
|
tf_path11_R = np.matrix([[np.cos(a_11), -np.sin(a_11), 0],
|
||||||
|
[np.sin(a_11), np.cos(a_11), 0],
|
||||||
|
[0, 0, 1]])
|
||||||
|
tf_path11_T = np.matrix([[1, 0, 100], [0, 1, 100], [0, 0, 1]])
|
||||||
|
tf_path11 = tf_path11_T.dot(tf_path11_R).dot(np.linalg.inv(tf_path11_T))
|
||||||
|
|
||||||
|
self.check_line(tf_matrix_group.dot(tf_skew_y_group).dot(tf_path11),
|
||||||
|
[180, 20], [-70, 80],
|
||||||
|
'path11', result)
|
|
@ -137,3 +137,7 @@ class TestParser(unittest.TestCase):
|
||||||
|
|
||||||
def test_errors(self):
|
def test_errors(self):
|
||||||
self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200')
|
self.assertRaises(ValueError, parse_path, 'M 100 100 L 200 200 Z 100 200')
|
||||||
|
|
||||||
|
def test_transform(self):
|
||||||
|
# TODO: Write these tests
|
||||||
|
pass
|
||||||
|
|
Loading…
Reference in New Issue