Merge branch lp:~louis-simard/scour/rework. Summary of changes:

scour.py, scour.inkscape.py, scour.inx:
 * Add options --quiet, --enable-comment-stripping, --shorten-ids, --remove-metadata, --renderer-workaround.
scour.py:
 * Optimisations in time (so Scour runs faster) and space (so Scour allocates less memory, less often).
 * Change #E+# to #e#, #E-# to #e-#, 0.# to .# and -0.# into -.# in path/polygon/polyline data + lengths, if renderer workarounds are disabled. Use spaces instead of commas in path/polygon/polyline data. Use lower-case #rrggbb and #rgb instead of upper-case. All of this makes gzip work better, since the rest of SVG documents mostly has lower-case letters in tag names and spaces to separate XML attributes etc.
 * Fix a bug whereby an SVG document would become filled with black if all elements had the same fill color.
 * Fix a bug whereby a path's second command would not start at the right coordinates if the first command was a relative moveto 'm' with at least 1 implied lineto.
 * Fix a bug whereby a path's absolute lineto 'L' commands would not become the right relative lineto 'l' commands.
 * Allow the implicit linetos after a path's moveto 'M'/'m' to be converted into relative horizontal linetos 'h' and vertical 'v' too.
scour.inx:
 * Fix help typos. Make options more descriptive in the plugin option window. Add something about enable-group-collapsing requiring enable-id-stripping.
testscour.py:
 * Rework tests that relied on #E+#, #E-#, 0.# and -0.# so that they accept the changes to scour.py. Add unit tests for strip-xml-prolog, enable-comment-stripping and remove-metadata.
This commit is contained in:
Cynthia Gauthier 2010-06-15 20:58:57 -04:00
parent 00804fb833
commit f4cca44faf
9 changed files with 1153 additions and 518 deletions

View file

@ -22,12 +22,18 @@ class ScourInkscape (inkex.Effect):
self.OptionParser.add_option("--enable-id-stripping", type="inkbool",
action="store", dest="strip_ids", default=False,
help="remove all un-referenced ID attributes")
self.OptionParser.add_option("--shorten-ids", type="inkbool",
action="store", dest="shorten_ids", default=False,
help="shorten all ID attributes to the least number of letters possible")
self.OptionParser.add_option("--embed-rasters", type="inkbool",
action="store", dest="embed_rasters", default=True,
help="won't embed rasters as base64-encoded data")
self.OptionParser.add_option("--keep-editor-data", type="inkbool",
action="store", dest="keep_editor_data", default=False,
help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes")
self.OptionParser.add_option("--remove-metadata", type="inkbool",
action="store", dest="remove_metadata", default=False,
help="remove <metadata> elements (which may contain license metadata etc.)")
self.OptionParser.add_option("--strip-xml-prolog", type="inkbool",
action="store", dest="strip_xml_prolog", default=False,
help="won't output the <?xml ?> prolog")
@ -40,7 +46,12 @@ class ScourInkscape (inkex.Effect):
self.OptionParser.add_option("--enable-viewboxing", type="inkbool",
action="store", dest="enable_viewboxing", default=False,
help="changes document width/height to 100%/100% and creates viewbox coordinates")
self.OptionParser.add_option("--enable-comment-stripping", type="inkbool",
action="store", dest="strip_comments", default=False,
help="remove all <!-- --> comments")
self.OptionParser.add_option("--renderer-workaround", type="inkbool",
action="store", dest="renderer_workaround", default=False,
help="work around various renderer bugs (currently only librsvg)")
def effect(self):
input = file(sys.argv[12], "r")

View file

@ -7,16 +7,20 @@
<dependency type="executable" location="extensions">yocto_css.py</dependency>
<param name="tab" type="notebook">
<page name="Options" _gui-text="Options">
<param name="simplify-colors" type="boolean" _gui-text="Simplify colors">true</param>
<param name="style-to-xml" type="boolean" _gui-text="Style to xml">true</param>
<param name="simplify-colors" type="boolean" _gui-text="Shorten color values">true</param>
<param name="style-to-xml" type="boolean" _gui-text="Convert CSS attributes to XML attributes">true</param>
<param name="group-collapsing" type="boolean" _gui-text="Group collapsing">true</param>
<param name="enable-id-stripping" type="boolean" _gui-text="Enable id stripping">false</param>
<param name="enable-id-stripping" type="boolean" _gui-text="Remove unused ID names for elements">false</param>
<param name="shorten-ids" type="boolean" _gui-text="Shorten IDs">false</param>
<param name="embed-rasters" type="boolean" _gui-text="Embed rasters">true</param>
<param name="keep-editor-data" type="boolean" _gui-text="Keep editor data">false</param>
<param name="remove-metadata" type="boolean" _gui-text="Remove metadata">false</param>
<param name="enable-comment-stripping" type="boolean" _gui-text="Remove comments">false</param>
<param name="renderer-workaround" type="boolean" _gui-text="Work around renderer bugs">false</param>
<param name="enable-viewboxing" type="boolean" _gui-text="Enable viewboxing">false</param>
<param name="strip-xml-prolog" type="boolean" _gui-text="Strip xml prolog">false</param>
<param name="set-precision" type="int" _gui-text="Set precision">5</param>
<param name="indent" type="enum" _gui-text="Indent">
<param name="strip-xml-prolog" type="boolean" _gui-text="Remove the <?xml?> declaration">false</param>
<param name="set-precision" type="int" _gui-text="Number of significant digits for coords">5</param>
<param name="indent" type="enum" _gui-text="XML indentation (pretty-printing)">
<_item value="space">Space</_item>
<_item value="tab">Tab</_item>
<_item value="none">None</_item>
@ -24,16 +28,19 @@
</page>
<page name="Help" _gui-text="Help">
<_param name="instructions" type="description" xml:space="preserve">This extension optimizes the SVG file according to the following options:
* Simplify colors: convert all colors to #RRGGBB format.
* Style to xml: convert styles into XML attributes.
* Group collapsing: collapse group elements.
* Enable id stripping: remove all un-referenced ID attributes.
* Embed rasters: embed rasters as base64-encoded data.
* Shorten color names: convert all colors to #RRGGBB or #RGB format.
* Convert CSS attributes to XML attributes: convert styles from <style> tags and inline style="" declarations into XML attributes.
* Group collapsing: removes useless <g> elements, promoting their contents up one level. Requires "Remove unused ID names for elements" to be set.
* Remove unused ID names for elements: remove all unreferenced ID attributes.
* Shorten IDs: reduce the length of all ID attributes, assigning the shortest to the most-referenced elements. For instance, #linearGradient5621, referenced 100 times, can become #a.
* Embed rasters: embed raster images as base64-encoded data URLs.
* Keep editor data: don't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes.
* Enable viewboxing: size image to 100%/100% and introduce a viewBox
* Strip xml prolog: don't output the xml prolog.
* Set precision: set number of significant digits (default: 5).
* Indent: indentation of the output: none, space, tab (default: space).</_param>
* Remove metadata: remove &lt;metadata&gt; tags along with all the information in them, which may include license metadata, alternate versions for non-SVG-enabled browsers, etc.
* Remove comments: remove &lt;!-- --&gt; tags.
* Work around renderer bugs: emits slightly larger SVG data, but works around a bug in librsvg's renderer, which is used in Eye of GNOME and other various applications.
* Enable viewboxing: size image to 100%/100% and introduce a viewBox.
* Number of significant digits for coords: all coordinates are output with that number of significant digits. For example, if 3 is specified, the coordinate 3.5153 is output as 3.51 and the coordinate 471.55 is output as 472.
* XML indentation (pretty-printing): either None for no indentation, Space to use one space per nesting level, or Tab to use one tab per nesting level.</_param>
</page>
</param>
<output>

1154
scour.py

File diff suppressed because it is too large Load diff

View file

@ -43,6 +43,7 @@ Out[5]: [('M', [(100.0, -200.0)])]
"""
import re
from decimal import *
# Sentinel.
@ -52,8 +53,8 @@ class _EOF(object):
EOF = _EOF()
lexicon = [
('float', r'[-\+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-\+]?[0-9]+)?'),
('int', r'[-\+]?[0-9]+'),
('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'),
('int', r'[-+]?[0-9]+'),
('command', r'[AaCcHhLlMmQqSsTtVvZz]'),
]
@ -161,7 +162,7 @@ class SVGPathParser(object):
def rule_closepath(self, next, token):
command = token[1]
token = next()
return (command, None), token
return (command, []), token
def rule_moveto_or_lineto(self, next, token):
command = token[1]
@ -169,7 +170,7 @@ class SVGPathParser(object):
coordinates = []
while token[0] in self.number_tokens:
pair, token = self.rule_coordinate_pair(next, token)
coordinates.append(pair)
coordinates.extend(pair)
return (command, coordinates), token
def rule_orthogonal_lineto(self, next, token):
@ -189,7 +190,9 @@ class SVGPathParser(object):
pair1, token = self.rule_coordinate_pair(next, token)
pair2, token = self.rule_coordinate_pair(next, token)
pair3, token = self.rule_coordinate_pair(next, token)
coordinates.append((pair1, pair2, pair3))
coordinates.extend(pair1)
coordinates.extend(pair2)
coordinates.extend(pair3)
return (command, coordinates), token
def rule_curveto2(self, next, token):
@ -199,7 +202,8 @@ class SVGPathParser(object):
while token[0] in self.number_tokens:
pair1, token = self.rule_coordinate_pair(next, token)
pair2, token = self.rule_coordinate_pair(next, token)
coordinates.append((pair1, pair2))
coordinates.extend(pair1)
coordinates.extend(pair2)
return (command, coordinates), token
def rule_curveto1(self, next, token):
@ -208,7 +212,7 @@ class SVGPathParser(object):
coordinates = []
while token[0] in self.number_tokens:
pair1, token = self.rule_coordinate_pair(next, token)
coordinates.append(pair1)
coordinates.extend(pair1)
return (command, coordinates), token
def rule_elliptical_arc(self, next, token):
@ -216,51 +220,51 @@ class SVGPathParser(object):
token = next()
arguments = []
while token[0] in self.number_tokens:
rx = float(token[1])
rx = Decimal(token[1]) * 1
if rx < 0.0:
raise SyntaxError("expecting a nonnegative number; got %r" % (token,))
token = next()
if token[0] not in self.number_tokens:
raise SyntaxError("expecting a number; got %r" % (token,))
ry = float(token[1])
ry = Decimal(token[1]) * 1
if ry < 0.0:
raise SyntaxError("expecting a nonnegative number; got %r" % (token,))
token = next()
if token[0] not in self.number_tokens:
raise SyntaxError("expecting a number; got %r" % (token,))
axis_rotation = float(token[1])
axis_rotation = Decimal(token[1]) * 1
token = next()
if token[1] not in ('0', '1'):
raise SyntaxError("expecting a boolean flag; got %r" % (token,))
large_arc_flag = bool(int(token[1]))
large_arc_flag = Decimal(token[1]) * 1
token = next()
if token[1] not in ('0', '1'):
raise SyntaxError("expecting a boolean flag; got %r" % (token,))
sweep_flag = bool(int(token[1]))
sweep_flag = Decimal(token[1]) * 1
token = next()
if token[0] not in self.number_tokens:
raise SyntaxError("expecting a number; got %r" % (token,))
x = float(token[1])
x = Decimal(token[1]) * 1
token = next()
if token[0] not in self.number_tokens:
raise SyntaxError("expecting a number; got %r" % (token,))
y = float(token[1])
y = Decimal(token[1]) * 1
token = next()
arguments.append(((rx,ry), axis_rotation, large_arc_flag, sweep_flag, (x,y)))
arguments.extend([rx, ry, axis_rotation, large_arc_flag, sweep_flag, x, y])
return (command, arguments), token
def rule_coordinate(self, next, token):
if token[0] not in self.number_tokens:
raise SyntaxError("expecting a number; got %r" % (token,))
x = float(token[1])
x = getcontext().create_decimal(token[1])
token = next()
return x, token
@ -269,13 +273,13 @@ class SVGPathParser(object):
# Inline these since this rule is so common.
if token[0] not in self.number_tokens:
raise SyntaxError("expecting a number; got %r" % (token,))
x = float(token[1])
x = getcontext().create_decimal(token[1])
token = next()
if token[0] not in self.number_tokens:
raise SyntaxError("expecting a number; got %r" % (token,))
y = float(token[1])
y = getcontext().create_decimal(token[1])
token = next()
return (x,y), token
return [x, y], token
svg_parser = SVGPathParser()

233
svg_transform.py Normal file
View file

@ -0,0 +1,233 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# SVG transformation list parser
#
# Copyright 2010
#
# This file is part of Scour, http://www.codedread.com/scour/
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
""" Small recursive descent parser for SVG transform="" data.
In [1]: from svg_transform import svg_transform_parser
In [3]: svg_transform_parser.parse('translate(50, 50)')
Out[3]: [('translate', [50.0, 50.0])]
In [4]: svg_transform_parser.parse('translate(50)')
Out[4]: [('translate', [50.0])]
In [5]: svg_transform_parser.parse('rotate(36 50,50)')
Out[5]: [('rotate', [36.0, 50.0, 50.0])]
In [6]: svg_transform_parser.parse('rotate(36)')
Out[6]: [('rotate', [36.0])]
In [7]: svg_transform_parser.parse('skewX(20)')
Out[7]: [('skewX', [20.0])]
In [8]: svg_transform_parser.parse('skewY(40)')
Out[8]: [('skewX', [20.0])]
In [9]: svg_transform_parser.parse('scale(2 .5)')
Out[9]: [('scale', [2.0, 0.5])]
In [10]: svg_transform_parser.parse('scale(.5)')
Out[10]: [('scale', [0.5])]
In [11]: svg_transform_parser.parse('matrix(1 0 50 0 1 80)')
Out[11]: [('matrix', [1.0, 0.0, 50.0, 0.0, 1.0, 80.0])]
Multiple transformations are supported:
In [12]: svg_transform_parser.parse('translate(30 -30) rotate(36)')
Out[12]: [('translate', [30.0, -30.0]), ('rotate', [36.0])]
"""
import re
from decimal import *
# Sentinel.
class _EOF(object):
def __repr__(self):
return 'EOF'
EOF = _EOF()
lexicon = [
('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'),
('int', r'[-+]?[0-9]+'),
('command', r'(?:matrix|translate|scale|rotate|skew[XY])'),
('coordstart', r'\('),
('coordend', r'\)'),
]
class Lexer(object):
""" Break SVG path data into tokens.
The SVG spec requires that tokens are greedy. This lexer relies on Python's
regexes defaulting to greediness.
This style of implementation was inspired by this article:
http://www.gooli.org/blog/a-simple-lexer-in-python/
"""
def __init__(self, lexicon):
self.lexicon = lexicon
parts = []
for name, regex in lexicon:
parts.append('(?P<%s>%s)' % (name, regex))
self.regex_string = '|'.join(parts)
self.regex = re.compile(self.regex_string)
def lex(self, text):
""" Yield (token_type, str_data) tokens.
The last token will be (EOF, None) where EOF is the singleton object
defined in this module.
"""
for match in self.regex.finditer(text):
for name, _ in self.lexicon:
m = match.group(name)
if m is not None:
yield (name, m)
break
yield (EOF, None)
svg_lexer = Lexer(lexicon)
class SVGTransformationParser(object):
""" Parse SVG transform="" data into a list of commands.
Each distinct command will take the form of a tuple (type, data). The
`type` is the character string that defines the type of transformation in the
transform data, so either of "translate", "rotate", "scale", "matrix",
"skewX" and "skewY". Data is always a list of numbers contained within the
transformation's parentheses.
See the SVG documentation for the interpretation of the individual elements
for each transformation.
The main method is `parse(text)`. It can only consume actual strings, not
filelike objects or iterators.
"""
def __init__(self, lexer=svg_lexer):
self.lexer = lexer
self.command_dispatch = {
'translate': self.rule_1or2numbers,
'scale': self.rule_1or2numbers,
'skewX': self.rule_1number,
'skewY': self.rule_1number,
'rotate': self.rule_1or3numbers,
'matrix': self.rule_6numbers,
}
# self.number_tokens = set(['int', 'float'])
self.number_tokens = list(['int', 'float'])
def parse(self, text):
""" Parse a string of SVG transform="" data.
"""
next = self.lexer.lex(text).next
commands = []
token = next()
while token[0] is not EOF:
command, token = self.rule_svg_transform(next, token)
commands.append(command)
return commands
def rule_svg_transform(self, next, token):
if token[0] != 'command':
raise SyntaxError("expecting a transformation type; got %r" % (token,))
command = token[1]
rule = self.command_dispatch[command]
token = next()
if token[0] != 'coordstart':
raise SyntaxError("expecting '('; got %r" % (token,))
numbers, token = rule(next, token)
if token[0] != 'coordend':
raise SyntaxError("expecting ')'; got %r" % (token,))
token = next()
return (command, numbers), token
def rule_1or2numbers(self, next, token):
numbers = []
# 1st number is mandatory
token = next()
number, token = self.rule_number(next, token)
numbers.append(number)
# 2nd number is optional
number, token = self.rule_optional_number(next, token)
if number is not None:
numbers.append(number)
return numbers, token
def rule_1number(self, next, token):
# this number is mandatory
token = next()
number, token = self.rule_number(next, token)
numbers = [number]
return numbers, token
def rule_1or3numbers(self, next, token):
numbers = []
# 1st number is mandatory
token = next()
number, token = self.rule_number(next, token)
numbers.append(number)
# 2nd number is optional
number, token = self.rule_optional_number(next, token)
if number is not None:
# but, if the 2nd number is provided, the 3rd is mandatory.
# we can't have just 2.
numbers.append(number)
number, token = self.rule_number(next, token)
numbers.append(number)
return numbers, token
def rule_6numbers(self, next, token):
numbers = []
token = next()
# all numbers are mandatory
for i in xrange(6):
number, token = self.rule_number(next, token)
numbers.append(number)
return numbers, token
def rule_number(self, next, token):
if token[0] not in self.number_tokens:
raise SyntaxError("expecting a number; got %r" % (token,))
x = Decimal(token[1]) * 1
token = next()
return x, token
def rule_optional_number(self, next, token):
if token[0] not in self.number_tokens:
return None, token
else:
x = Decimal(token[1]) * 1
token = next()
return x, token
svg_transform_parser = SVGTransformationParser()

View file

@ -48,6 +48,9 @@ class ScourOptions:
strip_xml_prolog = False
indent_type = "space"
enable_viewboxing = False
shorten_ids = False
strip_comments = False
remove_metadata = False
class NoInkscapeElements(unittest.TestCase):
def runTest(self):
@ -445,7 +448,7 @@ class ConvertFillPropertyToAttr(unittest.TestCase):
class ConvertFillOpacityPropertyToAttr(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/fill-none.svg')
self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-opacity'), '0.5',
self.assertEquals(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-opacity'), '.5',
'fill-opacity property not converted to XML attribute' )
class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase):
@ -483,14 +486,14 @@ class RemoveTrailingZerosFromPath(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg')
path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')
self.assertEquals(path[:4] == 'M300' and path[4] != '.', True,
self.assertEquals(path[:4] == 'm300' and path[4] != '.', True,
'Trailing zeros not removed from path data' )
class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/path-truncate-zeros-calc.svg')
path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')
self.assertEquals(path, 'M5.81,0h0.1',
self.assertEquals(path, 'm5.81 0h0.1',
'Trailing zeros not removed from path data after calculation' )
class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase):
@ -504,7 +507,7 @@ class UseScientificNotationToShortenCoordsInPath(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/path-use-scientific-notation.svg')
path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')
self.assertEquals(path, 'M1E+4,0',
self.assertEquals(path, 'm1e4 0',
'Not using scientific notation for path coord when representation is shorter')
class ConvertAbsoluteToRelativePathCommands(unittest.TestCase):
@ -513,23 +516,23 @@ class ConvertAbsoluteToRelativePathCommands(unittest.TestCase):
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
self.assertEquals(path[1][0], 'v',
'Absolute V command not converted to relative v command')
self.assertEquals(path[1][1][0], -20.0,
self.assertEquals(float(path[1][1][0]), -20.0,
'Absolute V value not converted to relative v value')
class RoundPathData(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/path-precision.svg')
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
self.assertEquals(path[0][1][0][0], 100.0,
self.assertEquals(float(path[0][1][0]), 100.0,
'Not rounding down' )
self.assertEquals(path[0][1][0][1], 100.0,
self.assertEquals(float(path[0][1][1]), 100.0,
'Not rounding up' )
class LimitPrecisionInPathData(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/path-precision.svg')
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
self.assertEquals(path[1][1][0], 100.01,
self.assertEquals(float(path[1][1][0]), 100.01,
'Not correctly limiting precision on path data' )
class RemoveEmptyLineSegmentsFromPath(unittest.TestCase):
@ -545,7 +548,7 @@ class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase):
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,
self.assertEquals(float(path[1][1][0]), 200.0,
'Did not calculate horizontal line segment in path correctly' )
class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase):
@ -554,19 +557,19 @@ class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase):
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,
self.assertEquals(float(path[2][1][0]), 100.0,
'Did not calculate vertical line segment in path correctly' )
class ChangeBezierToShorthandInPath(unittest.TestCase):
def runTest(self):
path = scour.scourXmlFile('unittests/path-bez-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
self.assertEquals(path.getAttribute('d'), 'm10,100c50-50,50,50,100,0s50,50,100,0',
self.assertEquals(path.getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0',
'Did not change bezier curves into shorthand curve segments in path')
class ChangeQuadToShorthandInPath(unittest.TestCase):
def runTest(self):
path = scour.scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
self.assertEquals(path.getAttribute('d'), 'm10,100q50-50,100,0t100,0',
self.assertEquals(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0',
'Did not change quadratic curves into shorthand curve segments in path')
class HandleNonAsciiUtf8(unittest.TestCase):
@ -585,31 +588,31 @@ class HandleSciNoInPathData(unittest.TestCase):
class TranslateRGBIntoHex(unittest.TestCase):
def runTest(self):
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0]
self.assertEquals( elem.getAttribute('fill'), '#0F1011',
self.assertEquals( elem.getAttribute('fill'), '#0f1011',
'Not converting rgb into hex')
class TranslateRGBPctIntoHex(unittest.TestCase):
def runTest(self):
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0]
self.assertEquals( elem.getAttribute('stop-color'), '#7F0000',
self.assertEquals( elem.getAttribute('stop-color'), '#7f0000',
'Not converting rgb pct into hex')
class TranslateColorNamesIntoHex(unittest.TestCase):
def runTest(self):
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0]
self.assertEquals( elem.getAttribute('stroke'), '#A9A9A9',
self.assertEquals( elem.getAttribute('stroke'), '#a9a9a9',
'Not converting standard color names into hex')
class TranslateExtendedColorNamesIntoHex(unittest.TestCase):
def runTest(self):
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0]
self.assertEquals( elem.getAttribute('solid-color'), '#FAFAD2',
self.assertEquals( elem.getAttribute('solid-color'), '#fafad2',
'Not converting extended color names into hex')
class TranslateLongHexColorIntoShortHex(unittest.TestCase):
def runTest(self):
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0]
self.assertEquals( elem.getAttribute('fill'), '#FFF',
self.assertEquals( elem.getAttribute('fill'), '#fff',
'Not converting long hex color into short hex')
class DoNotConvertShortColorNames(unittest.TestCase):
@ -633,62 +636,62 @@ class RemoveFontStylesFromNonTextShapes(unittest.TestCase):
class CollapseConsecutiveHLinesSegments(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
self.assertEquals( p.getAttribute('d'), 'M100,100h200v100h-200z',
self.assertEquals( p.getAttribute('d'), 'm100 100h200v100h-200z',
'Did not collapse consecutive hlines segments')
class CollapseConsecutiveHLinesCoords(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[1]
self.assertEquals( p.getAttribute('d'), 'M100,300h200v100h-200z',
self.assertEquals( p.getAttribute('d'), 'm100 300h200v100h-200z',
'Did not collapse consecutive hlines coordinates')
class DoNotCollapseConsecutiveHLinesSegsWithDifferingSigns(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[2]
self.assertEquals( p.getAttribute('d'), 'M100,500h300-100v100h-200z',
self.assertEquals( p.getAttribute('d'), 'm100 500h300-100v100h-200z',
'Collapsed consecutive hlines segments with differing signs')
class ConvertStraightCurvesToLines(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
self.assertEquals(p.getAttribute('d'), 'M10,10l40,40,40-40z',
self.assertEquals(p.getAttribute('d'), 'm10 10l40 40 40-40z',
'Did not convert straight curves into lines')
class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0]
self.assertEquals(p.getAttribute('points'), '50,50,150,50,150,150,50,150',
self.assertEquals(p.getAttribute('points'), '50 50 150 50 150 150 50 150',
'Unnecessary polygon end point not removed' )
class DoNotRemovePolgonLastPoint(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1]
self.assertEquals(p.getAttribute('points'), '200,50,300,50,300,150,200,150',
self.assertEquals(p.getAttribute('points'), '200 50 300 50 300 150 200 150',
'Last point of polygon removed' )
class ScourPolygonCoordsSciNo(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0]
self.assertEquals(p.getAttribute('points'), '1E+4,50',
self.assertEquals(p.getAttribute('points'), '1e4 50',
'Polygon coordinates not scoured')
class ScourPolylineCoordsSciNo(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0]
self.assertEquals(p.getAttribute('points'), '1E+4,50',
self.assertEquals(p.getAttribute('points'), '1e4 50',
'Polyline coordinates not scoured')
class ScourPolygonNegativeCoords(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0]
# points="100,-100,100-100,100-100-100,-100-100,200" />
self.assertEquals(p.getAttribute('points'), '100,-100,100,-100,100,-100,-100,-100,-100,200',
self.assertEquals(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200',
'Negative polygon coordinates not properly parsed')
class ScourPolylineNegativeCoords(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0]
self.assertEquals(p.getAttribute('points'), '100,-100,100,-100,100,-100,-100,-100,-100,200',
self.assertEquals(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200',
'Negative polyline coordinates not properly parsed')
class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase):
@ -700,7 +703,7 @@ class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase):
class AlwaysKeepClosePathSegments(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
self.assertEquals(p.getAttribute('d'), 'M10,10h100v100h-100z',
self.assertEquals(p.getAttribute('d'), 'm10 10h100v100h-100z',
'Path with closepath not preserved')
class RemoveDuplicateLinearGradients(unittest.TestCase):
@ -736,7 +739,7 @@ class RereferenceForRadialGradient(unittest.TestCase):
class CollapseSamePathPoints(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0];
self.assertEquals(p.getAttribute('d'), "M100,100l100.12,100.12z",
self.assertEquals(p.getAttribute('d'), "m100 100l100.12 100.12z",
'Did not collapse same path points')
class ScourUnitlessLengths(unittest.TestCase):
@ -923,7 +926,7 @@ class PropagateCommonAttributesUp(unittest.TestCase):
class PathEllipticalArcParsingCommaWsp(unittest.TestCase):
def runTest(self):
p = scour.scourXmlFile('unittests/path-elliptical-arc-parsing.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
self.assertEquals( p.getAttribute('d'), 'M100,100a100,100,0,1,1,-50,100z',
self.assertEquals( p.getAttribute('d'), 'm100 100a100 100 0 1 1 -50 100z',
'Did not parse elliptical arc command properly')
class RemoveUnusedAttributesOnParent(unittest.TestCase):
@ -1022,14 +1025,49 @@ class DoNotStripDoctype(unittest.TestCase):
class PathImplicitLineWithMoveCommands(unittest.TestCase):
def runTest(self):
path = scour.scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
self.assertEquals( path.getAttribute('d'), "M100,100,100,200m200-100-200,0m200,100,0-100",
self.assertEquals( path.getAttribute('d'), "m100 100v100m200-100h-200m200 100v-100",
"Implicit line segments after move not preserved")
class RemoveMetadataOption(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/full-metadata.svg',
scour.parse_args(['--remove-metadata'])[0])
self.assertEquals(doc.childNodes.length, 1,
'Did not remove <metadata> tag with --remove-metadata')
class EnableCommentStrippingOption(unittest.TestCase):
def runTest(self):
docStr = file('unittests/comment-beside-xml-decl.svg').read()
docStr = scour.scourString(docStr,
scour.parse_args(['--enable-comment-stripping'])[0])
self.assertEquals(docStr.find('<!--'), -1,
'Did not remove document-level comment with --enable-comment-stripping')
class StripXmlPrologOption(unittest.TestCase):
def runTest(self):
docStr = file('unittests/comment-beside-xml-decl.svg').read()
docStr = scour.scourString(docStr,
scour.parse_args(['--strip-xml-prolog'])[0])
self.assertEquals(docStr.find('<?xml'), -1,
'Did not remove <?xml?> with --strip-xml-prolog')
class ShortenIDsOption(unittest.TestCase):
def runTest(self):
doc = scour.scourXmlFile('unittests/shorten-ids.svg',
scour.parse_args(['--shorten-ids'])[0])
gradientTag = doc.getElementsByTagName('linearGradient')[0]
self.assertEquals(gradientTag.getAttribute('id'), 'a',
"Did not shorten a linear gradient's ID with --shorten-ids")
rectTag = doc.getElementsByTagName('rect')[0]
self.assertEquals(rectTag.getAttribute('fill'), 'url(#a)',
'Did not update reference to shortened ID')
# TODO: write tests for --enable-viewboxing
# TODO; write a test for embedding rasters
# TODO: write a test for --disable-embed-rasters
# TODO: write tests for --keep-editor-data
# TODO: write tests for --strip-xml-prolog
# TODO: write tests for scouring transformations
if __name__ == '__main__':
testcss = __import__('testcss')

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!-- Oh look a comment -->
<svg xmlns="http://www.w3.org/2000/svg">
</svg>

After

Width:  |  Height:  |  Size: 130 B

View file

@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>
<rdf:RDF
xmlns:rdf = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:rdfs = "http://www.w3.org/2000/01/rdf-schema#"
xmlns:dc = "http://purl.org/dc/elements/1.1/" >
<rdf:Description about="http://example.org/myfoo"
dc:title="MyFoo"
dc:description="Unit test for Scour's --remove-metadata option"
dc:publisher="No One"
dc:date="2010-06-09"
dc:format="image/svg+xml"
dc:language="en" >
<dc:creator>
<rdf:Bag>
<rdf:li>No One</rdf:li>
</rdf:Bag>
</dc:creator>
</rdf:Description>
</rdf:RDF>
</metadata>
</svg>

After

Width:  |  Height:  |  Size: 764 B

10
unittests/shorten-ids.svg Normal file
View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<defs>
<linearGradient id="this-abomination-should-be-shortened-to-a-single-letter">
<stop offset="0" stop-color="black" />
<stop offset="1" stop-color="white" />
</linearGradient>
</defs>
<rect fill="url(#this-abomination-should-be-shortened-to-a-single-letter)" x="20" y="20" width="160" height="160" />
</svg>

After

Width:  |  Height:  |  Size: 447 B