From 332e959f529aa3af9d76b34d2fa134cff5211c7c Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Tue, 8 May 2018 16:52:36 -0700 Subject: [PATCH] Finish implementation of flatten_paths --- svgpathtools/document.py | 105 +++++++++++++++++++++++++------------- svgpathtools/parser.py | 2 +- svgpathtools/path.py | 26 ++++++++++ svgpathtools/svg2paths.py | 2 +- 4 files changed, 98 insertions(+), 37 deletions(-) diff --git a/svgpathtools/document.py b/svgpathtools/document.py index 67baa6d..e3f8c1a 100644 --- a/svgpathtools/document.py +++ b/svgpathtools/document.py @@ -74,11 +74,17 @@ CONVERSIONS = {'path': path2pathd, 'rect': rect2pathd} -def flatten_paths(group, return_attribs = False, group_filter = lambda x: True, path_filter = lambda x: True, - path_conversions = CONVERSIONS): - """Returns the paths inside a group (recursively), expressing the paths in the root coordinates +def flatten_paths(group, return_attribs=False, + group_filter=lambda x: True, + path_filter=lambda x: True, + path_conversions=CONVERSIONS): + """Returns the paths inside a group (recursively), expressing the paths in the root coordinates. - @param group is an Element""" + 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). + """ if not isinstance(group, Element): raise TypeError('Must provide an xml.etree.Element object') @@ -86,57 +92,57 @@ def flatten_paths(group, return_attribs = False, group_filter = lambda x: True, if not group_filter(group): return [] - def get_relevant_children(parent): - return filter(group_filter, parent.findall('g')) - # 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 the group element, the second entry is its transform, the third is its - # list of child elements, and the fourth is the index of the next child to traverse for that element. - StackElement = collections.namedtuple('StackElement', ['group', 'transform', 'children', 'next_child_index']) + # 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): - return StackElement(element, parse_transform(element.get('transform')), get_relevant_children(element), 0) + def new_stack_element(element, last_tf): + return StackElement(element, last_tf * parse_transform(element.get('transform'))) - stack = [new_stack_element(group)] + def get_relevant_children(parent, last_tf): + children = [] + for elem in filter(group_filter, parent.iterfind('g')): + children.append(new_stack_element(elem, last_tf)) + return children + + stack = [new_stack_element(group, np.identity(3))] paths = [] - if return_attribs: path_attribs = [] + path_attribs = [] while stack: - top = stack[-1] + top = stack.pop() - for key in path_conversions: + # 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: for path_elem in filter(path_filter, top.group.iterfind(key)): - pass # TODO: Finish this - - - + paths.append(transform(parse_path(converter(path_elem)), top.transform)) + if return_attribs: + path_attribs.append(path_elem.attrib) + stack.extend(get_relevant_children(top.group, top.transform)) + if return_attribs: + return paths, path_attribs + else: + return paths 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 @@ -157,6 +163,35 @@ class Document: self._prefix = '' # etree.register_namespace('', prefix) + def flatten_paths(self, return_attribs=False, + group_filter=lambda x: True, + path_filter=lambda x: True, + path_conversions=CONVERSIONS): + paths = [] + path_attribs = [] + + # We don't need to worry about transforming any paths that lack a group. + # We can just append them to the list of paths and grab their attributes. + for key, converter in path_conversions: + for path_elem in filter(path_filter, self.tree.getroot().iterfind(key)): + paths.append(parse_path(converter(path_elem))) + if return_attribs: + path_attribs.append(path_elem.attrib) + + for group_elem in filter(group_filter, self.tree.getroot().iterfind('g')): + if return_attribs: + new_paths, new_attribs = flatten_paths(group_elem, return_attribs, + group_filter, path_filter, path_conversions) + path_attribs.extend(new_attribs) + else: + new_paths = flatten_paths(group_elem, return_attribs, + group_filter, path_filter, path_conversions) + paths.extend(new_paths) + + if return_attribs: + return new_paths, new_attribs + else: + return new_paths def get_elements_by_tag(self, tag): """Returns a generator of all elements with the given tag. diff --git a/svgpathtools/parser.py b/svgpathtools/parser.py index e32ea50..e9a7f94 100644 --- a/svgpathtools/parser.py +++ b/svgpathtools/parser.py @@ -202,7 +202,7 @@ def parse_path(pathdef, current_pos=0j, tree_element=None): def _check_num_parsed_values(values, allowed): - if not any( num == len(values) for num in 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)) 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 eab6790..cc8374c 100644 --- a/svgpathtools/svg2paths.py +++ b/svgpathtools/svg2paths.py @@ -10,7 +10,7 @@ import xml.etree.cElementTree as etree from .parser import parse_path def path2pathd(path): - return path.get('d', None) + return path.get('d', '') def ellipse2pathd(ellipse): """converts the parameters from an ellipse or a circle to a string