diff --git a/scour.py b/scour.py index 3dcb6e0..577fc74 100755 --- a/scour.py +++ b/scour.py @@ -28,8 +28,6 @@ # TODO: Adapt this script into an Inkscape python plugin # # * Specify a limit to the precision of all positional elements. -# * Clean up XML Elements -# * Collapse multiple redundent groups # * Clean up Definitions # * Remove duplicate gradient stops # * Collapse duplicate gradient definitions @@ -48,12 +46,23 @@ # * Collapse all group based transformations # Next Up: -# - Remove unnecessary nested elements -# - Remove duplicate gradient stops +# + Remove unnecessary nested elements +# - Remove duplicate gradient stops (same offset, stop-color, stop-opacity) # - Convert all colors to #RRGGBB format +# - Reduce #RRGGBB format to #RGB format when possible # - 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/ +# 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: +# +# +# +# +# will be flattened) + # necessary to get true division from __future__ import division @@ -255,7 +264,7 @@ def removeNamespacedAttributes(node, namespaces): # now recurse for children for child in node.childNodes: - removeNamespacedAttributes(child, namespaces) + num += removeNamespacedAttributes(child, namespaces) return num def removeNamespacedElements(node, namespaces): @@ -275,10 +284,34 @@ def removeNamespacedElements(node, namespaces): # now recurse for children for child in node.childNodes: - removeNamespacedElements(child, namespaces) + num += removeNamespacedElements(child, namespaces) 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*") scinumber = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+[eE][\\-\\+]?\\d+") @@ -629,13 +662,17 @@ def scourString(in_string): numElemsRemoved += 1 # remove unreferenced gradients/patterns outside of defs - removeUnreferencedElements(doc) + while removeUnreferencedElements(doc) > 0: + pass + while removeNestedGroups(doc.documentElement) > 0: + pass + # clean path data for elem in doc.documentElement.getElementsByTagNameNS(NS['SVG'], 'path') : 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') : embedRasters(elem) diff --git a/testscour.py b/testscour.py index 37ba143..2b7cef4 100755 --- a/testscour.py +++ b/testscour.py @@ -21,6 +21,8 @@ import unittest import scour 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 # "//*[namespace-uri()='http://example.com']" # 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, '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): # def runTest(self): # self.assertNotEquals(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement, @@ -102,54 +153,6 @@ class NoAdobeXPathElements(unittest.TestCase): # False, # '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__': unittest.main() diff --git a/unittests/empty-g.svg b/unittests/empty-g.svg index fe8fc36..e99aa7f 100644 --- a/unittests/empty-g.svg +++ b/unittests/empty-g.svg @@ -1,6 +1,6 @@ - + \ No newline at end of file