Remove uselessly nested groups, fixed up test script
This commit is contained in:
parent
747a282f5f
commit
579a1f2982
3 changed files with 98 additions and 58 deletions
55
scour.py
55
scour.py
|
|
@ -28,8 +28,6 @@
|
||||||
# TODO: Adapt this script into an Inkscape python plugin
|
# TODO: Adapt this script into an Inkscape python plugin
|
||||||
#
|
#
|
||||||
# * Specify a limit to the precision of all positional elements.
|
# * Specify a limit to the precision of all positional elements.
|
||||||
# * Clean up XML Elements
|
|
||||||
# * Collapse multiple redundent groups
|
|
||||||
# * Clean up Definitions
|
# * Clean up Definitions
|
||||||
# * Remove duplicate gradient stops
|
# * Remove duplicate gradient stops
|
||||||
# * Collapse duplicate gradient definitions
|
# * Collapse duplicate gradient definitions
|
||||||
|
|
@ -48,12 +46,23 @@
|
||||||
# * Collapse all group based transformations
|
# * Collapse all group based transformations
|
||||||
|
|
||||||
# Next Up:
|
# Next Up:
|
||||||
# - Remove unnecessary nested <g> elements
|
# + Remove unnecessary nested <g> elements
|
||||||
# - Remove duplicate gradient stops
|
# - Remove duplicate gradient stops (same offset, stop-color, stop-opacity)
|
||||||
# - Convert all colors to #RRGGBB format
|
# - Convert all colors to #RRGGBB format
|
||||||
|
# - Reduce #RRGGBB format to #RGB format when possible
|
||||||
# - rework command-line argument processing so that options are configurable
|
# - rework command-line argument processing so that options are configurable
|
||||||
# - remove unreferenced patterns? https://bugs.edge.launchpad.net/ubuntu/+source/human-icon-theme/+bug/361667/
|
# - remove unreferenced patterns? https://bugs.edge.launchpad.net/ubuntu/+source/human-icon-theme/+bug/361667/
|
||||||
|
|
||||||
|
# Some notes to not forget:
|
||||||
|
# - removing unreferenced IDs loses some semantic information
|
||||||
|
# - removing empty nested groups also potentially loses some semantic information
|
||||||
|
# (i.e. the following button:
|
||||||
|
# <g>
|
||||||
|
# <rect .../>
|
||||||
|
# <text .../>
|
||||||
|
# </g>
|
||||||
|
# will be flattened)
|
||||||
|
|
||||||
|
|
||||||
# necessary to get true division
|
# necessary to get true division
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
|
|
@ -255,7 +264,7 @@ def removeNamespacedAttributes(node, namespaces):
|
||||||
|
|
||||||
# now recurse for children
|
# now recurse for children
|
||||||
for child in node.childNodes:
|
for child in node.childNodes:
|
||||||
removeNamespacedAttributes(child, namespaces)
|
num += removeNamespacedAttributes(child, namespaces)
|
||||||
return num
|
return num
|
||||||
|
|
||||||
def removeNamespacedElements(node, namespaces):
|
def removeNamespacedElements(node, namespaces):
|
||||||
|
|
@ -275,10 +284,34 @@ def removeNamespacedElements(node, namespaces):
|
||||||
|
|
||||||
# now recurse for children
|
# now recurse for children
|
||||||
for child in node.childNodes:
|
for child in node.childNodes:
|
||||||
removeNamespacedElements(child, namespaces)
|
num += removeNamespacedElements(child, namespaces)
|
||||||
return num
|
return num
|
||||||
|
|
||||||
# TODO: create a class for a SVGLength type (including value and unit)
|
# this walks further and further down the tree, removing groups
|
||||||
|
# which do not have any attributes and promoting their children
|
||||||
|
# up one level
|
||||||
|
def removeNestedGroups(node):
|
||||||
|
global numElemsRemoved
|
||||||
|
num = 0
|
||||||
|
|
||||||
|
groupsToRemove = []
|
||||||
|
for child in node.childNodes:
|
||||||
|
if child.nodeName == 'g' and child.namespaceURI == NS['SVG'] and len(child.attributes) == 0:
|
||||||
|
groupsToRemove.append(child)
|
||||||
|
|
||||||
|
for g in groupsToRemove:
|
||||||
|
while g.childNodes.length > 0:
|
||||||
|
g.parentNode.insertBefore(g.firstChild, g)
|
||||||
|
g.parentNode.removeChild(g)
|
||||||
|
numElemsRemoved += 1
|
||||||
|
num += 1
|
||||||
|
|
||||||
|
# now recurse for children
|
||||||
|
for child in node.childNodes:
|
||||||
|
if child.nodeType == 1:
|
||||||
|
num += removeNestedGroups(child)
|
||||||
|
|
||||||
|
return num
|
||||||
|
|
||||||
coord = re.compile("\\-?\\d+\\.?\\d*")
|
coord = re.compile("\\-?\\d+\\.?\\d*")
|
||||||
scinumber = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+[eE][\\-\\+]?\\d+")
|
scinumber = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+[eE][\\-\\+]?\\d+")
|
||||||
|
|
@ -629,13 +662,17 @@ def scourString(in_string):
|
||||||
numElemsRemoved += 1
|
numElemsRemoved += 1
|
||||||
|
|
||||||
# remove unreferenced gradients/patterns outside of defs
|
# remove unreferenced gradients/patterns outside of defs
|
||||||
removeUnreferencedElements(doc)
|
while removeUnreferencedElements(doc) > 0:
|
||||||
|
pass
|
||||||
|
|
||||||
|
while removeNestedGroups(doc.documentElement) > 0:
|
||||||
|
pass
|
||||||
|
|
||||||
# clean path data
|
# clean path data
|
||||||
for elem in doc.documentElement.getElementsByTagNameNS(NS['SVG'], 'path') :
|
for elem in doc.documentElement.getElementsByTagNameNS(NS['SVG'], 'path') :
|
||||||
cleanPath(elem)
|
cleanPath(elem)
|
||||||
|
|
||||||
# convert rasters refereces to base64-encoded strings
|
# convert rasters references to base64-encoded strings
|
||||||
for elem in doc.documentElement.getElementsByTagNameNS(NS['SVG'], 'image') :
|
for elem in doc.documentElement.getElementsByTagNameNS(NS['SVG'], 'image') :
|
||||||
embedRasters(elem)
|
embedRasters(elem)
|
||||||
|
|
||||||
|
|
|
||||||
99
testscour.py
99
testscour.py
|
|
@ -21,6 +21,8 @@ import unittest
|
||||||
import scour
|
import scour
|
||||||
import xml.dom.minidom
|
import xml.dom.minidom
|
||||||
|
|
||||||
|
SVGNS = 'http://www.w3.org/2000/svg'
|
||||||
|
|
||||||
# I couldn't figure out how to get ElementTree to work with the following XPath
|
# I couldn't figure out how to get ElementTree to work with the following XPath
|
||||||
# "//*[namespace-uri()='http://example.com']"
|
# "//*[namespace-uri()='http://example.com']"
|
||||||
# so I decided to use minidom and this helper function that performs a test on a given node
|
# so I decided to use minidom and this helper function that performs a test on a given node
|
||||||
|
|
@ -95,6 +97,55 @@ class NoAdobeXPathElements(unittest.TestCase):
|
||||||
lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), False,
|
lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), False,
|
||||||
'Found Adobe XPath elements' )
|
'Found Adobe XPath elements' )
|
||||||
|
|
||||||
|
class DoNotRemoveMetadataWithOnlyText(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
doc = scour.scourXmlFile('unittests/metadata-with-text.svg')
|
||||||
|
self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1,
|
||||||
|
'Removed metadata element with only text child' )
|
||||||
|
|
||||||
|
class RemoveEmptyMetadataElement(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
doc = scour.scourXmlFile('unittests/empty-metadata.svg')
|
||||||
|
self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0,
|
||||||
|
'Did not remove empty metadata element' )
|
||||||
|
|
||||||
|
class RemoveEmptyGElements(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
doc = scour.scourXmlFile('unittests/empty-g.svg')
|
||||||
|
self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1,
|
||||||
|
'Did not remove empty g element' )
|
||||||
|
|
||||||
|
class RemoveUnreferencedPattern(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
doc = scour.scourXmlFile('unittests/unreferenced-pattern.svg')
|
||||||
|
self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0,
|
||||||
|
'Unreferenced pattern not removed' )
|
||||||
|
|
||||||
|
class RemoveUnreferencedLinearGradient(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
doc = scour.scourXmlFile('unittests/unreferenced-linearGradient.svg')
|
||||||
|
self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0,
|
||||||
|
'Unreferenced linearGradient not removed' )
|
||||||
|
|
||||||
|
class RemoveUnreferencedRadialGradient(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
doc = scour.scourXmlFile('unittests/unreferenced-radialGradient.svg')
|
||||||
|
self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'radialradient')), 0,
|
||||||
|
'Unreferenced radialGradient not removed' )
|
||||||
|
|
||||||
|
class RemoveUselessNestedGroups(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
doc = scour.scourXmlFile('unittests/nested-useless-groups.svg')
|
||||||
|
self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1,
|
||||||
|
'Useless nested groups not removed' )
|
||||||
|
|
||||||
|
# These tests will fail at present
|
||||||
|
class RemoveDuplicateGradientStops(unittest.TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
doc = scour.scourXmlFile('unittests/duplicate-gradient-stops.svg')
|
||||||
|
self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'stop')), 3,
|
||||||
|
'Duplicate gradient stops not removed' )
|
||||||
|
|
||||||
#class NoInkscapeAttributes(unittest.TestCase):
|
#class NoInkscapeAttributes(unittest.TestCase):
|
||||||
# def runTest(self):
|
# def runTest(self):
|
||||||
# self.assertNotEquals(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement,
|
# self.assertNotEquals(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement,
|
||||||
|
|
@ -102,54 +153,6 @@ class NoAdobeXPathElements(unittest.TestCase):
|
||||||
# False,
|
# False,
|
||||||
# 'Found Inkscape attributes')
|
# 'Found Inkscape attributes')
|
||||||
|
|
||||||
class DoNotRemoveMetadataWithOnlyText(unittest.TestCase):
|
|
||||||
def runTest(self):
|
|
||||||
doc = scour.scourXmlFile('unittests/metadata-with-text.svg')
|
|
||||||
self.assertEquals(len(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'metadata')), 1,
|
|
||||||
'Removed metadata element with only text child' )
|
|
||||||
|
|
||||||
class RemoveEmptyMetadataElement(unittest.TestCase):
|
|
||||||
def runTest(self):
|
|
||||||
doc = scour.scourXmlFile('unittests/empty-metadata.svg')
|
|
||||||
self.assertEquals(len(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'metadata')), 0,
|
|
||||||
'Did not remove empty metadata element' )
|
|
||||||
|
|
||||||
class RemoveEmptyGElements(unittest.TestCase):
|
|
||||||
def runTest(self):
|
|
||||||
doc = scour.scourXmlFile('unittests/empty-g.svg')
|
|
||||||
self.assertEquals(len(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'g')), 1,
|
|
||||||
'Did not remove empty g element' )
|
|
||||||
|
|
||||||
class RemoveUnreferencedPattern(unittest.TestCase):
|
|
||||||
def runTest(self):
|
|
||||||
doc = scour.scourXmlFile('unittests/unreferenced-pattern.svg')
|
|
||||||
self.assertEquals(len(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'pattern')), 0,
|
|
||||||
'Unreferenced pattern not removed' )
|
|
||||||
|
|
||||||
class RemoveUnreferencedLinearGradient(unittest.TestCase):
|
|
||||||
def runTest(self):
|
|
||||||
doc = scour.scourXmlFile('unittests/unreferenced-linearGradient.svg')
|
|
||||||
self.assertEquals(len(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'linearGradient')), 0,
|
|
||||||
'Unreferenced linearGradient not removed' )
|
|
||||||
|
|
||||||
class RemoveUnreferencedRadialGradient(unittest.TestCase):
|
|
||||||
def runTest(self):
|
|
||||||
doc = scour.scourXmlFile('unittests/unreferenced-radialGradient.svg')
|
|
||||||
self.assertEquals(len(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'radialradient')), 0,
|
|
||||||
'Unreferenced radialGradient not removed' )
|
|
||||||
|
|
||||||
# These tests will fail at present
|
|
||||||
class RemoveUselessNestedGroups(unittest.TestCase):
|
|
||||||
def runTest(self):
|
|
||||||
doc = scour.scourXmlFile('unittests/nested-useless-groups.svg')
|
|
||||||
self.assertEquals(len(doc.getElementsByTagNameNS('http://wwww.w3.org/2000/svg', 'g')), 1,
|
|
||||||
'Useless nested groups not removed' )
|
|
||||||
|
|
||||||
class RemoveDuplicateGradientStops(unittest.TestCase):
|
|
||||||
def runTest(self):
|
|
||||||
doc = scour.scourXmlFile('unittests/duplicate-gradient-stops.svg')
|
|
||||||
self.assertEquals(len(doc.getElementsByTagNameNS('http://www.w3.org/2000/svg', 'stop')), 3,
|
|
||||||
'Duplicate gradient stops not removed' )
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
<g/>
|
<g/>
|
||||||
<g>
|
<g transform="translate(10,10)">
|
||||||
<rect width="300" height="200" fill="green" />
|
<rect width="300" height="200" fill="green" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 118 B After Width: | Height: | Size: 147 B |
Loading…
Add table
Add a link
Reference in a new issue