Support flattening paths of only nested groups (#88)
* Fixed document.flatten_group(~) for nested groups and added a test * Add space for PEP8 conformance * Add documentation for document.get_group() * Use group_search_xpath to be consistent and customizable * Fix lexical mistake in comments * Fix grammar mistake in commentspull/114/head
parent
b117f85811
commit
f99f9d6bb3
|
@ -101,6 +101,8 @@ def flatten_all_paths(group, group_filter=lambda x: True,
|
||||||
|
|
||||||
# Stop right away if the group_selector rejects this group
|
# Stop right away if the group_selector rejects this group
|
||||||
if not group_filter(group):
|
if not group_filter(group):
|
||||||
|
warnings.warn('The input group [{}] (id attribute: {}) was rejected by the group filter'
|
||||||
|
.format(group, group.get('id')))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# To handle the transforms efficiently, we'll traverse the tree of
|
# To handle the transforms efficiently, we'll traverse the tree of
|
||||||
|
@ -174,10 +176,48 @@ def flatten_group(group_to_flatten, root, recursive=True,
|
||||||
else:
|
else:
|
||||||
desired_groups.add(id(group_to_flatten))
|
desired_groups.add(id(group_to_flatten))
|
||||||
|
|
||||||
|
ignore_paths = set()
|
||||||
|
# Use breadth-first search to find the path to the group that we care about
|
||||||
|
if root is not group_to_flatten:
|
||||||
|
search = [[root]]
|
||||||
|
route = None
|
||||||
|
while search:
|
||||||
|
top = search.pop(0)
|
||||||
|
frontier = top[-1]
|
||||||
|
for child in frontier.iterfind(group_search_xpath, SVG_NAMESPACE):
|
||||||
|
if child is group_to_flatten:
|
||||||
|
route = top
|
||||||
|
break
|
||||||
|
future_top = list(top)
|
||||||
|
future_top.append(child)
|
||||||
|
search.append(future_top)
|
||||||
|
|
||||||
|
if route is not None:
|
||||||
|
for group in route:
|
||||||
|
# Add each group from the root to the parent of the desired group
|
||||||
|
# to the list of groups that we should traverse. This makes sure
|
||||||
|
# that flatten_all_paths will not stop before reaching the desired
|
||||||
|
# group.
|
||||||
|
desired_groups.add(id(group))
|
||||||
|
for key in path_conversions.keys():
|
||||||
|
for path_elem in group.iterfind('svg:'+key, SVG_NAMESPACE):
|
||||||
|
# Add each path in the parent groups to the list of paths
|
||||||
|
# that should be ignored. The user has not requested to
|
||||||
|
# flatten the paths of the parent groups, so we should not
|
||||||
|
# include any of these in the result.
|
||||||
|
ignore_paths.add(id(path_elem))
|
||||||
|
break
|
||||||
|
|
||||||
|
if route is None:
|
||||||
|
raise ValueError('The group_to_flatten is not a descendant of the root!')
|
||||||
|
|
||||||
def desired_group_filter(x):
|
def desired_group_filter(x):
|
||||||
return (id(x) in desired_groups) and group_filter(x)
|
return (id(x) in desired_groups) and group_filter(x)
|
||||||
|
|
||||||
return flatten_all_paths(root, desired_group_filter, path_filter,
|
def desired_path_filter(x):
|
||||||
|
return (id(x) not in ignore_paths) and path_filter(x)
|
||||||
|
|
||||||
|
return flatten_all_paths(root, desired_group_filter, desired_path_filter,
|
||||||
path_conversions, group_search_xpath)
|
path_conversions, group_search_xpath)
|
||||||
|
|
||||||
|
|
||||||
|
@ -223,13 +263,17 @@ class Document:
|
||||||
if all(isinstance(s, str) for s in group):
|
if all(isinstance(s, str) for s in group):
|
||||||
# If we're given a list of strings, assume it represents a
|
# If we're given a list of strings, assume it represents a
|
||||||
# nested sequence
|
# nested sequence
|
||||||
group = self.get_or_add_group(group)
|
group = self.get_group(group)
|
||||||
elif not isinstance(group, Element):
|
elif not isinstance(group, Element):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
'Must provide a list of strings that represent a nested '
|
'Must provide a list of strings that represent a nested '
|
||||||
'group name, or provide an xml.etree.Element object. '
|
'group name, or provide an xml.etree.Element object. '
|
||||||
'Instead you provided {0}'.format(group))
|
'Instead you provided {0}'.format(group))
|
||||||
|
|
||||||
|
if group is None:
|
||||||
|
warnings.warn("Could not find the requested group!")
|
||||||
|
return []
|
||||||
|
|
||||||
return flatten_group(group, self.tree.getroot(), recursive,
|
return flatten_group(group, self.tree.getroot(), recursive,
|
||||||
group_filter, path_filter, path_conversions)
|
group_filter, path_filter, path_conversions)
|
||||||
|
|
||||||
|
@ -282,6 +326,37 @@ class Document:
|
||||||
def contains_group(self, group):
|
def contains_group(self, group):
|
||||||
return any(group is owned for owned in self.tree.iter())
|
return any(group is owned for owned in self.tree.iter())
|
||||||
|
|
||||||
|
def get_group(self, nested_names, name_attr='id'):
|
||||||
|
"""Get a group from the tree, or None if the requested group
|
||||||
|
does not exist. Use get_or_add_group(~) if you want a new group
|
||||||
|
to be created if it did not already exist.
|
||||||
|
|
||||||
|
`nested_names` is a list of strings which represent group names.
|
||||||
|
Each group name will be nested inside of the previous group name.
|
||||||
|
|
||||||
|
`name_attr` is the group attribute that is being used to
|
||||||
|
represent the group's name. Default is 'id', but some SVGs may
|
||||||
|
contain custom name labels, like 'inkscape:label'.
|
||||||
|
|
||||||
|
Returns the request group. If the requested group did not
|
||||||
|
exist, this function will return a None value.
|
||||||
|
"""
|
||||||
|
group = self.tree.getroot()
|
||||||
|
# Drill down through the names until we find the desired group
|
||||||
|
while len(nested_names):
|
||||||
|
prev_group = group
|
||||||
|
next_name = nested_names.pop(0)
|
||||||
|
for elem in group.iterfind(SVG_GROUP_TAG, SVG_NAMESPACE):
|
||||||
|
if elem.get(name_attr) == next_name:
|
||||||
|
group = elem
|
||||||
|
break
|
||||||
|
|
||||||
|
if prev_group is group:
|
||||||
|
# The nested group could not be found, so we return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
return group
|
||||||
|
|
||||||
def get_or_add_group(self, nested_names, name_attr='id'):
|
def get_or_add_group(self, nested_names, name_attr='id'):
|
||||||
"""Get a group from the tree, or add a new one with the given
|
"""Get a group from the tree, or add a new one with the given
|
||||||
name structure.
|
name structure.
|
||||||
|
|
|
@ -165,6 +165,14 @@ class TestGroups(unittest.TestCase):
|
||||||
|
|
||||||
self.assertEqual(expected_count, count)
|
self.assertEqual(expected_count, count)
|
||||||
|
|
||||||
|
def test_nested_group(self):
|
||||||
|
# A bug in the flatten_group() implementation made it so that only top-level
|
||||||
|
# groups could have their paths flattened. This is a regression test to make
|
||||||
|
# sure that when a nested group is requested, its paths can also be flattened.
|
||||||
|
doc = Document(join(dirname(__file__), 'groups.svg'))
|
||||||
|
result = doc.flatten_group(['matrix group', 'scale group'])
|
||||||
|
self.assertEqual(len(result), 5)
|
||||||
|
|
||||||
def test_add_group(self):
|
def test_add_group(self):
|
||||||
# Test `Document.add_group()` function and related Document functions.
|
# Test `Document.add_group()` function and related Document functions.
|
||||||
doc = Document(None)
|
doc = Document(None)
|
||||||
|
|
Loading…
Reference in New Issue