Finish implementation of flatten_paths
parent
88e21fcbc0
commit
332e959f52
|
@ -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,
|
||||||
|
group_filter=lambda x: True,
|
||||||
|
path_filter=lambda x: True,
|
||||||
path_conversions=CONVERSIONS):
|
path_conversions=CONVERSIONS):
|
||||||
"""Returns the paths inside a group (recursively), expressing the paths in the root coordinates
|
"""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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue