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