Merge pull request #58 from mxgrey/ElementTree

Element tree
ElementTree
Andy Port 2018-05-28 21:58:24 -07:00 committed by GitHub
commit acb4085fb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 657 additions and 72 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,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

View File

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

View File

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

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.

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