diff --git a/svgpathtools.egg-info/PKG-INFO b/svgpathtools.egg-info/PKG-INFO
index 29ff89b..9409782 100644
--- a/svgpathtools.egg-info/PKG-INFO
+++ b/svgpathtools.egg-info/PKG-INFO
@@ -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)
diff --git a/svgpathtools.egg-info/SOURCES.txt b/svgpathtools.egg-info/SOURCES.txt
index c1dde0f..1eb4a50 100644
--- a/svgpathtools.egg-info/SOURCES.txt
+++ b/svgpathtools.egg-info/SOURCES.txt
@@ -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
diff --git a/svgpathtools/__init__.py b/svgpathtools/__init__.py
index 60d2561..e678bb2 100644
--- a/svgpathtools/__init__.py
+++ b/svgpathtools/__init__.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
\ No newline at end of file
+ pass
diff --git a/svgpathtools/document.py b/svgpathtools/document.py
index 8065aa6..e87f13e 100644
--- a/svgpathtools/document.py
+++ b/svgpathtools/document.py
@@ -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:
diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py
index 9e7c22b..a4d710f 100644
--- a/svgpathtools/parser.py
+++ b/svgpathtools/parser.py
@@ -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
diff --git a/svgpathtools/path.py b/svgpathtools/path.py
index 13ec198..0c70a90 100644
--- a/svgpathtools/path.py
+++ b/svgpathtools/path.py
@@ -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.
diff --git a/svgpathtools/svg2paths.py b/svgpathtools/svg2paths.py
index 05258e8..e80645f 100644
--- a/svgpathtools/svg2paths.py
+++ b/svgpathtools/svg2paths.py
@@ -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,
diff --git a/test/groups.svg b/test/groups.svg
new file mode 100644
index 0000000..1787617
--- /dev/null
+++ b/test/groups.svg
@@ -0,0 +1,161 @@
+
+
\ No newline at end of file
diff --git a/test/test_groups.py b/test/test_groups.py
new file mode 100644
index 0000000..c3a1aef
--- /dev/null
+++ b/test/test_groups.py
@@ -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 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)
diff --git a/test/test_parsing.py b/test/test_parsing.py
index c052ad8..7c0ae42 100644
--- a/test/test_parsing.py
+++ b/test/test_parsing.py
@@ -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