Apply the patches submitted by Jan Thor in bug 714731. These patches remove attributes whose value equals its default value per the SVG specification, except if an element's parent node defines a non-default value explicitly, and add unit tests.

This commit is contained in:
Louis Simard 2011-02-10 19:48:53 -05:00
parent 2b68c7ed37
commit bdae2bafae
4 changed files with 236 additions and 64 deletions

217
scour.py
View file

@ -276,7 +276,66 @@ colors = {
'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 @@ def removeDuplicateGradients(doc):
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 @@ def repairStyle(node, options):
# 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 @@ def repairStyle(node, options):
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 @@ def repairStyle(node, options):
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 @@ def repairStyle(node, options):
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 @@ def repairStyle(node, options):
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 @@ def mayContainTextNodes(node):
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 @@ def removeDefaultAttributeValues(node, options):
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 @@ def convertColors(element) :
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 @@ def convertColors(element) :
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 @@ def reducePrecision(element) :
"""
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 @@ def reducePrecision(element) :
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 @@ def scourString(in_string, options=None):
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 @@ def scourString(in_string, options=None):
# 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)

View file

@ -456,7 +456,7 @@ class ConvertFillOpacityPropertyToAttr(unittest.TestCase):
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 @@ class PathEmptyMove(unittest.TestCase):
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

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path style="fill-rule:evenodd;stroke-linecap:butt;stroke-width:1.00;stroke:#000" d="m1,1z"/>
<path style="fill-rule:nonzero;stroke-linecap:butt;stroke:#000" d="m1,1z"/>
<g style="stroke:#f00;marker:none">
<path style="marker-start:none;fill-rule:evenodd;stroke-linecap:butt" d="m1,1z"/>
<path style="fill-rule:nonzero" d="m1,1z"/>
<g style="fill:#f0f;text-anchor:stop;fill-rule:evenodd;stroke-linecap:round;marker:url(#nirvana)">
<path style="marker-start:none;fill-rule:evenodd;stroke-linecap:butt" d="m1,1z"/>
<path style="color:#000;fill-rule:nonzero;" d="m1,1z"/>
<path d="m1,1z"/>
</g>
<g style="fill:#f0f;text-anchor:stop;fill-rule:evenodd;stroke-linecap:round;marker:url(#nirvana)">
<path style="marker-start:none;fill-rule:evenodd;stroke-linecap:butt" d="m1,1z"/>
<path style="color:#000;fill-rule:nonzero;" d="m1,1z"/>
</g>
<g style="text-anchor:stop;fill-rule:nonzero;marker:none;stroke-linecap:butt">
<path style="marker-start:none;fill-rule:evenodd;stroke-linecap:butt" d="m1,1z"/>
<path style="fill-rule:nonzero;" d="m1,1z"/>
<path d="m1,1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg">
<path style="fill: none; fill-rule: evenodd; fill-opacity: 0.5;" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" />
<path style="fill: black; fill-rule: nonzero; fill-opacity: 0.5;" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" />
<path style="fill: none; fill-rule: nonzero; fill-opacity: 0.5;" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" />
<path style="fill: black; fill-rule: evenodd; fill-opacity: 0.5;" d="M 7.7592046,36.982095 C 7.8831049,40.873696 7.8339808,45.305308 7.8339808,49.436888 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 429 B

After

Width:  |  Height:  |  Size: 429 B

Before After
Before After