Finish implementation of flatten_paths

pull/58/head
Michael X. Grey 2018-05-08 16:52:36 -07:00
parent 88e21fcbc0
commit 332e959f52
4 changed files with 98 additions and 37 deletions

View File

@ -74,11 +74,17 @@ CONVERSIONS = {'path': path2pathd,
'rect': rect2pathd} 'rect': rect2pathd}
def flatten_paths(group, return_attribs = False, group_filter = lambda x: True, path_filter = lambda x: True, def flatten_paths(group, return_attribs=False,
path_conversions = CONVERSIONS): group_filter=lambda x: True,
"""Returns the paths inside a group (recursively), expressing the paths in the root coordinates 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): if not isinstance(group, Element):
raise TypeError('Must provide an xml.etree.Element object') 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): if not group_filter(group):
return [] 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. # 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 # The first entry in the tuple is a group element and the second entry is its transform. As we pop each entry in
# list of child elements, and the fourth is the index of the next child to traverse for that element. # the stack, we will add all its child group elements to the stack.
StackElement = collections.namedtuple('StackElement', ['group', 'transform', 'children', 'next_child_index']) StackElement = collections.namedtuple('StackElement', ['group', 'transform'])
def new_stack_element(element): def new_stack_element(element, last_tf):
return StackElement(element, parse_transform(element.get('transform')), get_relevant_children(element), 0) 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 = [] paths = []
if return_attribs: path_attribs = [] path_attribs = []
while stack: 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)): 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: class Document:
def __init__(self, filename, conversions=False, transform_paths=True): def __init__(self, filename):
"""(EXPERIMENTAL) A container for a DOM-style document. """A container for a DOM-style SVG document.
The `Document` class provides a simple interface to modify and analyze The `Document` class provides a simple interface to modify and analyze
the path elements in a DOM-style document. The DOM-style document is the path elements in a DOM-style document. The DOM-style document is
parsed into an ElementTree object (stored in the `tree` attribute) and parsed into an ElementTree object (stored in the `tree` attribute).
all SVG-Path (and, optionally, Path-like) elements are extracted into a
list of svgpathtools Path objects. For more information on "Path-like" This class provides functions for extracting SVG data into Path objects.
objects, see the below explanation of the `conversions` argument. The Path output objects will be transformed based on their parent groups.
Args: Args:
merge_transforms (object):
filename (str): The filename of the DOM-style 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 # remember location of original svg file
@ -157,6 +163,35 @@ class Document:
self._prefix = '' self._prefix = ''
# etree.register_namespace('', 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): def get_elements_by_tag(self, tag):
"""Returns a generator of all elements with the given tag. """Returns a generator of all elements with the given tag.

View File

@ -202,7 +202,7 @@ def parse_path(pathdef, current_pos=0j, tree_element=None):
def _check_num_parsed_values(values, allowed): 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: if len(allowed) > 1:
warnings.warn('Expected one of the following number of values {0}, found {1}: {2}' warnings.warn('Expected one of the following number of values {0}, found {1}: {2}'
.format(allowed, len(values), values)) .format(allowed, len(values), values))

View File

@ -207,6 +207,32 @@ def translate(curve, z0):
"QuadraticBezier, CubicBezier, or Arc object.") "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): def bezier_unit_tangent(seg, t):
"""Returns the unit tangent of the segment at t. """Returns the unit tangent of the segment at t.

View File

@ -10,7 +10,7 @@ import xml.etree.cElementTree as etree
from .parser import parse_path from .parser import parse_path
def path2pathd(path): def path2pathd(path):
return path.get('d', None) return path.get('d', '')
def ellipse2pathd(ellipse): def ellipse2pathd(ellipse):
"""converts the parameters from an ellipse or a circle to a string """converts the parameters from an ellipse or a circle to a string