Compare commits

...

31 Commits

Author SHA1 Message Date
Andy Port acb4085fb2
Merge pull request #58 from mxgrey/ElementTree
Element tree
2018-05-28 21:58:24 -07:00
Michael X. Grey 0fdaad0284 Remove unnecessary import 2018-05-29 12:54:20 +08:00
Michael X. Grey 79ab1e6a43 Remove accidental paranthesis 2018-05-29 12:42:25 +08:00
Michael X. Grey a3649bc51e Merging with latest upstream ElementTree branch 2018-05-29 12:34:08 +08:00
Andy Port d4ab8ad435 updated to match master 2018-05-28 20:40:54 -07:00
Andy Port 45d3389303
Merge pull request #57 from mathandy/master
minor docstring improvements
2018-05-28 20:25:38 -07:00
Andy Port 609cbc776a got rid of svg2path changes (reverted to master) 2018-05-28 20:14:45 -07:00
Andy Port f966999bc5 minor docstring improvements 2018-05-28 19:48:48 -07:00
Andy Port 30d511577b Updated with master 2018-05-28 19:35:40 -07:00
Michael X. Grey ff96a37d16 Add a test for <path> elements that contain a 'transform' attribute 2018-05-25 16:50:40 +08:00
Michael X. Grey a6ceec4f0d Return a reference to an element instead of a copied dictionary of attributes 2018-05-25 16:42:36 +08:00
Michael X. Grey b547969846 Create a unit test for parsing SVG groups 2018-05-24 19:40:41 +08:00
Michael X. Grey 29eb1e9364 Fix multiplication of numpy matrices -- need to use .dot() instead of operator* 2018-05-24 19:36:22 +08:00
Michael X. Grey 1c8ca10f73 Print out some paths to see that they're sane 2018-05-10 17:06:16 -07:00
Michael X. Grey 457a054fb8 Improve the way the svg namespace is handled 2018-05-10 17:05:47 -07:00
Michael X. Grey a29b392234 Debugging xml namespace behavior -- needs improvement 2018-05-10 16:45:41 -07:00
Michael X. Grey be675f1b1c Clean up implementation of document classes 2018-05-10 12:55:19 -07:00
Michael X. Grey f5a7fe77d1 Refactoring flatten_paths() into flatten_all_paths() 2018-05-09 17:40:49 -07:00
Michael X. Grey 3512f86968 Beginning to write tests for groups 2018-05-09 17:39:54 -07:00
Michael X. Grey 332e959f52 Finish implementation of flatten_paths 2018-05-08 16:52:36 -07:00
Michael X. Grey 88e21fcbc0 Fix merge conflicts with master 2018-05-08 13:24:50 -07:00
Michael X. Grey 70f6a78288 Implementing a depth-first flattening of groups 2018-05-08 13:22:19 -07:00
Michael X. Grey 8a4801bcde Tweaks to transform parsing implementation 2018-05-08 11:55:37 -07:00
Michael X. Grey 17d283abe0 Iterate on the implementation of the Document class 2018-05-07 18:31:06 -07:00
Michael X. Grey 9370d3f5a2 Add a method to parse transform strings 2018-05-07 18:30:15 -07:00
Michael X. Grey f2897f6b79 Make the Document class available when importing the library 2018-05-07 18:29:19 -07:00
Andy 52ffcf1746 made some design changes 2017-09-19 20:47:36 -04:00
Andy 4d18ef885f style changes 2017-09-14 22:17:58 -04:00
Andy 8a44431d5a fixed documentation line-width to be PEP 8 compliant 2017-09-14 22:16:37 -04:00
Andy 5489321203 fixed documentation line-width to be PEP 8 compliant 2017-09-14 22:06:25 -04:00
Andy d20ef060aa Some progress (and added CONTRIBUTING.md) 2017-07-13 20:41:55 -07:00
10 changed files with 789 additions and 8 deletions

View File

@ -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)

View File

@ -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

View File

@ -12,6 +12,7 @@ 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

346
svgpathtools/document.py Normal file
View File

@ -0,0 +1,346 @@
"""(Experimental) replacement for import/export functionality.
This module contains the `Document` class, a container for a DOM-style
document (e.g. svg, html, xml, etc.) designed to replace and improve
upon the IO functionality of svgpathtools (i.e. the svg2paths and
disvg/wsvg functions).
An Historic Note:
The functionality in this module is meant to replace and improve
upon the IO functionality previously provided by the the
`svg2paths` and `disvg`/`wsvg` functions.
Example:
Typical usage looks something like the following.
>> from svgpathtools import *
>> doc = Document('my_file.html')
>> for p in doc.paths:
>> foo(p) # do stuff using svgpathtools functionality
>> foo2(doc.tree) # do stuff using ElementTree's functionality
>> doc.display() # display doc in OS's default application
>> doc.save('my_new_file.html')
Todo: (please see contributor guidelines in CONTRIBUTING.md)
* Finish "NotImplemented" methods.
* Find some clever (and easy to implement) way to create a thorough
set of unittests.
* Finish Documentation for each method (approximately following the
Google Python Style Guide, see [1]_ for some nice examples).
For nice style examples, see [1]_.
Some thoughts on this module's direction:
* The `Document` class should ONLY grab path elements that are
inside an SVG.
* To handle transforms... there should be a "get_transform"
function and also a "flatten_transforms" tool that removes any
present transform attributes from all SVG-Path elements in the
document (applying the transformations before to the svgpathtools
Path objects).
Note: This ability to "flatten" will ignore CSS files (and any
relevant files that are not parsed into the tree automatically by
ElementTree)... that is unless you have any bright ideas on this.
I really know very little about DOM-style documents.
A Big Problem:
Derivatives and other functions may be messed up by
transforms unless transforms are flattened (and not included in
css)
"""
# External dependencies
from __future__ import division, absolute_import, print_function
import os
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 .parser import parse_transform
from .svg2paths import (path2pathd, ellipse2pathd, line2pathd, polyline2pathd,
polygon2pathd, rect2pathd)
from .misctools import open_in_browser
from .path import *
# 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):
"""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).
This class provides functions for extracting SVG data into Path objects.
The Path output objects will be transformed based on their parent groups.
Args:
filename (str): The filename of the DOM-style object.
"""
# remember location of original svg file
if filename is not None and os.path.dirname(filename) == '':
self.original_filename = os.path.join(os.getcwd(), filename)
else:
self.original_filename = filename
# parse svg to ElementTree object
self.tree = etree.parse(filename)
self.root = self.tree.getroot()
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)
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.
Note: for more advanced XML-related functionality, use the
`tree` attribute (an ElementTree object).
"""
return self.tree.iter(tag=self._prefix + tag)
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.tree.getroot().iter('path')]
def add_path(self, path, attribs=None, group=None):
"""Add a new path to the SVG."""
# 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()
elif not self.contains_group(parent):
warnings.warn('The requested group {0} does not belong to this Document'.format(parent))
if group_attribs is None:
group_attribs = {}
else:
group_attribs = group_attribs.copy()
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):
"""Displays/opens the doc using the OS's default application."""
if filename is None:
filename = self.original_filename
# write to a (by default temporary) file
with open(filename, 'w') as output_svg:
output_svg.write(etree.tostring(self.tree.getroot()))
open_in_browser(filename)

View File

@ -1,11 +1,12 @@
"""This submodule contains the path_parse() function used to convert SVG path
element d-strings into svgpathtools Path objects.
Note: This file was taken (nearly) as is from the svg.path module
(v 2.0)."""
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
@ -26,7 +27,7 @@ def _tokenize_path(pathdef):
yield token
def parse_path(pathdef, current_pos=0j):
def parse_path(pathdef, current_pos=0j, tree_element=None):
# In the SVG specs, initial movetos are absolute, even if
# specified as 'm'. This is the default behavior here as well.
# But if you pass in a current_pos variable, the initial moveto
@ -35,7 +36,11 @@ def parse_path(pathdef, current_pos=0j):
# Reverse for easy use of .pop()
elements.reverse()
segments = Path()
if tree_element is None:
segments = Path()
else:
segments = Path(tree_element=tree_element)
start_pos = None
command = None
@ -194,3 +199,98 @@ def parse_path(pathdef, current_pos=0j):
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

View File

@ -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.
@ -1718,6 +1744,9 @@ class Path(MutableSequence):
self._start = None
self._end = None
if 'tree_element' in kw:
self._tree_element = kw['tree_element']
def __getitem__(self, index):
return self._segments[index]

View File

@ -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,

161
test/groups.svg Normal file
View File

@ -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

135
test/test_groups.py Normal file
View File

@ -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)

View File

@ -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