=== modified file 'scour.py'
--- scour.py 2011-02-09 20:44:41 +0000
+++ scour.py 2011-02-11 00:44:46 +0000
@@ -276,7 +276,66 @@
'yellow': 'rgb(255, 255, 0)',
'yellowgreen': 'rgb(154, 205, 50)',
}
-
+
+default_attributes = { # excluded all attributes with 'auto' as default
+ # SVG 1.1 presentation attributes
+ 'baseline-shift': 'baseline',
+ 'clip-path': 'none',
+ 'clip-rule': 'nonzero',
+ 'color': '#000',
+ 'color-interpolation-filters': 'linearRGB',
+ 'color-interpolation': 'sRGB',
+ 'direction': 'ltr',
+ 'display': 'inline',
+ 'enable-background': 'accumulate',
+ 'fill': '#000',
+ 'fill-opacity': '1',
+ 'fill-rule': 'nonzero',
+ 'filter': 'none',
+ 'flood-color': '#000',
+ 'flood-opacity': '1',
+ 'font-size-adjust': 'none',
+ 'font-size': 'medium',
+ 'font-stretch': 'normal',
+ 'font-style': 'normal',
+ 'font-variant': 'normal',
+ 'font-weight': 'normal',
+ 'glyph-orientation-horizontal': '0deg',
+ 'letter-spacing': 'normal',
+ 'lighting-color': '#fff',
+ 'marker': 'none',
+ 'marker-start': 'none',
+ 'marker-mid': 'none',
+ 'marker-end': 'none',
+ 'mask': 'none',
+ 'opacity': '1',
+ 'pointer-events': 'visiblePainted',
+ 'stop-color': '#000',
+ 'stop-opacity': '1',
+ 'stroke': 'none',
+ 'stroke-dasharray': 'none',
+ 'stroke-dashoffset': '0',
+ 'stroke-linecap': 'butt',
+ 'stroke-linejoin': 'miter',
+ 'stroke-miterlimit': '4',
+ 'stroke-opacity': '1',
+ 'stroke-width': '1',
+ 'text-anchor': 'start',
+ 'text-decoration': 'none',
+ 'unicode-bidi': 'normal',
+ 'visibility': 'visible',
+ 'word-spacing': 'normal',
+ 'writing-mode': 'lr-tb',
+ # SVG 1.2 tiny properties
+ 'audio-level': '1',
+ 'solid-color': '#000',
+ 'solid-opacity': '1',
+ 'text-align': 'start',
+ 'vector-effect': 'none',
+ 'viewport-fill': 'none',
+ 'viewport-fill-opacity': '1',
+ }
+
def isSameSign(a,b): return (a <= 0 and b <= 0) or (a >= 0 and b >= 0)
scinumber = re.compile(r"[-+]?(\d*\.?)?\d+[eE][-+]?\d+")
@@ -1211,16 +1270,32 @@
num += 1
return num
-def repairStyle(node, options):
- num = 0
+def _getStyle(node):
+ u"""Returns the style attribute of a node as a dictionary."""
if node.nodeType == 1 and len(node.getAttribute('style')) > 0 :
- # get all style properties and stuff them into a dictionary
styleMap = { }
rawStyles = node.getAttribute('style').split(';')
for style in rawStyles:
propval = style.split(':')
if len(propval) == 2 :
styleMap[propval[0].strip()] = propval[1].strip()
+ return styleMap
+ else:
+ return {}
+
+def _setStyle(node, styleMap):
+ u"""Sets the style attribute of a node to the dictionary ``styleMap``."""
+ fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in styleMap.keys()])
+ if fixedStyle != '' :
+ node.setAttribute('style', fixedStyle)
+ elif node.getAttribute('style'):
+ node.removeAttribute('style')
+ return node
+
+def repairStyle(node, options):
+ num = 0
+ styleMap = _getStyle(node)
+ if styleMap:
# I've seen this enough to know that I need to correct it:
# fill: url(#linearGradient4918) rgb(0, 0, 0);
@@ -1235,13 +1310,8 @@
# opacity:1
if styleMap.has_key('opacity') :
opacity = float(styleMap['opacity'])
- # opacity='1.0' is useless, remove it
- if opacity == 1.0 :
- del styleMap['opacity']
- num += 1
-
# if opacity='0' then all fill and stroke properties are useless, remove them
- elif opacity == 0.0 :
+ if opacity == 0.0 :
for uselessStyle in ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-linejoin',
'stroke-opacity', 'stroke-miterlimit', 'stroke-linecap', 'stroke-dasharray',
'stroke-dashoffset', 'stroke-opacity'] :
@@ -1268,33 +1338,19 @@
del styleMap[fillstyle]
num += 1
- # stop-opacity: 1
- if styleMap.has_key('stop-opacity') :
- if float(styleMap['stop-opacity']) == 1.0 :
- del styleMap['stop-opacity']
- num += 1
-
- # fill-opacity: 1 or 0
+ # fill-opacity: 0
if styleMap.has_key('fill-opacity') :
fillOpacity = float(styleMap['fill-opacity'])
- # TODO: This is actually a problem if the parent element does not have fill-opacity=1
- if fillOpacity == 1.0 :
- del styleMap['fill-opacity']
- num += 1
- elif fillOpacity == 0.0 :
+ if fillOpacity == 0.0 :
for uselessFillStyle in [ 'fill', 'fill-rule' ] :
if styleMap.has_key(uselessFillStyle):
del styleMap[uselessFillStyle]
num += 1
- # stroke-opacity: 1 or 0
+ # stroke-opacity: 0
if styleMap.has_key('stroke-opacity') :
strokeOpacity = float(styleMap['stroke-opacity'])
- # TODO: This is actually a problem if the parent element does not have stroke-opacity=1
- if strokeOpacity == 1.0 :
- del styleMap['stroke-opacity']
- num += 1
- elif strokeOpacity == 0.0 :
+ if strokeOpacity == 0.0 :
for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap',
'stroke-dasharray', 'stroke-dashoffset' ] :
if styleMap.has_key(uselessStrokeStyle):
@@ -1331,18 +1387,6 @@
del styleMap[inkscapeStyle]
num += 1
- # visibility: visible
- if styleMap.has_key('visibility') :
- if styleMap['visibility'] == 'visible':
- del styleMap['visibility']
- num += 1
-
- # display: inline
- if styleMap.has_key('display') :
- if styleMap['display'] == 'inline':
- del styleMap['display']
- num += 1
-
if styleMap.has_key('overflow') :
# overflow specified on element other than svg, marker, pattern
if not node.nodeName in ['svg','marker','pattern']:
@@ -1361,12 +1405,6 @@
del styleMap['overflow']
num += 1
- # marker: none
- if styleMap.has_key('marker') :
- if styleMap['marker'] == 'none':
- del styleMap['marker']
- num += 1
-
# now if any of the properties match known SVG attributes we prefer attributes
# over style so emit them and remove them from the style map
if options.style_to_xml:
@@ -1374,15 +1412,9 @@
if propName in svgAttributes :
node.setAttribute(propName, styleMap[propName])
del styleMap[propName]
-
- # sew our remaining style properties back together into a style attribute
- fixedStyle = [prop + ':' + styleMap[prop] + ';' for prop in styleMap.keys()]
-
- if len(fixedStyle) > 0:
- node.setAttribute('style', "".join(fixedStyle))
- else:
- node.removeAttribute('style')
-
+
+ _setStyle(node, styleMap)
+
# recurse for our child elements
for child in node.childNodes :
num += repairStyle(child,options)
@@ -1431,7 +1463,21 @@
node.mayContainTextNodes = result
return result
-def removeDefaultAttributeValues(node, options):
+def taint(taintedSet, taintedAttribute):
+ u"""Adds an attribute to a set of attributes.
+
+ Related attributes are also included."""
+ taintedSet.add(taintedAttribute)
+ if taintedAttribute == 'marker':
+ taintedSet |= set(['marker-start', 'marker-mid', 'marker-end'])
+ if taintedAttribute in ['marker-start', 'marker-mid', 'marker-end']:
+ taintedSet.add('marker')
+ return taintedSet
+
+def removeDefaultAttributeValues(node, options, tainted=set()):
+ u"""'tainted' keeps a set of attributes defined in parent nodes.
+
+ For such attributes, we don't delete attributes with default values."""
num = 0
if node.nodeType != 1: return 0
@@ -1505,10 +1551,33 @@
if (r.value == 50 and r.units == Unit.PCT) or (r.value == 0.5 and r.units == Unit.NONE):
node.removeAttribute('r')
num += 1
+
+ # Summarily get rid of some more attributes
+ attributes = [node.attributes.item(i).nodeName
+ for i in range(node.attributes.length)]
+ for attribute in attributes:
+ if attribute not in tainted:
+ if attribute in default_attributes.keys():
+ if node.getAttribute(attribute) == default_attributes[attribute]:
+ node.removeAttribute(attribute)
+ num += 1
+ else:
+ tainted = taint(tainted, attribute)
+ # These attributes might also occur as styles
+ styles = _getStyle(node)
+ for attribute in styles.keys():
+ if attribute not in tainted:
+ if attribute in default_attributes.keys():
+ if styles[attribute] == default_attributes[attribute]:
+ del styles[attribute]
+ num += 1
+ else:
+ tainted = taint(tainted, attribute)
+ _setStyle(node, styles)
# recurse for our child elements
for child in node.childNodes :
- num += removeDefaultAttributeValues(child,options)
+ num += removeDefaultAttributeValues(child, options, tainted.copy())
return num
@@ -1563,6 +1632,7 @@
attrsToConvert = ['solid-color']
# now convert all the color formats
+ styles = _getStyle(element)
for attr in attrsToConvert:
oldColorValue = element.getAttribute(attr)
if oldColorValue != '':
@@ -1572,6 +1642,16 @@
if oldBytes > newBytes:
element.setAttribute(attr, newColorValue)
numBytes += (oldBytes - len(element.getAttribute(attr)))
+ # colors might also hide in styles
+ if attr in styles.keys():
+ oldColorValue = styles[attr]
+ newColorValue = convertColor(oldColorValue)
+ oldBytes = len(oldColorValue)
+ newBytes = len(newColorValue)
+ if oldBytes > newBytes:
+ styles[attr] = newColorValue
+ numBytes += (oldBytes - len(element.getAttribute(attr)))
+ _setStyle(element, styles)
# now recurse for our child elements
for child in element.childNodes :
@@ -2182,7 +2262,12 @@
"""
num = 0
- for lengthAttr in ['opacity', 'flood-opacity', 'fill-opacity', 'stroke-opacity', 'stop-opacity', 'stroke-miterlimit', 'stroke-dashoffset', 'letter-spacing', 'word-spacing', 'kerning', 'font-size-adjust', 'font-size', 'stroke-width']:
+ styles = _getStyle(element)
+ for lengthAttr in ['opacity', 'flood-opacity', 'fill-opacity',
+ 'stroke-opacity', 'stop-opacity', 'stroke-miterlimit',
+ 'stroke-dashoffset', 'letter-spacing', 'word-spacing',
+ 'kerning', 'font-size-adjust', 'font-size',
+ 'stroke-width']:
val = element.getAttribute(lengthAttr)
if val != '':
valLen = SVGLength(val)
@@ -2191,6 +2276,16 @@
if len(newVal) < len(val):
num += len(val) - len(newVal)
element.setAttribute(lengthAttr, newVal)
+ # repeat for attributes hidden in styles
+ if lengthAttr in styles.keys():
+ val = styles[lengthAttr]
+ valLen = SVGLength(val)
+ if valLen.units != Unit.INVALID:
+ newVal = scourLength(val)
+ if len(newVal) < len(val):
+ num += len(val) - len(newVal)
+ styles[lengthAttr] = newVal
+ _setStyle(element, styles)
for child in element.childNodes:
if child.nodeType == 1:
@@ -2769,9 +2864,6 @@
if options.shorten_ids:
numBytesSavedInIDs += shortenIDs(doc)
- # remove default values of attributes
- numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options)
-
# scour lengths (including coordinates)
for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', 'linearGradient', 'radialGradient', 'stop', 'filter']:
for elem in doc.getElementsByTagName(type):
@@ -2783,6 +2875,9 @@
# more length scouring in this function
numBytesSavedInLengths = reducePrecision(doc.documentElement)
+ # remove default values of attributes
+ numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options)
+
# reduce the length of transformation attributes
numBytesSavedInTransforms = optimizeTransforms(doc.documentElement, options)
=== modified file 'testscour.py'
--- testscour.py 2011-02-09 21:11:33 +0000
+++ testscour.py 2011-02-11 00:44:46 +0000
@@ -456,7 +456,7 @@
class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/fill-none.svg')
- self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'nonzero',
+ self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'evenodd',
'fill-rule property not converted to XML attribute' )
class CollapseSinglyReferencedGradients(unittest.TestCase):
@@ -1151,6 +1151,60 @@
self.assertEquals(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100l200 100z');
self.assertEquals(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200l100 100z');
+class DefaultsRemovalToplevel(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[1].getAttribute('fill-rule'), '',
+ 'Default attribute fill-rule:nonzero not removed');
+
+class DefaultsRemovalToplevelInverse(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[0].getAttribute('fill-rule'), 'evenodd',
+ 'Non-Default attribute fill-rule:evenodd removed');
+
+class DefaultsRemovalToplevelFormat(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[0].getAttribute('stroke-width'), '',
+ 'Default attribute stroke-width:1.00 not removed');
+
+class DefaultsRemovalInherited(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[3].getAttribute('fill-rule'), '',
+ 'Default attribute fill-rule:nonzero not removed in child');
+
+class DefaultsRemovalInheritedInverse(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[2].getAttribute('fill-rule'), 'evenodd',
+ 'Non-Default attribute fill-rule:evenodd removed in child');
+
+class DefaultsRemovalInheritedFormat(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[2].getAttribute('stroke-width'), '',
+ 'Default attribute stroke-width:1.00 not removed in child');
+
+class DefaultsRemovalOverwrite(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[5].getAttribute('fill-rule'), 'nonzero',
+ 'Default attribute removed, although it overwrites parent element');
+
+class DefaultsRemovalOverwriteMarker(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[4].getAttribute('marker-start'), 'none',
+ 'Default marker attribute removed, although it overwrites parent element');
+
+class DefaultsRemovalNonOverwrite(unittest.TestCase):
+ def runTest(self):
+ doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg')
+ self.assertEquals(doc.getElementsByTagName('path')[10].getAttribute('fill-rule'), '',
+ 'Default attribute not removed, although its parent used default');
+
# TODO: write tests for --enable-viewboxing
# TODO; write a test for embedding rasters
# TODO: write a test for --disable-embed-rasters
=== added file 'unittests/cascading-default-attribute-removal.svg'
--- unittests/cascading-default-attribute-removal.svg 1970-01-01 00:00:00 +0000
+++ unittests/cascading-default-attribute-removal.svg 2011-02-10 17:45:43 +0000
@@ -0,0 +1,23 @@
+
+
\ No newline at end of file
=== modified file 'unittests/fill-none.svg'
--- unittests/fill-none.svg 2010-06-16 00:58:57 +0000
+++ unittests/fill-none.svg 2011-02-10 17:39:07 +0000
@@ -1,5 +1,5 @@
\ No newline at end of file