diff --git a/crunch.sh b/crunch.sh new file mode 100755 index 0000000..5f053b2 --- /dev/null +++ b/crunch.sh @@ -0,0 +1,7 @@ +#!/bin/bash +mkdir $1 +for FILE in `ls fulltests` +do + ./scour.py -i fulltests/$FILE -o $1/$FILE >> $1/report.txt +done + \ No newline at end of file diff --git a/fulltests/acid.svg b/fulltests/acid.svg new file mode 100644 index 0000000..5b12c90 --- /dev/null +++ b/fulltests/acid.svg @@ -0,0 +1,58 @@ + + + + + + + + + image/svg+xml + + Sign corrosive + 11/09/2006 + + + h0us3s + + + es + + + Inkscape + Sign Hazard + Warning + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/package.sh b/package.sh index ac24f64..e417883 100755 --- a/package.sh +++ b/package.sh @@ -1,6 +1,6 @@ #!/bin/bash SCOURVER="0.10" cd .. -tar cvf scour/tarballs/scour-$SCOURVER.tar scour/scour.py scour/LICENSE scour/NOTICE scour/README.txt scour/release-notes.html +tar cvf scour/tarballs/scour-$SCOURVER.tar scour/scour.py scour/svg_regex.py scour/LICENSE scour/NOTICE scour/README.txt scour/release-notes.html gzip scour/tarballs/scour-$SCOURVER.tar cd scour diff --git a/release-notes.html b/release-notes.html index e86aedc..6d0cf0d 100644 --- a/release-notes.html +++ b/release-notes.html @@ -20,6 +20,10 @@
  • Convert from absolute to relative path data
  • Remove trailing zeroes from path data
  • Limit to no more than 6 digits of precision
  • +
  • Remove empty line segments
  • +
  • Convert lines to horiz/vertical line segments where possible
  • +
  • Remove some more default styles (display:none, visibility:visible, overflow:visible, + marker:none)
  • diff --git a/scour.py b/scour.py index d1357c2..38492ab 100755 --- a/scour.py +++ b/scour.py @@ -45,8 +45,8 @@ # * Put id attributes first in the serialization (or make the d attribute last) # Next Up: +# - implement command-line option to output svgz # - Remove unnecessary units of precision on attributes (use decimal: http://docs.python.org/library/decimal.html) -# - 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/ @@ -96,6 +96,7 @@ unwanted_ns = [ NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], svgAttributes = [ 'clip-rule', + 'display', 'fill', 'fill-opacity', 'fill-rule', @@ -107,7 +108,9 @@ svgAttributes = [ 'font-variant', 'font-weight', 'line-height', + 'marker', 'opacity', + 'overflow', 'stop-color', 'stop-opacity', 'stroke', @@ -117,6 +120,7 @@ svgAttributes = [ 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'visibility' ] def findElementById(node, id): @@ -187,6 +191,7 @@ numIDsRemoved = 0 numElemsRemoved = 0 numAttrsRemoved = 0 numRastersEmbedded = 0 +numPathSegmentsReduced = 0 numBytesSavedInPathData = 0 # removes all unreferenced elements except for , , , , and @@ -583,6 +588,30 @@ def repairStyle(node): # TODO: what else? + # 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 + + # overflow: visible or overflow specified on element other than svg, marker, pattern + if styleMap.has_key('overflow') : + if styleMap['overflow'] == 'visible' or node.nodeName in ['svg','marker','pattern']: + 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 for propName in styleMap.keys() : @@ -609,6 +638,7 @@ def repairStyle(node): # - parse the path data and reserialize def cleanPath(element) : global numBytesSavedInPathData + global numPathSegmentsReduced # this gets the parser object from svg_regex.py oldPathStr = element.getAttribute('d') @@ -616,24 +646,37 @@ def cleanPath(element) : # however, this parser object has some ugliness in it (lists of tuples of tuples of # numbers and booleans). we just need a list of (cmd,[numbers]): + # TODO: remove empty path segments path = [] for (cmd,dataset) in pathObj: if cmd in ['M','m','L','l','T','t']: # one or more tuples, each containing two numbers nums = [] for t in dataset: - # convert to a Decimal and ensure precision - nums.append(Decimal(str(t[0])) * Decimal(1)) - nums.append(Decimal(str(t[1])) * Decimal(1)) - path.append( (cmd, nums) ) + # only create this coord pair if it is non-zero or is an absolute Move (first cmd) + if cmd == 'M' or (t[0] != 0 or t[1] != 0): + # convert to a Decimal and ensure precision + nums.append(Decimal(str(t[0])) * Decimal(1)) + nums.append(Decimal(str(t[1])) * Decimal(1)) + else: + numPathSegmentsReduced += 1 + + # only create this segment if it is not empty + if nums: + path.append( (cmd, nums) ) elif cmd in ['V','v','H','h']: # one or more numbers nums = [] for n in dataset: - nums.append(Decimal(str(n))) - path.append( (cmd, nums) ) + if n != 0: + nums.append(Decimal(str(n))) + else: + numPathSegmentsRemoved += 1 + if nums: + path.append( (cmd, nums) ) + # TODO: remove empty curve segments elif cmd in ['C','c']: # one or more tuples, each containing three tuples of two numbers each nums = [] @@ -643,6 +686,7 @@ def cleanPath(element) : nums.append(Decimal(str(pair[1])) * Decimal(1)) path.append( (cmd, nums) ) + # TODO: remove empty curve segments elif cmd in ['S','s','Q','q']: # one or more tuples, each containing two tuples of two numbers each nums = [] @@ -652,6 +696,7 @@ def cleanPath(element) : nums.append(Decimal(str(pair[1])) * Decimal(1)) path.append( (cmd, nums) ) + # TODO: remove empty curve segments elif cmd in ['A','a']: # one or more tuples, each containing a tuple of two numbers, a number, a boolean, # another boolean, and a tuple of two numbers @@ -674,12 +719,9 @@ def cleanPath(element) : elif cmd in ['Z','z']: path.append( (cmd, []) ) - # TODO: convert to fixed point values - # convert absolute coordinates into relative ones (start with the second subcommand # and leave the first M as absolute) (x,y) = path[0][1] - i = 1 for (cmd,data) in path[1:]: # adjust abs to rel @@ -738,8 +780,36 @@ def cleanPath(element) : x += data[k+2] y += data[k+3] k += 4 - - # TODO: collapse adjacent H or V segments that have coords in the same direction + + # collapse adjacent H or V segments that have coords in the same direction + newPath = [path[0]] + for (cmd,data) in path[1:]: + if cmd == 'l': + i = 0 + lineTuples = [] + while i < len(data): + if data[i] == 0: + # vertical + if lineTuples: + # change the line command, then append the v and then the remaining line coords + if lineTuples: newPath.append( ('l', lineTuples) ) + lineTuples = [] + newPath.append( ('v', [data[i+1]]) ) + numPathSegmentsReduced += 1 + elif data[i+1] == 0: + if lineTuples: + # change the line command, then append the h and then the remaining line coords + if lineTuples: newPath.append( ('l', lineTuples) ) + lineTuples = [] + newPath.append( ('h', [data[i]]) ) + numPathSegmentsReduced += 1 + else: + lineTuples.append(data[i]) + lineTuples.append(data[i+1]) + i += 2 + else: + newPath.append( (cmd, data) ) + path = newPath newPathStr = serializePath(path) numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) ) @@ -1051,6 +1121,7 @@ if __name__ == '__main__': print ' Number of attributes removed:', numAttrsRemoved print ' Number of style properties fixed:', numStylePropsFixed print ' Number of raster images embedded inline:', numRastersEmbedded + print ' Number of path segments reduced/removed:', numPathSegmentsReduced print ' Number of bytes saved in path data:', numBytesSavedInPathData oldsize = os.path.getsize(input.name) newsize = os.path.getsize(output.name) diff --git a/testscour.py b/testscour.py index cf2667d..d582edd 100755 --- a/testscour.py +++ b/testscour.py @@ -459,6 +459,32 @@ class LimitPrecisionInPathData(unittest.TestCase): path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) self.assertEquals(path[1][1][0], 100.001, 'Not correctly limiting precision on path data' ) - + +class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEquals(path[4][0], 'z', + 'Did not remove an empty line segment from path' ) + +class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEquals(path[1][0], 'h', + 'Did not change line to horizontal line segment in path' ) + self.assertEquals(path[1][1][0], 200.0, + 'Did not calculate horizontal line segment in path correctly' ) + +class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase): + def runTest(self): + doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEquals(path[2][0], 'v', + 'Did not change line to vertical line segment in path' ) + self.assertEquals(path[2][1][0], 100.0, + 'Did not calculate vertical line segment in path correctly' ) + + if __name__ == '__main__': unittest.main() diff --git a/unittests/path-line-optimize.svg b/unittests/path-line-optimize.svg new file mode 100644 index 0000000..30e93f8 --- /dev/null +++ b/unittests/path-line-optimize.svg @@ -0,0 +1,3 @@ + + +