diff --git a/scour.py b/scour.py
index adab447..6ae23b6 100755
--- a/scour.py
+++ b/scour.py
@@ -49,6 +49,9 @@
# Next Up:
# + fix bug when removing stroke styles
+# + Remove gradients that are only referenced by one other gradient
+# - Remove unnecessary units of precision on attributes
+# - Remove unnecessary units of precision on path coordinates
# - Convert all colors to #RRGGBB format
# - Reduce #RRGGBB format to #RGB format when possible
# https://bugs.edge.launchpad.net/ubuntu/+source/human-icon-theme/+bug/361667/
@@ -147,8 +150,8 @@ def findElementsWithId(node,elems={}):
findElementsWithId(child, elems)
return elems
-# returns the number of times an id is referenced
-# currently looks at fill, stroke and xlink:href attributes
+# returns the number of times an id is referenced as well as all elements that reference it
+# currently looks at fill, stroke, clip-path, mask, marker and xlink:href attributes
def findReferencedElements(node,ids={}):
# TODO: error here (ids is not cleared upon next invocation), the
# input argument ids is clunky here (see below how it is called)
@@ -159,9 +162,10 @@ def findReferencedElements(node,ids={}):
# we remove the hash mark from the beginning of the id
id = href[1:]
if ids.has_key(id) :
- ids[id] += 1
+ ids[id][0] += 1
+ ids[id][1].append(node)
else:
- ids[id] = 1
+ ids[id] = [1,[node]]
# now get all style properties and the fill, stroke, filter attributes
styles = string.split(node.getAttribute('style'),';')
@@ -178,9 +182,10 @@ def findReferencedElements(node,ids={}):
if prop in referencingProps and val != '' and val[0:5] == 'url(#' :
id = val[5:val.find(')')]
if ids.has_key(id) :
- ids[id] += 1
+ ids[id][0] += 1
+ ids[id][1].append(node)
else:
- ids[id] = 1
+ ids[id] = [1,[node]]
if node.hasChildNodes() :
for child in node.childNodes:
@@ -340,6 +345,61 @@ def removeDuplicateGradientStops(doc):
# linear gradients
return num
+def collapseSinglyReferencedGradients(doc):
+ global numElemsRemoved
+ num = 0
+
+ # make sure to reset the ref'ed ids for when we are running this in testscour
+ for rid,nodeCount in findReferencedElements(doc.documentElement, {}).iteritems():
+ count = nodeCount[0]
+ nodes = nodeCount[1]
+ if count == 1:
+ elem = findElementById(doc.documentElement,rid)
+ if elem != None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \
+ and elem.namespaceURI == NS['SVG']:
+ # found a gradient that is referenced by only 1 other element
+ refElem = nodes[0]
+ if refElem.nodeType == 1 and refElem.nodeName in ['linearGradient', 'radialGradient'] \
+ and refElem.namespaceURI == NS['SVG']:
+ # elem is a gradient referenced by only one other gradient (refElem)
+ # TODO: update elem with properties and stops from refElem
+
+ # add the stops to the referencing gradient (this removes them from elem)
+ if len(refElem.getElementsByTagNameNS(NS['SVG'], 'stop')) == 0:
+ stopsToAdd = elem.getElementsByTagNameNS(NS['SVG'], 'stop')
+ for stop in stopsToAdd:
+ refElem.appendChild(stop)
+
+ # adopt the gradientUnits, spreadMethod, gradientTransform attributess if
+ # they are unspecified on refElem
+ for attr in ['gradientUnits','spreadMethod','gradientTransform']:
+ if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '':
+ refElem.setAttributeNS(None, attr, elem.getAttribute(attr))
+
+ # if both are radialGradients, adopt elem's fx,fy,cx,cy,r attributes if
+ # they are unspecified on refElem
+ if elem.nodeName == 'radialGradient' and refElem.nodeName == 'radialGradient':
+ for attr in ['fx','fy','cx','cy','r']:
+ if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '':
+ refElem.setAttributeNS(None, attr, elem.getAttribute(attr))
+
+ # if both are linearGradients, adopt elem's x1,y1,x2,y2 attributes if
+ # they are unspecified on refElem
+ if elem.nodeName == 'linearGradient' and refElem.nodeName == 'linearGradient':
+ for attr in ['x1','y1','x2','y2']:
+ if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '':
+ refElem.setAttributeNS(None, attr, elem.getAttribute(attr))
+
+ # now remove the xlink:href from refElem
+ refElem.removeAttributeNS(NS['XLINK'], 'href')
+
+ # now delete elem
+ elem.parentNode.removeChild(elem)
+ numElemsRemoved += 1
+ num += 1
+
+ return num
+
coord = re.compile("\\-?\\d+\\.?\\d*")
scinumber = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+[eE][\\-\\+]?\\d+")
number = re.compile("[\\-\\+]?(\\d*\\.?)?\\d+")
@@ -703,6 +763,10 @@ def scourString(in_string, options=[]):
while removeDuplicateGradientStops(doc) > 0:
pass
+ # remove gradients that are only referenced by one other gradient
+ while collapseSinglyReferencedGradients(doc) > 0:
+ pass
+
# clean path data
for elem in doc.documentElement.getElementsByTagNameNS(NS['SVG'], 'path') :
cleanPath(elem)
@@ -732,7 +796,9 @@ def scourString(in_string, options=[]):
# returns the minidom doc representation of the SVG
def scourXmlFile(filename, options=[]):
in_string = open(filename).read()
+# print 'IN=',in_string
out_string = scourString(in_string, options)
+# print 'OUT=',out_string
return xml.dom.minidom.parseString(out_string)
def printHeader():
diff --git a/testscour.py b/testscour.py
index 44cc0e4..040687e 100755
--- a/testscour.py
+++ b/testscour.py
@@ -370,7 +370,34 @@ class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase):
self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'nonzero',
'fill-rule property not converted to XML attribute' )
-
+class CollapseSinglyReferencedGradients(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/collapse-gradients.svg')
+ self.assertEquals(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0,
+ 'Singly-referenced linear gradient not collapsed' )
+
+class InheritGradientUnitsUponCollapsing(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/collapse-gradients.svg')
+# print doc.toprettyxml(' ')
+ self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'),
+ 'userSpaceOnUse',
+ 'gradientUnits not properly inherited when collapsing gradients' )
+
+class OverrideGradientUnitsUponCollapsing(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/collapse-gradients-gradientUnits.svg')
+ self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'),
+ 'objectBoundingBox',
+ 'gradientUnits not properly overrode when collapsing gradients' )
+
+class DoNotCollapseMultiplyReferencedGradients(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/dont-collapse-gradients.svg')
+ self.assertNotEquals(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0,
+ 'Multiply-referenced linear gradient collapsed' )
+
+
#class RemoveUnreferencedFonts(unittest.TestCase):
# def runTest(self):
# doc = scour.scourXmlFile('unittests/unreferenced-font.svg')
diff --git a/unittests/collapse-gradients-gradientUnits.svg b/unittests/collapse-gradients-gradientUnits.svg
new file mode 100644
index 0000000..2039db9
--- /dev/null
+++ b/unittests/collapse-gradients-gradientUnits.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/unittests/collapse-gradients.svg b/unittests/collapse-gradients.svg
new file mode 100644
index 0000000..ba6d433
--- /dev/null
+++ b/unittests/collapse-gradients.svg
@@ -0,0 +1,10 @@
+
\ No newline at end of file
diff --git a/unittests/dont-collapse-gradients.svg b/unittests/dont-collapse-gradients.svg
new file mode 100644
index 0000000..0152e29
--- /dev/null
+++ b/unittests/dont-collapse-gradients.svg
@@ -0,0 +1,12 @@
+
\ No newline at end of file