From cdf5e479a689f4d3b61a665e91e5479214809b79 Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Tue, 20 Mar 2018 21:34:20 +0000 Subject: [PATCH] Merge sibling nodes with identical attributes In some cases, gnuplot generates a very suboptimal SVG content of the following pattern: ... repeated 10+ more times here ... A more optimal pattern would be: ... 10+ more paths here ... This patch enables that optimization by handling the merging of two sibling entries that have identical attributes. In the above example that does not solve the rewrite from "currentColor" to "#000" for the stroke attribute. However, the existing code already handles that automatically after the elements have been merged. This change provides comparable results to --create-groups as shown by the following diagram while being a distinct optimization: +----------------------------+-------+--------+ | Test | Size | in % | +----------------------------+-------+--------+ | baseline | 17961 | 100% | | baseline + --create-groups | 17418 | 97.0% | | patched | 16939 | 94.3% | | patched + --create-groups | 16855 | 93.8% | +----------------------------+-------+--------+ The image used in the size table above was generated based on the instructions from https://bugs.debian.org/858039#10 with gnuplot 5.2 patchlevel 2. Beyond the test-based "--create-groups", the following scour command-line parameters were used: --enable-id-stripping --enable-comment-stripping \ --shorten-ids --indent=none Note that the baseline was scour'ed repeatedly to stablize the image size. Signed-off-by: Niels Thykier --- scour/scour.py | 71 +++++++++++++++++++++++++++++++ testscour.py | 15 +++++++ unittests/group-sibling-merge.svg | 29 +++++++++++++ 3 files changed, 115 insertions(+) create mode 100644 unittests/group-sibling-merge.svg diff --git a/scour/scour.py b/scour/scour.py index 661dd9a..4b71ad1 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -1134,6 +1134,75 @@ def moveCommonAttributesToParentGroup(elem, referencedElements): return num +def mergeSiblingGroupsWithCommonAttributes(elem): + """ + Merge two or more sibling elements with the identical attributes. + + This function acts recursively on the given element. + """ + + num = 0 + i = elem.childNodes.length - 1 + while i >= 0: + currentNode = elem.childNodes.item(i) + if currentNode.nodeType != Node.ELEMENT_NODE or currentNode.nodeName != 'g' or \ + currentNode.namespaceURI != NS['SVG']: + i -= 1 + continue + attributes = {a.nodeName: a.nodeValue for a in currentNode.attributes.values()} + runStart, runEnd = i, i + runElements = 1 + while runStart > 0: + nextNode = elem.childNodes.item(runStart - 1) + if nextNode.nodeType == Node.ELEMENT_NODE: + if nextNode.nodeName != 'g' or nextNode.namespaceURI != NS['SVG']: + break + nextAttributes = {a.nodeName: a.nodeValue for a in nextNode.attributes.values()} + hasNoMergeTags = (True for n in nextNode.childNodes + if n.nodeType == Node.ELEMENT_NODE + and n.nodeName in ('title', 'desc') + and n.namespaceURI == NS['SVG']) + if attributes != nextAttributes or any(hasNoMergeTags): + break + else: + runElements += 1 + runStart -= 1 + else: + runStart -= 1 + + # Next loop will start from here + i = runStart - 1 + + if runElements < 2: + continue + + # Find the entry that starts the run (we might have run + # past it into a text node or a comment node. + while True: + node = elem.childNodes.item(runStart) + if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: + break + runStart += 1 + primaryGroup = elem.childNodes.item(runStart) + runStart += 1 + nodes = elem.childNodes[runStart:runEnd+1] + for node in nodes: + if node.nodeType == Node.ELEMENT_NODE and node.nodeName == 'g' and node.namespaceURI == NS['SVG']: + # Merge + primaryGroup.childNodes.extend(node.childNodes) + node.childNodes = [] + else: + primaryGroup.childNodes.append(node) + elem.childNodes.remove(node) + + # each child gets the same treatment, recursively + for childNode in elem.childNodes: + if childNode.nodeType == Node.ELEMENT_NODE: + num += mergeSiblingGroupsWithCommonAttributes(childNode) + + return num + + def createGroupsForCommonAttributes(elem): """ Creates elements to contain runs of 3 or more @@ -3658,6 +3727,8 @@ def scourString(in_string, options=None): while removeDuplicateGradients(doc) > 0: pass + if options.group_collapse: + _num_elements_removed += mergeSiblingGroupsWithCommonAttributes(doc.documentElement) # create elements if there are runs of elements with the same attributes. # this MUST be before moveCommonAttributesToParentGroup. if options.group_create: diff --git a/testscour.py b/testscour.py index 68521ce..d9a460b 100755 --- a/testscour.py +++ b/testscour.py @@ -2075,6 +2075,21 @@ class MustKeepGInSwitch2(unittest.TestCase): 'Erroneously removed a in a ') +class GroupSiblingMerge(unittest.TestCase): + + def test_sibling_merge(self): + doc = scourXmlFile('unittests/group-sibling-merge.svg', + parse_args([])) + self.assertEqual(doc.getElementsByTagName('g').length, 5, + 'Merged sibling tags with similar values') + + def test_sibling_merge_disabled(self): + doc = scourXmlFile('unittests/group-sibling-merge.svg', + parse_args(['--disable-group-collapsing'])) + self.assertEqual(doc.getElementsByTagName('g').length, 8, + 'Sibling merging is disabled by --disable-group-collapsing') + + class GroupCreation(unittest.TestCase): def runTest(self): diff --git a/unittests/group-sibling-merge.svg b/unittests/group-sibling-merge.svg new file mode 100644 index 0000000..0a2181c --- /dev/null +++ b/unittests/group-sibling-merge.svg @@ -0,0 +1,29 @@ + + +Produced by GNUPLOT 5.2 patchlevel 8 + + + + +0 + + + + + +5000 + + + + + +10000 + + + + + +15000 + + +