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:
parent
00804fb833
commit
f4cca44faf
9 changed files with 1153 additions and 518 deletions
|
|
@ -22,12 +22,18 @@ class ScourInkscape (inkex.Effect):
|
||||||
self.OptionParser.add_option("--enable-id-stripping", type="inkbool",
|
self.OptionParser.add_option("--enable-id-stripping", type="inkbool",
|
||||||
action="store", dest="strip_ids", default=False,
|
action="store", dest="strip_ids", default=False,
|
||||||
help="remove all un-referenced ID attributes")
|
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",
|
self.OptionParser.add_option("--embed-rasters", type="inkbool",
|
||||||
action="store", dest="embed_rasters", default=True,
|
action="store", dest="embed_rasters", default=True,
|
||||||
help="won't embed rasters as base64-encoded data")
|
help="won't embed rasters as base64-encoded data")
|
||||||
self.OptionParser.add_option("--keep-editor-data", type="inkbool",
|
self.OptionParser.add_option("--keep-editor-data", type="inkbool",
|
||||||
action="store", dest="keep_editor_data", default=False,
|
action="store", dest="keep_editor_data", default=False,
|
||||||
help="won't remove Inkscape, Sodipodi or Adobe Illustrator elements and attributes")
|
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",
|
self.OptionParser.add_option("--strip-xml-prolog", type="inkbool",
|
||||||
action="store", dest="strip_xml_prolog", default=False,
|
action="store", dest="strip_xml_prolog", default=False,
|
||||||
help="won't output the <?xml ?> prolog")
|
help="won't output the <?xml ?> prolog")
|
||||||
|
|
@ -40,7 +46,12 @@ class ScourInkscape (inkex.Effect):
|
||||||
self.OptionParser.add_option("--enable-viewboxing", type="inkbool",
|
self.OptionParser.add_option("--enable-viewboxing", type="inkbool",
|
||||||
action="store", dest="enable_viewboxing", default=False,
|
action="store", dest="enable_viewboxing", default=False,
|
||||||
help="changes document width/height to 100%/100% and creates viewbox coordinates")
|
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):
|
def effect(self):
|
||||||
input = file(sys.argv[12], "r")
|
input = file(sys.argv[12], "r")
|
||||||
|
|
|
||||||
37
scour.inx
37
scour.inx
|
|
@ -7,16 +7,20 @@
|
||||||
<dependency type="executable" location="extensions">yocto_css.py</dependency>
|
<dependency type="executable" location="extensions">yocto_css.py</dependency>
|
||||||
<param name="tab" type="notebook">
|
<param name="tab" type="notebook">
|
||||||
<page name="Options" _gui-text="Options">
|
<page name="Options" _gui-text="Options">
|
||||||
<param name="simplify-colors" type="boolean" _gui-text="Simplify colors">true</param>
|
<param name="simplify-colors" type="boolean" _gui-text="Shorten color values">true</param>
|
||||||
<param name="style-to-xml" type="boolean" _gui-text="Style to xml">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="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="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="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="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="strip-xml-prolog" type="boolean" _gui-text="Remove the <?xml?> declaration">false</param>
|
||||||
<param name="set-precision" type="int" _gui-text="Set precision">5</param>
|
<param name="set-precision" type="int" _gui-text="Number of significant digits for coords">5</param>
|
||||||
<param name="indent" type="enum" _gui-text="Indent">
|
<param name="indent" type="enum" _gui-text="XML indentation (pretty-printing)">
|
||||||
<_item value="space">Space</_item>
|
<_item value="space">Space</_item>
|
||||||
<_item value="tab">Tab</_item>
|
<_item value="tab">Tab</_item>
|
||||||
<_item value="none">None</_item>
|
<_item value="none">None</_item>
|
||||||
|
|
@ -24,16 +28,19 @@
|
||||||
</page>
|
</page>
|
||||||
<page name="Help" _gui-text="Help">
|
<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:
|
<_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.
|
* Shorten color names: convert all colors to #RRGGBB or #RGB format.
|
||||||
* Style to xml: convert styles into XML attributes.
|
* Convert CSS attributes to XML attributes: convert styles from <style> tags and inline style="" declarations into XML attributes.
|
||||||
* Group collapsing: collapse group elements.
|
* Group collapsing: removes useless <g> elements, promoting their contents up one level. Requires "Remove unused ID names for elements" to be set.
|
||||||
* Enable id stripping: remove all un-referenced ID attributes.
|
* Remove unused ID names for elements: remove all unreferenced ID attributes.
|
||||||
* Embed rasters: embed rasters as base64-encoded data.
|
* 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.
|
* 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
|
* Remove metadata: remove <metadata> tags along with all the information in them, which may include license metadata, alternate versions for non-SVG-enabled browsers, etc.
|
||||||
* Strip xml prolog: don't output the xml prolog.
|
* Remove comments: remove <!-- --> tags.
|
||||||
* Set precision: set number of significant digits (default: 5).
|
* 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.
|
||||||
* Indent: indentation of the output: none, space, tab (default: space).</_param>
|
* 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>
|
</page>
|
||||||
</param>
|
</param>
|
||||||
<output>
|
<output>
|
||||||
|
|
|
||||||
42
svg_regex.py
42
svg_regex.py
|
|
@ -43,6 +43,7 @@ Out[5]: [('M', [(100.0, -200.0)])]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from decimal import *
|
||||||
|
|
||||||
|
|
||||||
# Sentinel.
|
# Sentinel.
|
||||||
|
|
@ -52,8 +53,8 @@ class _EOF(object):
|
||||||
EOF = _EOF()
|
EOF = _EOF()
|
||||||
|
|
||||||
lexicon = [
|
lexicon = [
|
||||||
('float', r'[-\+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-\+]?[0-9]+)?'),
|
('float', r'[-+]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.?))(?:[Ee][-+]?[0-9]+)?'),
|
||||||
('int', r'[-\+]?[0-9]+'),
|
('int', r'[-+]?[0-9]+'),
|
||||||
('command', r'[AaCcHhLlMmQqSsTtVvZz]'),
|
('command', r'[AaCcHhLlMmQqSsTtVvZz]'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -161,7 +162,7 @@ class SVGPathParser(object):
|
||||||
def rule_closepath(self, next, token):
|
def rule_closepath(self, next, token):
|
||||||
command = token[1]
|
command = token[1]
|
||||||
token = next()
|
token = next()
|
||||||
return (command, None), token
|
return (command, []), token
|
||||||
|
|
||||||
def rule_moveto_or_lineto(self, next, token):
|
def rule_moveto_or_lineto(self, next, token):
|
||||||
command = token[1]
|
command = token[1]
|
||||||
|
|
@ -169,7 +170,7 @@ class SVGPathParser(object):
|
||||||
coordinates = []
|
coordinates = []
|
||||||
while token[0] in self.number_tokens:
|
while token[0] in self.number_tokens:
|
||||||
pair, token = self.rule_coordinate_pair(next, token)
|
pair, token = self.rule_coordinate_pair(next, token)
|
||||||
coordinates.append(pair)
|
coordinates.extend(pair)
|
||||||
return (command, coordinates), token
|
return (command, coordinates), token
|
||||||
|
|
||||||
def rule_orthogonal_lineto(self, next, token):
|
def rule_orthogonal_lineto(self, next, token):
|
||||||
|
|
@ -189,7 +190,9 @@ class SVGPathParser(object):
|
||||||
pair1, token = self.rule_coordinate_pair(next, token)
|
pair1, token = self.rule_coordinate_pair(next, token)
|
||||||
pair2, token = self.rule_coordinate_pair(next, token)
|
pair2, token = self.rule_coordinate_pair(next, token)
|
||||||
pair3, 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
|
return (command, coordinates), token
|
||||||
|
|
||||||
def rule_curveto2(self, next, token):
|
def rule_curveto2(self, next, token):
|
||||||
|
|
@ -199,7 +202,8 @@ class SVGPathParser(object):
|
||||||
while token[0] in self.number_tokens:
|
while token[0] in self.number_tokens:
|
||||||
pair1, token = self.rule_coordinate_pair(next, token)
|
pair1, token = self.rule_coordinate_pair(next, token)
|
||||||
pair2, 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
|
return (command, coordinates), token
|
||||||
|
|
||||||
def rule_curveto1(self, next, token):
|
def rule_curveto1(self, next, token):
|
||||||
|
|
@ -208,7 +212,7 @@ class SVGPathParser(object):
|
||||||
coordinates = []
|
coordinates = []
|
||||||
while token[0] in self.number_tokens:
|
while token[0] in self.number_tokens:
|
||||||
pair1, token = self.rule_coordinate_pair(next, token)
|
pair1, token = self.rule_coordinate_pair(next, token)
|
||||||
coordinates.append(pair1)
|
coordinates.extend(pair1)
|
||||||
return (command, coordinates), token
|
return (command, coordinates), token
|
||||||
|
|
||||||
def rule_elliptical_arc(self, next, token):
|
def rule_elliptical_arc(self, next, token):
|
||||||
|
|
@ -216,51 +220,51 @@ class SVGPathParser(object):
|
||||||
token = next()
|
token = next()
|
||||||
arguments = []
|
arguments = []
|
||||||
while token[0] in self.number_tokens:
|
while token[0] in self.number_tokens:
|
||||||
rx = float(token[1])
|
rx = Decimal(token[1]) * 1
|
||||||
if rx < 0.0:
|
if rx < 0.0:
|
||||||
raise SyntaxError("expecting a nonnegative number; got %r" % (token,))
|
raise SyntaxError("expecting a nonnegative number; got %r" % (token,))
|
||||||
|
|
||||||
token = next()
|
token = next()
|
||||||
if token[0] not in self.number_tokens:
|
if token[0] not in self.number_tokens:
|
||||||
raise SyntaxError("expecting a number; got %r" % (token,))
|
raise SyntaxError("expecting a number; got %r" % (token,))
|
||||||
ry = float(token[1])
|
ry = Decimal(token[1]) * 1
|
||||||
if ry < 0.0:
|
if ry < 0.0:
|
||||||
raise SyntaxError("expecting a nonnegative number; got %r" % (token,))
|
raise SyntaxError("expecting a nonnegative number; got %r" % (token,))
|
||||||
|
|
||||||
token = next()
|
token = next()
|
||||||
if token[0] not in self.number_tokens:
|
if token[0] not in self.number_tokens:
|
||||||
raise SyntaxError("expecting a number; got %r" % (token,))
|
raise SyntaxError("expecting a number; got %r" % (token,))
|
||||||
axis_rotation = float(token[1])
|
axis_rotation = Decimal(token[1]) * 1
|
||||||
|
|
||||||
token = next()
|
token = next()
|
||||||
if token[1] not in ('0', '1'):
|
if token[1] not in ('0', '1'):
|
||||||
raise SyntaxError("expecting a boolean flag; got %r" % (token,))
|
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()
|
token = next()
|
||||||
if token[1] not in ('0', '1'):
|
if token[1] not in ('0', '1'):
|
||||||
raise SyntaxError("expecting a boolean flag; got %r" % (token,))
|
raise SyntaxError("expecting a boolean flag; got %r" % (token,))
|
||||||
sweep_flag = bool(int(token[1]))
|
sweep_flag = Decimal(token[1]) * 1
|
||||||
|
|
||||||
token = next()
|
token = next()
|
||||||
if token[0] not in self.number_tokens:
|
if token[0] not in self.number_tokens:
|
||||||
raise SyntaxError("expecting a number; got %r" % (token,))
|
raise SyntaxError("expecting a number; got %r" % (token,))
|
||||||
x = float(token[1])
|
x = Decimal(token[1]) * 1
|
||||||
|
|
||||||
token = next()
|
token = next()
|
||||||
if token[0] not in self.number_tokens:
|
if token[0] not in self.number_tokens:
|
||||||
raise SyntaxError("expecting a number; got %r" % (token,))
|
raise SyntaxError("expecting a number; got %r" % (token,))
|
||||||
y = float(token[1])
|
y = Decimal(token[1]) * 1
|
||||||
|
|
||||||
token = next()
|
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
|
return (command, arguments), token
|
||||||
|
|
||||||
def rule_coordinate(self, next, token):
|
def rule_coordinate(self, next, token):
|
||||||
if token[0] not in self.number_tokens:
|
if token[0] not in self.number_tokens:
|
||||||
raise SyntaxError("expecting a number; got %r" % (token,))
|
raise SyntaxError("expecting a number; got %r" % (token,))
|
||||||
x = float(token[1])
|
x = getcontext().create_decimal(token[1])
|
||||||
token = next()
|
token = next()
|
||||||
return x, token
|
return x, token
|
||||||
|
|
||||||
|
|
@ -269,13 +273,13 @@ class SVGPathParser(object):
|
||||||
# Inline these since this rule is so common.
|
# Inline these since this rule is so common.
|
||||||
if token[0] not in self.number_tokens:
|
if token[0] not in self.number_tokens:
|
||||||
raise SyntaxError("expecting a number; got %r" % (token,))
|
raise SyntaxError("expecting a number; got %r" % (token,))
|
||||||
x = float(token[1])
|
x = getcontext().create_decimal(token[1])
|
||||||
token = next()
|
token = next()
|
||||||
if token[0] not in self.number_tokens:
|
if token[0] not in self.number_tokens:
|
||||||
raise SyntaxError("expecting a number; got %r" % (token,))
|
raise SyntaxError("expecting a number; got %r" % (token,))
|
||||||
y = float(token[1])
|
y = getcontext().create_decimal(token[1])
|
||||||
token = next()
|
token = next()
|
||||||
return (x,y), token
|
return [x, y], token
|
||||||
|
|
||||||
|
|
||||||
svg_parser = SVGPathParser()
|
svg_parser = SVGPathParser()
|
||||||
|
|
|
||||||
233
svg_transform.py
Normal file
233
svg_transform.py
Normal 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()
|
||||||
102
testscour.py
102
testscour.py
|
|
@ -48,6 +48,9 @@ class ScourOptions:
|
||||||
strip_xml_prolog = False
|
strip_xml_prolog = False
|
||||||
indent_type = "space"
|
indent_type = "space"
|
||||||
enable_viewboxing = False
|
enable_viewboxing = False
|
||||||
|
shorten_ids = False
|
||||||
|
strip_comments = False
|
||||||
|
remove_metadata = False
|
||||||
|
|
||||||
class NoInkscapeElements(unittest.TestCase):
|
class NoInkscapeElements(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
|
|
@ -445,7 +448,7 @@ class ConvertFillPropertyToAttr(unittest.TestCase):
|
||||||
class ConvertFillOpacityPropertyToAttr(unittest.TestCase):
|
class ConvertFillOpacityPropertyToAttr(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
doc = scour.scourXmlFile('unittests/fill-none.svg')
|
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' )
|
'fill-opacity property not converted to XML attribute' )
|
||||||
|
|
||||||
class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase):
|
class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase):
|
||||||
|
|
@ -483,14 +486,14 @@ class RemoveTrailingZerosFromPath(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg')
|
doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg')
|
||||||
path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')
|
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' )
|
'Trailing zeros not removed from path data' )
|
||||||
|
|
||||||
class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase):
|
class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
doc = scour.scourXmlFile('unittests/path-truncate-zeros-calc.svg')
|
doc = scour.scourXmlFile('unittests/path-truncate-zeros-calc.svg')
|
||||||
path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')
|
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' )
|
'Trailing zeros not removed from path data after calculation' )
|
||||||
|
|
||||||
class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase):
|
class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase):
|
||||||
|
|
@ -504,7 +507,7 @@ class UseScientificNotationToShortenCoordsInPath(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
doc = scour.scourXmlFile('unittests/path-use-scientific-notation.svg')
|
doc = scour.scourXmlFile('unittests/path-use-scientific-notation.svg')
|
||||||
path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')
|
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')
|
'Not using scientific notation for path coord when representation is shorter')
|
||||||
|
|
||||||
class ConvertAbsoluteToRelativePathCommands(unittest.TestCase):
|
class ConvertAbsoluteToRelativePathCommands(unittest.TestCase):
|
||||||
|
|
@ -513,23 +516,23 @@ class ConvertAbsoluteToRelativePathCommands(unittest.TestCase):
|
||||||
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
|
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
|
||||||
self.assertEquals(path[1][0], 'v',
|
self.assertEquals(path[1][0], 'v',
|
||||||
'Absolute V command not converted to relative v command')
|
'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')
|
'Absolute V value not converted to relative v value')
|
||||||
|
|
||||||
class RoundPathData(unittest.TestCase):
|
class RoundPathData(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
doc = scour.scourXmlFile('unittests/path-precision.svg')
|
doc = scour.scourXmlFile('unittests/path-precision.svg')
|
||||||
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
|
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' )
|
'Not rounding down' )
|
||||||
self.assertEquals(path[0][1][0][1], 100.0,
|
self.assertEquals(float(path[0][1][1]), 100.0,
|
||||||
'Not rounding up' )
|
'Not rounding up' )
|
||||||
|
|
||||||
class LimitPrecisionInPathData(unittest.TestCase):
|
class LimitPrecisionInPathData(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
doc = scour.scourXmlFile('unittests/path-precision.svg')
|
doc = scour.scourXmlFile('unittests/path-precision.svg')
|
||||||
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
|
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' )
|
'Not correctly limiting precision on path data' )
|
||||||
|
|
||||||
class RemoveEmptyLineSegmentsFromPath(unittest.TestCase):
|
class RemoveEmptyLineSegmentsFromPath(unittest.TestCase):
|
||||||
|
|
@ -545,7 +548,7 @@ class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase):
|
||||||
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
|
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
|
||||||
self.assertEquals(path[1][0], 'h',
|
self.assertEquals(path[1][0], 'h',
|
||||||
'Did not change line to horizontal line segment in path' )
|
'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' )
|
'Did not calculate horizontal line segment in path correctly' )
|
||||||
|
|
||||||
class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase):
|
class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase):
|
||||||
|
|
@ -554,19 +557,19 @@ class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase):
|
||||||
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
|
path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d'))
|
||||||
self.assertEquals(path[2][0], 'v',
|
self.assertEquals(path[2][0], 'v',
|
||||||
'Did not change line to vertical line segment in path' )
|
'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' )
|
'Did not calculate vertical line segment in path correctly' )
|
||||||
|
|
||||||
class ChangeBezierToShorthandInPath(unittest.TestCase):
|
class ChangeBezierToShorthandInPath(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
path = scour.scourXmlFile('unittests/path-bez-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
|
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')
|
'Did not change bezier curves into shorthand curve segments in path')
|
||||||
|
|
||||||
class ChangeQuadToShorthandInPath(unittest.TestCase):
|
class ChangeQuadToShorthandInPath(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
path = scour.scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
|
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')
|
'Did not change quadratic curves into shorthand curve segments in path')
|
||||||
|
|
||||||
class HandleNonAsciiUtf8(unittest.TestCase):
|
class HandleNonAsciiUtf8(unittest.TestCase):
|
||||||
|
|
@ -585,31 +588,31 @@ class HandleSciNoInPathData(unittest.TestCase):
|
||||||
class TranslateRGBIntoHex(unittest.TestCase):
|
class TranslateRGBIntoHex(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0]
|
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')
|
'Not converting rgb into hex')
|
||||||
|
|
||||||
class TranslateRGBPctIntoHex(unittest.TestCase):
|
class TranslateRGBPctIntoHex(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0]
|
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')
|
'Not converting rgb pct into hex')
|
||||||
|
|
||||||
class TranslateColorNamesIntoHex(unittest.TestCase):
|
class TranslateColorNamesIntoHex(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0]
|
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')
|
'Not converting standard color names into hex')
|
||||||
|
|
||||||
class TranslateExtendedColorNamesIntoHex(unittest.TestCase):
|
class TranslateExtendedColorNamesIntoHex(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0]
|
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')
|
'Not converting extended color names into hex')
|
||||||
|
|
||||||
class TranslateLongHexColorIntoShortHex(unittest.TestCase):
|
class TranslateLongHexColorIntoShortHex(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0]
|
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')
|
'Not converting long hex color into short hex')
|
||||||
|
|
||||||
class DoNotConvertShortColorNames(unittest.TestCase):
|
class DoNotConvertShortColorNames(unittest.TestCase):
|
||||||
|
|
@ -633,62 +636,62 @@ class RemoveFontStylesFromNonTextShapes(unittest.TestCase):
|
||||||
class CollapseConsecutiveHLinesSegments(unittest.TestCase):
|
class CollapseConsecutiveHLinesSegments(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
|
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')
|
'Did not collapse consecutive hlines segments')
|
||||||
|
|
||||||
class CollapseConsecutiveHLinesCoords(unittest.TestCase):
|
class CollapseConsecutiveHLinesCoords(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[1]
|
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')
|
'Did not collapse consecutive hlines coordinates')
|
||||||
|
|
||||||
class DoNotCollapseConsecutiveHLinesSegsWithDifferingSigns(unittest.TestCase):
|
class DoNotCollapseConsecutiveHLinesSegsWithDifferingSigns(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[2]
|
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')
|
'Collapsed consecutive hlines segments with differing signs')
|
||||||
|
|
||||||
class ConvertStraightCurvesToLines(unittest.TestCase):
|
class ConvertStraightCurvesToLines(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
|
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')
|
'Did not convert straight curves into lines')
|
||||||
|
|
||||||
class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase):
|
class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0]
|
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' )
|
'Unnecessary polygon end point not removed' )
|
||||||
|
|
||||||
class DoNotRemovePolgonLastPoint(unittest.TestCase):
|
class DoNotRemovePolgonLastPoint(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1]
|
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' )
|
'Last point of polygon removed' )
|
||||||
|
|
||||||
class ScourPolygonCoordsSciNo(unittest.TestCase):
|
class ScourPolygonCoordsSciNo(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0]
|
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')
|
'Polygon coordinates not scoured')
|
||||||
|
|
||||||
class ScourPolylineCoordsSciNo(unittest.TestCase):
|
class ScourPolylineCoordsSciNo(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0]
|
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')
|
'Polyline coordinates not scoured')
|
||||||
|
|
||||||
class ScourPolygonNegativeCoords(unittest.TestCase):
|
class ScourPolygonNegativeCoords(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0]
|
p = scour.scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0]
|
||||||
# points="100,-100,100-100,100-100-100,-100-100,200" />
|
# 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')
|
'Negative polygon coordinates not properly parsed')
|
||||||
|
|
||||||
class ScourPolylineNegativeCoords(unittest.TestCase):
|
class ScourPolylineNegativeCoords(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0]
|
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')
|
'Negative polyline coordinates not properly parsed')
|
||||||
|
|
||||||
class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase):
|
class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase):
|
||||||
|
|
@ -700,7 +703,7 @@ class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase):
|
||||||
class AlwaysKeepClosePathSegments(unittest.TestCase):
|
class AlwaysKeepClosePathSegments(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
|
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')
|
'Path with closepath not preserved')
|
||||||
|
|
||||||
class RemoveDuplicateLinearGradients(unittest.TestCase):
|
class RemoveDuplicateLinearGradients(unittest.TestCase):
|
||||||
|
|
@ -736,7 +739,7 @@ class RereferenceForRadialGradient(unittest.TestCase):
|
||||||
class CollapseSamePathPoints(unittest.TestCase):
|
class CollapseSamePathPoints(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0];
|
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')
|
'Did not collapse same path points')
|
||||||
|
|
||||||
class ScourUnitlessLengths(unittest.TestCase):
|
class ScourUnitlessLengths(unittest.TestCase):
|
||||||
|
|
@ -923,7 +926,7 @@ class PropagateCommonAttributesUp(unittest.TestCase):
|
||||||
class PathEllipticalArcParsingCommaWsp(unittest.TestCase):
|
class PathEllipticalArcParsingCommaWsp(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
p = scour.scourXmlFile('unittests/path-elliptical-arc-parsing.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
|
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')
|
'Did not parse elliptical arc command properly')
|
||||||
|
|
||||||
class RemoveUnusedAttributesOnParent(unittest.TestCase):
|
class RemoveUnusedAttributesOnParent(unittest.TestCase):
|
||||||
|
|
@ -1022,14 +1025,49 @@ class DoNotStripDoctype(unittest.TestCase):
|
||||||
class PathImplicitLineWithMoveCommands(unittest.TestCase):
|
class PathImplicitLineWithMoveCommands(unittest.TestCase):
|
||||||
def runTest(self):
|
def runTest(self):
|
||||||
path = scour.scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0]
|
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")
|
"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 tests for --enable-viewboxing
|
||||||
# TODO; write a test for embedding rasters
|
# TODO; write a test for embedding rasters
|
||||||
# TODO: write a test for --disable-embed-rasters
|
# TODO: write a test for --disable-embed-rasters
|
||||||
# TODO: write tests for --keep-editor-data
|
# TODO: write tests for --keep-editor-data
|
||||||
# TODO: write tests for --strip-xml-prolog
|
# TODO: write tests for scouring transformations
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
testcss = __import__('testcss')
|
testcss = __import__('testcss')
|
||||||
|
|
|
||||||
4
unittests/comment-beside-xml-decl.svg
Normal file
4
unittests/comment-beside-xml-decl.svg
Normal 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 |
22
unittests/full-metadata.svg
Normal file
22
unittests/full-metadata.svg
Normal 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
10
unittests/shorten-ids.svg
Normal 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 |
Loading…
Add table
Add a link
Reference in a new issue