commit
acb4085fb2
|
@ -7,7 +7,6 @@ Author: Andy Port
|
|||
Author-email: AndyAPort@gmail.com
|
||||
License: MIT
|
||||
Download-URL: http://github.com/mathandy/svgpathtools/tarball/1.3.2
|
||||
Description-Content-Type: UNKNOWN
|
||||
Description:
|
||||
svgpathtools
|
||||
============
|
||||
|
@ -595,9 +594,8 @@ Description:
|
|||
of the 'parallel' offset curve."""
|
||||
nls = []
|
||||
for seg in path:
|
||||
ct = 1
|
||||
for k in range(steps):
|
||||
t = k / steps
|
||||
t = k / float(steps)
|
||||
offset_vector = offset_distance * seg.normal(t)
|
||||
nl = Line(seg.point(t), seg.point(t) + offset_vector)
|
||||
nls.append(nl)
|
||||
|
|
|
@ -15,6 +15,7 @@ test.svg
|
|||
vectorframes.svg
|
||||
svgpathtools/__init__.py
|
||||
svgpathtools/bezier.py
|
||||
svgpathtools/document.py
|
||||
svgpathtools/misctools.py
|
||||
svgpathtools/parser.py
|
||||
svgpathtools/path.py
|
||||
|
@ -29,11 +30,13 @@ svgpathtools.egg-info/requires.txt
|
|||
svgpathtools.egg-info/top_level.txt
|
||||
test/circle.svg
|
||||
test/ellipse.svg
|
||||
test/groups.svg
|
||||
test/polygons.svg
|
||||
test/rects.svg
|
||||
test/test.svg
|
||||
test/test_bezier.py
|
||||
test/test_generation.py
|
||||
test/test_groups.py
|
||||
test/test_parsing.py
|
||||
test/test_path.py
|
||||
test/test_polytools.py
|
||||
|
|
|
@ -12,8 +12,9 @@ from .paths2svg import disvg, wsvg
|
|||
from .polytools import polyroots, polyroots01, rational_limit, real, imag
|
||||
from .misctools import hex2rgb, rgb2hex
|
||||
from .smoothing import smoothed_path, smoothed_joint, is_differentiable, kinks
|
||||
from .document import Document
|
||||
|
||||
try:
|
||||
from .svg2paths import svg2paths, svg2paths2
|
||||
except ImportError:
|
||||
pass
|
||||
pass
|
||||
|
|
|
@ -51,45 +51,143 @@ A Big Problem:
|
|||
# External dependencies
|
||||
from __future__ import division, absolute_import, print_function
|
||||
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
|
||||
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)
|
||||
from .misctools import open_in_browser
|
||||
from .path import *
|
||||
|
||||
# THESE MUST BE WRAPPED TO OUPUT ElementTree.element objects
|
||||
CONVERSIONS = {'circle': ellipse2pathd,
|
||||
# Let xml.etree.ElementTree know about the SVG namespace
|
||||
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,
|
||||
'line': line2pathd,
|
||||
'polyline': polyline2pathd,
|
||||
'polygon': polygon2pathd,
|
||||
'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:
|
||||
def __init__(self, filename, conversions=False, transform_paths=True):
|
||||
"""(EXPERIMENTAL) A container for a DOM-style document.
|
||||
def __init__(self, filename):
|
||||
"""A container for a DOM-style SVG document.
|
||||
|
||||
The `Document` class provides a simple interface to modify and analyze
|
||||
the path elements in a DOM-style document. The DOM-style document is
|
||||
parsed into an ElementTree object (stored in the `tree` attribute and
|
||||
all SVG-Path (and, optionally, Path-like) elements are extracted into a
|
||||
list of svgpathtools Path objects. For more information on "Path-like"
|
||||
objects, see the below explanation of the `conversions` argument.
|
||||
parsed into an ElementTree object (stored in the `tree` attribute).
|
||||
|
||||
This class provides functions for extracting SVG data into Path objects.
|
||||
The Path output objects will be transformed based on their parent groups.
|
||||
|
||||
Args:
|
||||
merge_transforms (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
|
||||
|
@ -102,15 +200,26 @@ class Document:
|
|||
self.tree = etree.parse(filename)
|
||||
self.root = self.tree.getroot()
|
||||
|
||||
# get URI namespace (only necessary in OS X?)
|
||||
root_tag = self.tree.getroot().tag
|
||||
if root_tag[0] == "{":
|
||||
self._prefix = root_tag[:root_tag.find('}') + 1]
|
||||
else:
|
||||
self._prefix = ''
|
||||
# etree.register_namespace('', prefix)
|
||||
def flatten_all_paths(self,
|
||||
group_filter=lambda x: True,
|
||||
path_filter=lambda x: True,
|
||||
path_conversions=CONVERSIONS):
|
||||
return flatten_all_paths(self.tree.getroot(), group_filter, path_filter, path_conversions)
|
||||
|
||||
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):
|
||||
"""Returns a generator of all elements with the given tag.
|
||||
|
@ -120,68 +229,115 @@ class Document:
|
|||
"""
|
||||
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):
|
||||
"""To help with backwards compatibility."""
|
||||
return self.get_elements_by_tag('svg')[0].attrib
|
||||
|
||||
def get_path_attributes(self):
|
||||
"""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."""
|
||||
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."""
|
||||
if parent is None:
|
||||
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):
|
||||
"""Rewrite d-string's for each path in the `tree` attribute."""
|
||||
raise NotImplementedError
|
||||
if group_attribs is None:
|
||||
group_attribs = {}
|
||||
else:
|
||||
group_attribs = group_attribs.copy()
|
||||
|
||||
def save(self, filename, update=True):
|
||||
"""Write to svg to a file."""
|
||||
if update:
|
||||
self.update_tree()
|
||||
return SubElement(parent, 'g', group_attribs)
|
||||
|
||||
def save(self, filename=None):
|
||||
if filename is None:
|
||||
filename = self.original_filename
|
||||
|
||||
with open(filename, 'w') as output_svg:
|
||||
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."""
|
||||
if update:
|
||||
self.update_tree()
|
||||
|
||||
if filename is None:
|
||||
raise NotImplementedError
|
||||
filename = self.original_filename
|
||||
|
||||
# write to a (by default temporary) file
|
||||
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
|
||||
from __future__ import division, absolute_import, print_function
|
||||
import re
|
||||
import numpy as np
|
||||
import warnings
|
||||
|
||||
# Internal dependencies
|
||||
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
|
||||
|
||||
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.")
|
||||
|
||||
|
||||
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):
|
||||
"""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*)'
|
||||
)
|
||||
|
||||
def path2pathd(path):
|
||||
return path.get('d', '')
|
||||
|
||||
def ellipse2pathd(ellipse):
|
||||
"""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))
|
||||
return d
|
||||
|
||||
def line2pathd(l):
|
||||
return 'M' + l['x1'] + ' ' + l['y1'] + 'L' + l['x2'] + ' ' + l['y2']
|
||||
|
||||
def svg2paths(svg_file_location,
|
||||
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):
|
||||
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