Add optimization that prunes nested <g>-tags
An optimization that prunes nested <g>-tags when they contain exactly one <g> and nothing else (except whitespace nodes). This looks a bit like `removeNestedGroups` except it only touches <g> tags without attributes (but can remove <g>-tags completely from a tree, whereas this optimization always leaves at least one <g> tag behind). Closes: #215 Signed-off-by: Niels Thykier <niels@thykier.net>
This commit is contained in:
parent
7e917c9ca0
commit
e8e104d8b8
5 changed files with 216 additions and 0 deletions
|
|
@ -1143,6 +1143,78 @@ def moveCommonAttributesToParentGroup(elem, referencedElements):
|
||||||
return num
|
return num
|
||||||
|
|
||||||
|
|
||||||
|
def mergeSingleChildGroupIntoParent(elem):
|
||||||
|
"""
|
||||||
|
Merge <g X></g Y>...</g></g> into <g X Y>...</g>
|
||||||
|
|
||||||
|
Optimize the pattern:
|
||||||
|
|
||||||
|
<g a="1" c="4"><g a="2" b="3">...</g>/</g>
|
||||||
|
|
||||||
|
into
|
||||||
|
|
||||||
|
<g a="2" b="3" c="4">...</g>
|
||||||
|
|
||||||
|
This is only possible when:
|
||||||
|
|
||||||
|
* The parent has exactly one <g>-child node (ignoring whitespace)
|
||||||
|
* The child node is mergeable (per :func:`g_tag_is_unmergeable`)
|
||||||
|
|
||||||
|
Note that this function overlaps to some extend with `removeNestedGroups`.
|
||||||
|
However, `removeNestedGroups` work entirely on empty <g> and can completely
|
||||||
|
remove empty `<g>` tags. This works on any <g> tag containing <g> tags
|
||||||
|
(regardless of their attributes) and as such this function can never
|
||||||
|
completely eliminate all <g>-tags (but it does deal with attributes).
|
||||||
|
|
||||||
|
This function acts recursively on the given element.
|
||||||
|
"""
|
||||||
|
num = 0
|
||||||
|
|
||||||
|
# Depth first - fix child notes first
|
||||||
|
for childNode in elem.childNodes:
|
||||||
|
if childNode.nodeType == Node.ELEMENT_NODE:
|
||||||
|
num += mergeSingleChildGroupIntoParent(childNode)
|
||||||
|
|
||||||
|
# The elem node must be a <g> tag for this to be relevant.
|
||||||
|
if elem.nodeType != Node.ELEMENT_NODE or elem.nodeName != 'g' or \
|
||||||
|
elem.namespaceURI != NS['SVG']:
|
||||||
|
return num
|
||||||
|
|
||||||
|
if g_tag_is_unmergeable(elem):
|
||||||
|
# elem itself is protected, then leave it alone.
|
||||||
|
return num
|
||||||
|
|
||||||
|
g_node_idx = None
|
||||||
|
for idx, node in enumerate(elem.childNodes):
|
||||||
|
if node.nodeType == Node.TEXT_NODE and not node.nodeValue.strip():
|
||||||
|
# whitespace-only node; ignore
|
||||||
|
continue
|
||||||
|
if node.nodeType != Node.ELEMENT_NODE or node.nodeName != 'g' or \
|
||||||
|
node.namespaceURI != NS['SVG']:
|
||||||
|
# The elem node has something other than <g> tag; then this
|
||||||
|
# optimization does not apply
|
||||||
|
return num
|
||||||
|
if g_tag_is_unmergeable(node) or g_node_idx is not None:
|
||||||
|
# The elem node has two or more <g> tags or the <g> node it has
|
||||||
|
# is unmergeable; then this optimization does not apply
|
||||||
|
return num
|
||||||
|
g_node_idx = idx
|
||||||
|
|
||||||
|
# We got a optimization candidate
|
||||||
|
g_node = elem.childNodes[g_node_idx]
|
||||||
|
elem.childNodes.remove(g_node)
|
||||||
|
|
||||||
|
elem.childNodes[g_node_idx:g_node_idx] = g_node.childNodes
|
||||||
|
g_node.childNodes = []
|
||||||
|
|
||||||
|
for a in g_node.attributes.values():
|
||||||
|
elem.setAttribute(a.nodeName, a.nodeValue)
|
||||||
|
|
||||||
|
num += 1
|
||||||
|
|
||||||
|
return num
|
||||||
|
|
||||||
|
|
||||||
def mergeSiblingGroupsWithCommonAttributes(elem):
|
def mergeSiblingGroupsWithCommonAttributes(elem):
|
||||||
"""
|
"""
|
||||||
Merge two or more sibling <g> elements with the identical attributes.
|
Merge two or more sibling <g> elements with the identical attributes.
|
||||||
|
|
@ -3754,6 +3826,7 @@ def scourString(in_string, options=None):
|
||||||
# Collapse groups LAST, because we've created groups. If done before
|
# Collapse groups LAST, because we've created groups. If done before
|
||||||
# moveAttributesToParentGroup, empty <g>'s may remain.
|
# moveAttributesToParentGroup, empty <g>'s may remain.
|
||||||
if options.group_collapse:
|
if options.group_collapse:
|
||||||
|
_num_elements_removed += mergeSingleChildGroupIntoParent(doc.documentElement)
|
||||||
while removeNestedGroups(doc.documentElement) > 0:
|
while removeNestedGroups(doc.documentElement) > 0:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2075,6 +2075,104 @@ class MustKeepGInSwitch2(unittest.TestCase):
|
||||||
'Erroneously removed a <g> in a <switch>')
|
'Erroneously removed a <g> in a <switch>')
|
||||||
|
|
||||||
|
|
||||||
|
class GroupParentMerge(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_parent_merge(self):
|
||||||
|
doc = scourXmlFile('unittests/group-parent-merge.svg',
|
||||||
|
parse_args([]))
|
||||||
|
g_tags = doc.getElementsByTagName('g')
|
||||||
|
attrs = {
|
||||||
|
'font-family': 'Liberation Sans,Arial,Helvetica,sans-serif',
|
||||||
|
'text-anchor': 'middle',
|
||||||
|
'font-weight': '400',
|
||||||
|
'font-size': '24',
|
||||||
|
}
|
||||||
|
self.assertEqual(g_tags.length, 1,
|
||||||
|
'Inline single-child node <g> tags into parent <g>-tags')
|
||||||
|
|
||||||
|
g_tag = g_tags[0]
|
||||||
|
for attr_name, attr_value in attrs.items():
|
||||||
|
|
||||||
|
self.assertEqual(g_tag.getAttribute(attr_name), attr_value,
|
||||||
|
'Parent now has inherited attributes of obsolete <g>-tags')
|
||||||
|
|
||||||
|
def test_parent_merge_disabled(self):
|
||||||
|
doc = scourXmlFile('unittests/group-parent-merge.svg',
|
||||||
|
parse_args(['--disable-group-collapsing']))
|
||||||
|
g_tags = doc.getElementsByTagName('g')
|
||||||
|
attrs = {
|
||||||
|
'font-family': 'Liberation Sans,Arial,Helvetica,sans-serif',
|
||||||
|
'text-anchor': '',
|
||||||
|
'font-weight': '',
|
||||||
|
'font-size': '',
|
||||||
|
}
|
||||||
|
self.assertEqual(g_tags.length, 4,
|
||||||
|
'Inline single-child node <g> tags into parent <g>-tags')
|
||||||
|
|
||||||
|
# There should be exactly one <g> tag in the top of the document
|
||||||
|
# Note that the order returned by getElementsByTagName is not specified
|
||||||
|
# so we do not rely on its return value
|
||||||
|
g_tags = [g for g in doc.documentElement.childNodes if g.nodeName == 'g']
|
||||||
|
self.assertEqual(len(g_tags), 1,
|
||||||
|
'Optimization must not move the <g> up to the root')
|
||||||
|
g_tag = g_tags[0]
|
||||||
|
for attr_name, attr_value in attrs.items():
|
||||||
|
self.assertEqual(g_tag.getAttribute(attr_name), attr_value,
|
||||||
|
'Parent now has inherited attributes of obsolete <g>-tags')
|
||||||
|
|
||||||
|
def test_parent_merge2(self):
|
||||||
|
doc = scourXmlFile('unittests/group-parent-merge2.svg',
|
||||||
|
parse_args([]))
|
||||||
|
attrs = {
|
||||||
|
'font-family': 'Liberation Sans,Arial,Helvetica,sans-serif',
|
||||||
|
'text-anchor': 'middle',
|
||||||
|
'font-weight': '400',
|
||||||
|
'font-size': '', # The top-level g-node cannot have gotten this.
|
||||||
|
}
|
||||||
|
# There is one inner <g> that cannot be optimized, so there must be 2
|
||||||
|
# <g> tags in total
|
||||||
|
self.assertEqual(doc.getElementsByTagName('g').length, 2,
|
||||||
|
'Inline single-child node <g> tags into parent <g>-tags')
|
||||||
|
|
||||||
|
# There should be exactly one <g> tag in the top of the document
|
||||||
|
# Note that the order returned by getElementsByTagName is not specified
|
||||||
|
# so we do not rely on its return value
|
||||||
|
g_tags = [g for g in doc.documentElement.childNodes if g.nodeName == 'g']
|
||||||
|
self.assertEqual(len(g_tags), 1,
|
||||||
|
'Optimization must not move the <g> up to the root')
|
||||||
|
g_tag = g_tags[0]
|
||||||
|
for attr_name, attr_value in attrs.items():
|
||||||
|
self.assertEqual(g_tag.getAttribute(attr_name), attr_value,
|
||||||
|
'Parent now has inherited attributes of obsolete <g>-tags')
|
||||||
|
|
||||||
|
def test_parent_merge3(self):
|
||||||
|
doc = scourXmlFile('unittests/group-parent-merge3.svg',
|
||||||
|
parse_args(['--protect-ids-list=foo']))
|
||||||
|
attrs = {
|
||||||
|
'font-family': 'Liberation Sans,Arial,Helvetica,sans-serif',
|
||||||
|
'text-anchor': 'middle',
|
||||||
|
'font-weight': '400',
|
||||||
|
'font-size': '', # The top-level g-node cannot have gotten this.
|
||||||
|
}
|
||||||
|
# There is one inner <g> that cannot be optimized, so there must be 2
|
||||||
|
# <g> tags in total
|
||||||
|
self.assertEqual(doc.getElementsByTagName('g').length, 2,
|
||||||
|
'Inline single-child node <g> tags into parent <g>-tags')
|
||||||
|
|
||||||
|
self.assertIsNotNone(doc.getElementById('foo'), 'The inner <g> was left untouched')
|
||||||
|
|
||||||
|
# There should be exactly one <g> tag in the top of the document
|
||||||
|
# Note that the order returned by getElementsByTagName is not specified
|
||||||
|
# so we do not rely on its return value
|
||||||
|
g_tags = [g for g in doc.documentElement.childNodes if g.nodeName == 'g']
|
||||||
|
self.assertEqual(len(g_tags), 1,
|
||||||
|
'Optimization must not move the <g> up to the root')
|
||||||
|
g_tag = g_tags[0]
|
||||||
|
for attr_name, attr_value in attrs.items():
|
||||||
|
self.assertEqual(g_tag.getAttribute(attr_name), attr_value,
|
||||||
|
'Parent now has inherited attributes of obsolete <g>-tags')
|
||||||
|
|
||||||
|
|
||||||
class GroupSiblingMerge(unittest.TestCase):
|
class GroupSiblingMerge(unittest.TestCase):
|
||||||
|
|
||||||
def test_sibling_merge(self):
|
def test_sibling_merge(self):
|
||||||
|
|
|
||||||
15
unittests/group-parent-merge.svg
Normal file
15
unittests/group-parent-merge.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g font-family="Liberation Sans,Arial,Helvetica,sans-serif">
|
||||||
|
<g text-anchor="middle">
|
||||||
|
<g font-weight="400">
|
||||||
|
<g font-size="24">
|
||||||
|
<text x="50" y="30">Text1</text>
|
||||||
|
<text x="50" y="55">Text2</text>
|
||||||
|
<text x="50" y="80">Text3</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 491 B |
15
unittests/group-parent-merge2.svg
Normal file
15
unittests/group-parent-merge2.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g font-family="Liberation Sans,Arial,Helvetica,sans-serif">
|
||||||
|
<g text-anchor="middle">
|
||||||
|
<g font-weight="400">
|
||||||
|
<text x="50" y="30">Text1</text>
|
||||||
|
<g font-size="24">
|
||||||
|
<text x="50" y="55">Text2</text>
|
||||||
|
<text x="50" y="80">Text3</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 489 B |
15
unittests/group-parent-merge3.svg
Normal file
15
unittests/group-parent-merge3.svg
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'>
|
||||||
|
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g font-family="Liberation Sans,Arial,Helvetica,sans-serif">
|
||||||
|
<g text-anchor="middle">
|
||||||
|
<g font-weight="400">
|
||||||
|
<g id="foo" font-size="24">
|
||||||
|
<text x="50" y="30">Text1</text>
|
||||||
|
<text x="50" y="55">Text2</text>
|
||||||
|
<text x="50" y="80">Text3</text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 500 B |
Loading…
Add table
Add a link
Reference in a new issue