Merge sibling <g> nodes with identical attributes

In some cases, gnuplot generates a very suboptimal SVG content of the
following pattern:

        <g color="black" fill="none" stroke="currentColor">
        <path d="m82.5 323.3v-4.1" stroke="#000"/>
        </g>
        <g color="black" fill="none" stroke="currentColor">
        <path d="m116.4 323.3v-4.1" stroke="#000"/>
        </g>
        ... repeated 10+ more times here ...
        <g color="black" fill="none" stroke="currentColor">
        <path d="m65.4 72.8v250.5h420v-250.5h-420z" stroke="#000"/>
        </g>

A more optimal pattern would be:

        <g color="black" fill="none" stroke="#000">
        <path d="m82.5 323.3v-4.1"/>
        <path d="m116.4 323.3v-4.1"/>
        ... 10+ more paths here ...
        <path d="m65.4 72.8v250.5h420v-250.5h-420z"/>
        </g>

This patch enables that optimization by handling the merging of two
sibling <g> 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 <g> 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 <niels@thykier.net>
This commit is contained in:
Niels Thykier 2018-03-20 21:34:20 +00:00
parent 40753af88a
commit cdf5e479a6
No known key found for this signature in database
GPG key ID: A65B78DBE67C7AAC
3 changed files with 115 additions and 0 deletions

View file

@ -1134,6 +1134,75 @@ def moveCommonAttributesToParentGroup(elem, referencedElements):
return num
def mergeSiblingGroupsWithCommonAttributes(elem):
"""
Merge two or more sibling <g> 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 <g> 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 <g> 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 <g> elements if there are runs of elements with the same attributes.
# this MUST be before moveCommonAttributesToParentGroup.
if options.group_create:

View file

@ -2075,6 +2075,21 @@ class MustKeepGInSwitch2(unittest.TestCase):
'Erroneously removed a <g> in a <switch>')
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 <g> 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):

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg">
<desc>Produced by GNUPLOT 5.2 patchlevel 8</desc>
<rect width="900" height="600" fill="none"/>
<g color="black" fill="none">
<path d="m88.5 564h9m777.5 0h-9" stroke="#000"/>
<g transform="translate(80.2,567.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end">
<text><tspan font-family="Arial">0</tspan></text>
</g>
</g>
<g color="black" fill="none">
<path d="m88.5 473h9m777.5 0h-9" stroke="#000"/>
<g transform="translate(80.2,476.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end">
<text><tspan font-family="Arial">5000</tspan></text>
</g>
</g>
<g color="black" fill="none">
<path d="m88.5 382h9m777.5 0h-9" stroke="#000"/>
<g transform="translate(80.2,385.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end">
<text><tspan font-family="Arial">10000</tspan></text>
</g>
</g>
<g color="black" fill="none">
<path d="m88.5 291h9m777.5 0h-9" stroke="#000"/>
<g transform="translate(80.2,294.9)" fill="#000" font-family="Arial" font-size="12" text-anchor="end">
<text><tspan font-family="Arial">15000</tspan></text>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB