From 0f1974c1e230b789eaab36b105aecf5af98fcf44 Mon Sep 17 00:00:00 2001 From: Eduard Braun Date: Thu, 15 Sep 2016 00:35:13 +0200 Subject: [PATCH 1/6] Consistent whitespace across source files according to PEP 8 (mostly automated by using autopep8, fixes #69) --- scour/__init__.py | 30 +- scour/scour.py | 6191 ++++++++++++++++++++-------------------- scour/svg_regex.py | 5 +- scour/svg_transform.py | 6 +- scour/yocto_css.py | 46 +- setup.py | 92 +- testcss.py | 35 +- testscour.py | 2658 ++++++++++------- 8 files changed, 4787 insertions(+), 4276 deletions(-) diff --git a/scour/__init__.py b/scour/__init__.py index 568336e..aca9dcf 100644 --- a/scour/__init__.py +++ b/scour/__init__.py @@ -1,19 +1,19 @@ ############################################################################### -## -## Copyright (C) 2010 Jeff Schiller, 2010 Louis Simard, 2013-2015 Tavendo GmbH -## -## 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. -## +# +# Copyright (C) 2010 Jeff Schiller, 2010 Louis Simard, 2013-2015 Tavendo GmbH +# +# 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. +# ############################################################################### __version__ = u'0.35' diff --git a/scour/scour.py b/scour/scour.py index da212fb..78bb66b 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -68,9 +68,9 @@ from decimal import Context, Decimal, InvalidOperation, getcontext # select the most precise walltime measurement function available on the platform if sys.platform.startswith('win'): - walltime = time.clock + walltime = time.clock else: - walltime = time.time + walltime = time.time from scour import __version__ @@ -95,11 +95,11 @@ NS = {'SVG': 'http://www.w3.org/2000/svg', 'SKETCH': 'http://www.bohemiancoding.com/sketch/ns' } -unwanted_ns = [ NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], - NS['ADOBE_GRAPHS'], NS['ADOBE_SVG_VIEWER'], NS['ADOBE_VARIABLES'], - NS['ADOBE_SFW'], NS['ADOBE_EXTENSIBILITY'], NS['ADOBE_FLOWS'], - NS['ADOBE_IMAGE_REPLACEMENT'], NS['ADOBE_CUSTOM'], - NS['ADOBE_XPATH'], NS['SKETCH'] ] +unwanted_ns = [NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], + NS['ADOBE_GRAPHS'], NS['ADOBE_SVG_VIEWER'], NS['ADOBE_VARIABLES'], + NS['ADOBE_SFW'], NS['ADOBE_EXTENSIBILITY'], NS['ADOBE_FLOWS'], + NS['ADOBE_IMAGE_REPLACEMENT'], NS['ADOBE_CUSTOM'], + NS['ADOBE_XPATH'], NS['SKETCH']] # A list of all SVG presentation properties # @@ -109,230 +109,230 @@ unwanted_ns = [ NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], # https://www.w3.org/TR/SVG2/propidx.html (not yet implemented) # svgAttributes = [ - # SVG 1.1 - 'alignment-baseline', - 'baseline-shift', - 'clip', - 'clip-path', - 'clip-rule', - 'color', - 'color-interpolation', - 'color-interpolation-filters', - 'color-profile', - 'color-rendering', - 'cursor', - 'direction', - 'display', - 'dominant-baseline', - 'enable-background', - 'fill', - 'fill-opacity', - 'fill-rule', - 'filter', - 'flood-color', - 'flood-opacity', - 'font', - 'font-family', - 'font-size', - 'font-size-adjust', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'glyph-orientation-horizontal', - 'glyph-orientation-vertical', - 'image-rendering', - 'kerning', - 'letter-spacing', - 'lighting-color', - 'marker', - 'marker-end', - 'marker-mid', - 'marker-start', - 'mask', - 'opacity', - 'overflow', - 'pointer-events', - 'shape-rendering', - 'stop-color', - 'stop-opacity', - 'stroke', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke-width', - 'text-anchor', - 'text-decoration', - 'text-rendering', - 'unicode-bidi', - 'visibility', - 'word-spacing', - 'writing-mode', - # SVG 1.2 Tiny - 'audio-level', - 'buffered-rendering', - 'display-align', - 'line-increment', - 'solid-color', - 'solid-opacity', - 'text-align', - 'vector-effect', - 'viewport-fill', - 'viewport-fill-opacity', - ] + # SVG 1.1 + 'alignment-baseline', + 'baseline-shift', + 'clip', + 'clip-path', + 'clip-rule', + 'color', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'cursor', + 'direction', + 'display', + 'dominant-baseline', + 'enable-background', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'flood-color', + 'flood-opacity', + 'font', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'glyph-orientation-horizontal', + 'glyph-orientation-vertical', + 'image-rendering', + 'kerning', + 'letter-spacing', + 'lighting-color', + 'marker', + 'marker-end', + 'marker-mid', + 'marker-start', + 'mask', + 'opacity', + 'overflow', + 'pointer-events', + 'shape-rendering', + 'stop-color', + 'stop-opacity', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'text-anchor', + 'text-decoration', + 'text-rendering', + 'unicode-bidi', + 'visibility', + 'word-spacing', + 'writing-mode', + # SVG 1.2 Tiny + 'audio-level', + 'buffered-rendering', + 'display-align', + 'line-increment', + 'solid-color', + 'solid-opacity', + 'text-align', + 'vector-effect', + 'viewport-fill', + 'viewport-fill-opacity', +] colors = { - 'aliceblue': 'rgb(240, 248, 255)', - 'antiquewhite': 'rgb(250, 235, 215)', - 'aqua': 'rgb( 0, 255, 255)', - 'aquamarine': 'rgb(127, 255, 212)', - 'azure': 'rgb(240, 255, 255)', - 'beige': 'rgb(245, 245, 220)', - 'bisque': 'rgb(255, 228, 196)', - 'black': 'rgb( 0, 0, 0)', - 'blanchedalmond': 'rgb(255, 235, 205)', - 'blue': 'rgb( 0, 0, 255)', - 'blueviolet': 'rgb(138, 43, 226)', - 'brown': 'rgb(165, 42, 42)', - 'burlywood': 'rgb(222, 184, 135)', - 'cadetblue': 'rgb( 95, 158, 160)', - 'chartreuse': 'rgb(127, 255, 0)', - 'chocolate': 'rgb(210, 105, 30)', - 'coral': 'rgb(255, 127, 80)', - 'cornflowerblue': 'rgb(100, 149, 237)', - 'cornsilk': 'rgb(255, 248, 220)', - 'crimson': 'rgb(220, 20, 60)', - 'cyan': 'rgb( 0, 255, 255)', - 'darkblue': 'rgb( 0, 0, 139)', - 'darkcyan': 'rgb( 0, 139, 139)', - 'darkgoldenrod': 'rgb(184, 134, 11)', - 'darkgray': 'rgb(169, 169, 169)', - 'darkgreen': 'rgb( 0, 100, 0)', - 'darkgrey': 'rgb(169, 169, 169)', - 'darkkhaki': 'rgb(189, 183, 107)', - 'darkmagenta': 'rgb(139, 0, 139)', - 'darkolivegreen': 'rgb( 85, 107, 47)', - 'darkorange': 'rgb(255, 140, 0)', - 'darkorchid': 'rgb(153, 50, 204)', - 'darkred': 'rgb(139, 0, 0)', - 'darksalmon': 'rgb(233, 150, 122)', - 'darkseagreen': 'rgb(143, 188, 143)', - 'darkslateblue': 'rgb( 72, 61, 139)', - 'darkslategray': 'rgb( 47, 79, 79)', - 'darkslategrey': 'rgb( 47, 79, 79)', - 'darkturquoise': 'rgb( 0, 206, 209)', - 'darkviolet': 'rgb(148, 0, 211)', - 'deeppink': 'rgb(255, 20, 147)', - 'deepskyblue': 'rgb( 0, 191, 255)', - 'dimgray': 'rgb(105, 105, 105)', - 'dimgrey': 'rgb(105, 105, 105)', - 'dodgerblue': 'rgb( 30, 144, 255)', - 'firebrick': 'rgb(178, 34, 34)', - 'floralwhite': 'rgb(255, 250, 240)', - 'forestgreen': 'rgb( 34, 139, 34)', - 'fuchsia': 'rgb(255, 0, 255)', - 'gainsboro': 'rgb(220, 220, 220)', - 'ghostwhite': 'rgb(248, 248, 255)', - 'gold': 'rgb(255, 215, 0)', - 'goldenrod': 'rgb(218, 165, 32)', - 'gray': 'rgb(128, 128, 128)', - 'grey': 'rgb(128, 128, 128)', - 'green': 'rgb( 0, 128, 0)', - 'greenyellow': 'rgb(173, 255, 47)', - 'honeydew': 'rgb(240, 255, 240)', - 'hotpink': 'rgb(255, 105, 180)', - 'indianred': 'rgb(205, 92, 92)', - 'indigo': 'rgb( 75, 0, 130)', - 'ivory': 'rgb(255, 255, 240)', - 'khaki': 'rgb(240, 230, 140)', - 'lavender': 'rgb(230, 230, 250)', - 'lavenderblush': 'rgb(255, 240, 245)', - 'lawngreen': 'rgb(124, 252, 0)', - 'lemonchiffon': 'rgb(255, 250, 205)', - 'lightblue': 'rgb(173, 216, 230)', - 'lightcoral': 'rgb(240, 128, 128)', - 'lightcyan': 'rgb(224, 255, 255)', - 'lightgoldenrodyellow': 'rgb(250, 250, 210)', - 'lightgray': 'rgb(211, 211, 211)', - 'lightgreen': 'rgb(144, 238, 144)', - 'lightgrey': 'rgb(211, 211, 211)', - 'lightpink': 'rgb(255, 182, 193)', - 'lightsalmon': 'rgb(255, 160, 122)', - 'lightseagreen': 'rgb( 32, 178, 170)', - 'lightskyblue': 'rgb(135, 206, 250)', - 'lightslategray': 'rgb(119, 136, 153)', - 'lightslategrey': 'rgb(119, 136, 153)', - 'lightsteelblue': 'rgb(176, 196, 222)', - 'lightyellow': 'rgb(255, 255, 224)', - 'lime': 'rgb( 0, 255, 0)', - 'limegreen': 'rgb( 50, 205, 50)', - 'linen': 'rgb(250, 240, 230)', - 'magenta': 'rgb(255, 0, 255)', - 'maroon': 'rgb(128, 0, 0)', - 'mediumaquamarine': 'rgb(102, 205, 170)', - 'mediumblue': 'rgb( 0, 0, 205)', - 'mediumorchid': 'rgb(186, 85, 211)', - 'mediumpurple': 'rgb(147, 112, 219)', - 'mediumseagreen': 'rgb( 60, 179, 113)', - 'mediumslateblue': 'rgb(123, 104, 238)', - 'mediumspringgreen': 'rgb( 0, 250, 154)', - 'mediumturquoise': 'rgb( 72, 209, 204)', - 'mediumvioletred': 'rgb(199, 21, 133)', - 'midnightblue': 'rgb( 25, 25, 112)', - 'mintcream': 'rgb(245, 255, 250)', - 'mistyrose': 'rgb(255, 228, 225)', - 'moccasin': 'rgb(255, 228, 181)', - 'navajowhite': 'rgb(255, 222, 173)', - 'navy': 'rgb( 0, 0, 128)', - 'oldlace': 'rgb(253, 245, 230)', - 'olive': 'rgb(128, 128, 0)', - 'olivedrab': 'rgb(107, 142, 35)', - 'orange': 'rgb(255, 165, 0)', - 'orangered': 'rgb(255, 69, 0)', - 'orchid': 'rgb(218, 112, 214)', - 'palegoldenrod': 'rgb(238, 232, 170)', - 'palegreen': 'rgb(152, 251, 152)', - 'paleturquoise': 'rgb(175, 238, 238)', - 'palevioletred': 'rgb(219, 112, 147)', - 'papayawhip': 'rgb(255, 239, 213)', - 'peachpuff': 'rgb(255, 218, 185)', - 'peru': 'rgb(205, 133, 63)', - 'pink': 'rgb(255, 192, 203)', - 'plum': 'rgb(221, 160, 221)', - 'powderblue': 'rgb(176, 224, 230)', - 'purple': 'rgb(128, 0, 128)', - 'red': 'rgb(255, 0, 0)', - 'rosybrown': 'rgb(188, 143, 143)', - 'royalblue': 'rgb( 65, 105, 225)', - 'saddlebrown': 'rgb(139, 69, 19)', - 'salmon': 'rgb(250, 128, 114)', - 'sandybrown': 'rgb(244, 164, 96)', - 'seagreen': 'rgb( 46, 139, 87)', - 'seashell': 'rgb(255, 245, 238)', - 'sienna': 'rgb(160, 82, 45)', - 'silver': 'rgb(192, 192, 192)', - 'skyblue': 'rgb(135, 206, 235)', - 'slateblue': 'rgb(106, 90, 205)', - 'slategray': 'rgb(112, 128, 144)', - 'slategrey': 'rgb(112, 128, 144)', - 'snow': 'rgb(255, 250, 250)', - 'springgreen': 'rgb( 0, 255, 127)', - 'steelblue': 'rgb( 70, 130, 180)', - 'tan': 'rgb(210, 180, 140)', - 'teal': 'rgb( 0, 128, 128)', - 'thistle': 'rgb(216, 191, 216)', - 'tomato': 'rgb(255, 99, 71)', - 'turquoise': 'rgb( 64, 224, 208)', - 'violet': 'rgb(238, 130, 238)', - 'wheat': 'rgb(245, 222, 179)', - 'white': 'rgb(255, 255, 255)', - 'whitesmoke': 'rgb(245, 245, 245)', - 'yellow': 'rgb(255, 255, 0)', - 'yellowgreen': 'rgb(154, 205, 50)', - } + 'aliceblue': 'rgb(240, 248, 255)', + 'antiquewhite': 'rgb(250, 235, 215)', + 'aqua': 'rgb( 0, 255, 255)', + 'aquamarine': 'rgb(127, 255, 212)', + 'azure': 'rgb(240, 255, 255)', + 'beige': 'rgb(245, 245, 220)', + 'bisque': 'rgb(255, 228, 196)', + 'black': 'rgb( 0, 0, 0)', + 'blanchedalmond': 'rgb(255, 235, 205)', + 'blue': 'rgb( 0, 0, 255)', + 'blueviolet': 'rgb(138, 43, 226)', + 'brown': 'rgb(165, 42, 42)', + 'burlywood': 'rgb(222, 184, 135)', + 'cadetblue': 'rgb( 95, 158, 160)', + 'chartreuse': 'rgb(127, 255, 0)', + 'chocolate': 'rgb(210, 105, 30)', + 'coral': 'rgb(255, 127, 80)', + 'cornflowerblue': 'rgb(100, 149, 237)', + 'cornsilk': 'rgb(255, 248, 220)', + 'crimson': 'rgb(220, 20, 60)', + 'cyan': 'rgb( 0, 255, 255)', + 'darkblue': 'rgb( 0, 0, 139)', + 'darkcyan': 'rgb( 0, 139, 139)', + 'darkgoldenrod': 'rgb(184, 134, 11)', + 'darkgray': 'rgb(169, 169, 169)', + 'darkgreen': 'rgb( 0, 100, 0)', + 'darkgrey': 'rgb(169, 169, 169)', + 'darkkhaki': 'rgb(189, 183, 107)', + 'darkmagenta': 'rgb(139, 0, 139)', + 'darkolivegreen': 'rgb( 85, 107, 47)', + 'darkorange': 'rgb(255, 140, 0)', + 'darkorchid': 'rgb(153, 50, 204)', + 'darkred': 'rgb(139, 0, 0)', + 'darksalmon': 'rgb(233, 150, 122)', + 'darkseagreen': 'rgb(143, 188, 143)', + 'darkslateblue': 'rgb( 72, 61, 139)', + 'darkslategray': 'rgb( 47, 79, 79)', + 'darkslategrey': 'rgb( 47, 79, 79)', + 'darkturquoise': 'rgb( 0, 206, 209)', + 'darkviolet': 'rgb(148, 0, 211)', + 'deeppink': 'rgb(255, 20, 147)', + 'deepskyblue': 'rgb( 0, 191, 255)', + 'dimgray': 'rgb(105, 105, 105)', + 'dimgrey': 'rgb(105, 105, 105)', + 'dodgerblue': 'rgb( 30, 144, 255)', + 'firebrick': 'rgb(178, 34, 34)', + 'floralwhite': 'rgb(255, 250, 240)', + 'forestgreen': 'rgb( 34, 139, 34)', + 'fuchsia': 'rgb(255, 0, 255)', + 'gainsboro': 'rgb(220, 220, 220)', + 'ghostwhite': 'rgb(248, 248, 255)', + 'gold': 'rgb(255, 215, 0)', + 'goldenrod': 'rgb(218, 165, 32)', + 'gray': 'rgb(128, 128, 128)', + 'grey': 'rgb(128, 128, 128)', + 'green': 'rgb( 0, 128, 0)', + 'greenyellow': 'rgb(173, 255, 47)', + 'honeydew': 'rgb(240, 255, 240)', + 'hotpink': 'rgb(255, 105, 180)', + 'indianred': 'rgb(205, 92, 92)', + 'indigo': 'rgb( 75, 0, 130)', + 'ivory': 'rgb(255, 255, 240)', + 'khaki': 'rgb(240, 230, 140)', + 'lavender': 'rgb(230, 230, 250)', + 'lavenderblush': 'rgb(255, 240, 245)', + 'lawngreen': 'rgb(124, 252, 0)', + 'lemonchiffon': 'rgb(255, 250, 205)', + 'lightblue': 'rgb(173, 216, 230)', + 'lightcoral': 'rgb(240, 128, 128)', + 'lightcyan': 'rgb(224, 255, 255)', + 'lightgoldenrodyellow': 'rgb(250, 250, 210)', + 'lightgray': 'rgb(211, 211, 211)', + 'lightgreen': 'rgb(144, 238, 144)', + 'lightgrey': 'rgb(211, 211, 211)', + 'lightpink': 'rgb(255, 182, 193)', + 'lightsalmon': 'rgb(255, 160, 122)', + 'lightseagreen': 'rgb( 32, 178, 170)', + 'lightskyblue': 'rgb(135, 206, 250)', + 'lightslategray': 'rgb(119, 136, 153)', + 'lightslategrey': 'rgb(119, 136, 153)', + 'lightsteelblue': 'rgb(176, 196, 222)', + 'lightyellow': 'rgb(255, 255, 224)', + 'lime': 'rgb( 0, 255, 0)', + 'limegreen': 'rgb( 50, 205, 50)', + 'linen': 'rgb(250, 240, 230)', + 'magenta': 'rgb(255, 0, 255)', + 'maroon': 'rgb(128, 0, 0)', + 'mediumaquamarine': 'rgb(102, 205, 170)', + 'mediumblue': 'rgb( 0, 0, 205)', + 'mediumorchid': 'rgb(186, 85, 211)', + 'mediumpurple': 'rgb(147, 112, 219)', + 'mediumseagreen': 'rgb( 60, 179, 113)', + 'mediumslateblue': 'rgb(123, 104, 238)', + 'mediumspringgreen': 'rgb( 0, 250, 154)', + 'mediumturquoise': 'rgb( 72, 209, 204)', + 'mediumvioletred': 'rgb(199, 21, 133)', + 'midnightblue': 'rgb( 25, 25, 112)', + 'mintcream': 'rgb(245, 255, 250)', + 'mistyrose': 'rgb(255, 228, 225)', + 'moccasin': 'rgb(255, 228, 181)', + 'navajowhite': 'rgb(255, 222, 173)', + 'navy': 'rgb( 0, 0, 128)', + 'oldlace': 'rgb(253, 245, 230)', + 'olive': 'rgb(128, 128, 0)', + 'olivedrab': 'rgb(107, 142, 35)', + 'orange': 'rgb(255, 165, 0)', + 'orangered': 'rgb(255, 69, 0)', + 'orchid': 'rgb(218, 112, 214)', + 'palegoldenrod': 'rgb(238, 232, 170)', + 'palegreen': 'rgb(152, 251, 152)', + 'paleturquoise': 'rgb(175, 238, 238)', + 'palevioletred': 'rgb(219, 112, 147)', + 'papayawhip': 'rgb(255, 239, 213)', + 'peachpuff': 'rgb(255, 218, 185)', + 'peru': 'rgb(205, 133, 63)', + 'pink': 'rgb(255, 192, 203)', + 'plum': 'rgb(221, 160, 221)', + 'powderblue': 'rgb(176, 224, 230)', + 'purple': 'rgb(128, 0, 128)', + 'red': 'rgb(255, 0, 0)', + 'rosybrown': 'rgb(188, 143, 143)', + 'royalblue': 'rgb( 65, 105, 225)', + 'saddlebrown': 'rgb(139, 69, 19)', + 'salmon': 'rgb(250, 128, 114)', + 'sandybrown': 'rgb(244, 164, 96)', + 'seagreen': 'rgb( 46, 139, 87)', + 'seashell': 'rgb(255, 245, 238)', + 'sienna': 'rgb(160, 82, 45)', + 'silver': 'rgb(192, 192, 192)', + 'skyblue': 'rgb(135, 206, 235)', + 'slateblue': 'rgb(106, 90, 205)', + 'slategray': 'rgb(112, 128, 144)', + 'slategrey': 'rgb(112, 128, 144)', + 'snow': 'rgb(255, 250, 250)', + 'springgreen': 'rgb( 0, 255, 127)', + 'steelblue': 'rgb( 70, 130, 180)', + 'tan': 'rgb(210, 180, 140)', + 'teal': 'rgb( 0, 128, 128)', + 'thistle': 'rgb(216, 191, 216)', + 'tomato': 'rgb(255, 99, 71)', + 'turquoise': 'rgb( 64, 224, 208)', + 'violet': 'rgb(238, 130, 238)', + 'wheat': 'rgb(245, 222, 179)', + 'white': 'rgb(255, 255, 255)', + 'whitesmoke': 'rgb(245, 245, 245)', + 'yellow': 'rgb(255, 255, 0)', + 'yellowgreen': 'rgb(154, 205, 50)', +} # A list of default poperties that are safe to remove # @@ -341,279 +341,287 @@ colors = { # https://www.w3.org/TR/SVGTiny12/attributeTable.html (implemented) # https://www.w3.org/TR/SVG2/propidx.html (not yet implemented) # -default_properties = { # excluded all properties with 'auto' as default - # SVG 1.1 presentation attributes - 'baseline-shift': 'baseline', - 'clip-path': 'none', - 'clip-rule': 'nonzero', - 'color': '#000', - 'color-interpolation-filters': 'linearRGB', - 'color-interpolation': 'sRGB', - 'direction': 'ltr', - 'display': 'inline', - 'enable-background': 'accumulate', - 'fill': '#000', - 'fill-opacity': '1', - 'fill-rule': 'nonzero', - 'filter': 'none', - 'flood-color': '#000', - 'flood-opacity': '1', - 'font-size-adjust': 'none', - 'font-size': 'medium', - 'font-stretch': 'normal', - 'font-style': 'normal', - 'font-variant': 'normal', - 'font-weight': 'normal', - 'glyph-orientation-horizontal': '0deg', - 'letter-spacing': 'normal', - 'lighting-color': '#fff', - 'marker': 'none', - 'marker-start': 'none', - 'marker-mid': 'none', - 'marker-end': 'none', - 'mask': 'none', - 'opacity': '1', - 'pointer-events': 'visiblePainted', - 'stop-color': '#000', - 'stop-opacity': '1', - 'stroke': 'none', - 'stroke-dasharray': 'none', - 'stroke-dashoffset': '0', - 'stroke-linecap': 'butt', - 'stroke-linejoin': 'miter', - 'stroke-miterlimit': '4', - 'stroke-opacity': '1', - 'stroke-width': '1', - 'text-anchor': 'start', - 'text-decoration': 'none', - 'unicode-bidi': 'normal', - 'visibility': 'visible', - 'word-spacing': 'normal', - 'writing-mode': 'lr-tb', - # SVG 1.2 tiny properties - 'audio-level': '1', - 'solid-color': '#000', - 'solid-opacity': '1', - 'text-align': 'start', - 'vector-effect': 'none', - 'viewport-fill': 'none', - 'viewport-fill-opacity': '1', - } +default_properties = { # excluded all properties with 'auto' as default + # SVG 1.1 presentation attributes + 'baseline-shift': 'baseline', + 'clip-path': 'none', + 'clip-rule': 'nonzero', + 'color': '#000', + 'color-interpolation-filters': 'linearRGB', + 'color-interpolation': 'sRGB', + 'direction': 'ltr', + 'display': 'inline', + 'enable-background': 'accumulate', + 'fill': '#000', + 'fill-opacity': '1', + 'fill-rule': 'nonzero', + 'filter': 'none', + 'flood-color': '#000', + 'flood-opacity': '1', + 'font-size-adjust': 'none', + 'font-size': 'medium', + 'font-stretch': 'normal', + 'font-style': 'normal', + 'font-variant': 'normal', + 'font-weight': 'normal', + 'glyph-orientation-horizontal': '0deg', + 'letter-spacing': 'normal', + 'lighting-color': '#fff', + 'marker': 'none', + 'marker-start': 'none', + 'marker-mid': 'none', + 'marker-end': 'none', + 'mask': 'none', + 'opacity': '1', + 'pointer-events': 'visiblePainted', + 'stop-color': '#000', + 'stop-opacity': '1', + 'stroke': 'none', + 'stroke-dasharray': 'none', + 'stroke-dashoffset': '0', + 'stroke-linecap': 'butt', + 'stroke-linejoin': 'miter', + 'stroke-miterlimit': '4', + 'stroke-opacity': '1', + 'stroke-width': '1', + 'text-anchor': 'start', + 'text-decoration': 'none', + 'unicode-bidi': 'normal', + 'visibility': 'visible', + 'word-spacing': 'normal', + 'writing-mode': 'lr-tb', + # SVG 1.2 tiny properties + 'audio-level': '1', + 'solid-color': '#000', + 'solid-opacity': '1', + 'text-align': 'start', + 'vector-effect': 'none', + 'viewport-fill': 'none', + 'viewport-fill-opacity': '1', +} -def isSameSign(a,b): return (a <= 0 and b <= 0) or (a >= 0 and b >= 0) + +def isSameSign(a, b): return (a <= 0 and b <= 0) or (a >= 0 and b >= 0) scinumber = re.compile(r"[-+]?(\d*\.?)?\d+[eE][-+]?\d+") number = re.compile(r"[-+]?(\d*\.?)?\d+") sciExponent = re.compile(r"[eE]([-+]?\d+)") unit = re.compile("(em|ex|px|pt|pc|cm|mm|in|%){1,1}$") + class Unit(object): - # Integer constants for units. - INVALID = -1 - NONE = 0 - PCT = 1 - PX = 2 - PT = 3 - PC = 4 - EM = 5 - EX = 6 - CM = 7 - MM = 8 - IN = 9 + # Integer constants for units. + INVALID = -1 + NONE = 0 + PCT = 1 + PX = 2 + PT = 3 + PC = 4 + EM = 5 + EX = 6 + CM = 7 + MM = 8 + IN = 9 - # String to Unit. Basically, converts unit strings to their integer constants. - s2u = { - '': NONE, - '%': PCT, - 'px': PX, - 'pt': PT, - 'pc': PC, - 'em': EM, - 'ex': EX, - 'cm': CM, - 'mm': MM, - 'in': IN, - } + # String to Unit. Basically, converts unit strings to their integer constants. + s2u = { + '': NONE, + '%': PCT, + 'px': PX, + 'pt': PT, + 'pc': PC, + 'em': EM, + 'ex': EX, + 'cm': CM, + 'mm': MM, + 'in': IN, + } - # Unit to String. Basically, converts unit integer constants to their corresponding strings. - u2s = { - NONE: '', - PCT: '%', - PX: 'px', - PT: 'pt', - PC: 'pc', - EM: 'em', - EX: 'ex', - CM: 'cm', - MM: 'mm', - IN: 'in', - } + # Unit to String. Basically, converts unit integer constants to their corresponding strings. + u2s = { + NONE: '', + PCT: '%', + PX: 'px', + PT: 'pt', + PC: 'pc', + EM: 'em', + EX: 'ex', + CM: 'cm', + MM: 'mm', + IN: 'in', + } # @staticmethod - def get(unitstr): - if unitstr is None: return Unit.NONE - try: - return Unit.s2u[unitstr] - except KeyError: - return Unit.INVALID + def get(unitstr): + if unitstr is None: + return Unit.NONE + try: + return Unit.s2u[unitstr] + except KeyError: + return Unit.INVALID # @staticmethod - def str(unitint): - try: - return Unit.u2s[unitint] - except KeyError: - return 'INVALID' + def str(unitint): + try: + return Unit.u2s[unitint] + except KeyError: + return 'INVALID' + + get = staticmethod(get) + str = staticmethod(str) - get = staticmethod(get) - str = staticmethod(str) class SVGLength(object): - def __init__(self, str): - try: # simple unitless and no scientific notation - self.value = float(str) - if int(self.value) == self.value: - self.value = int(self.value) - self.units = Unit.NONE - except ValueError: - # we know that the length string has an exponent, a unit, both or is invalid - # parse out number, exponent and unit - self.value = 0 - unitBegin = 0 - scinum = scinumber.match(str) - if scinum != None: - # this will always match, no need to check it - numMatch = number.match(str) - expMatch = sciExponent.search(str, numMatch.start(0)) - self.value = (float(numMatch.group(0)) * - 10 ** float(expMatch.group(1))) - unitBegin = expMatch.end(1) - else: - # unit or invalid - numMatch = number.match(str) - if numMatch != None: - self.value = float(numMatch.group(0)) - unitBegin = numMatch.end(0) + def __init__(self, str): + try: # simple unitless and no scientific notation + self.value = float(str) + if int(self.value) == self.value: + self.value = int(self.value) + self.units = Unit.NONE + except ValueError: + # we know that the length string has an exponent, a unit, both or is invalid - if int(self.value) == self.value: - self.value = int(self.value) - - if unitBegin != 0: - unitMatch = unit.search(str, unitBegin) - if unitMatch != None: - self.units = Unit.get(unitMatch.group(0)) - - # invalid - else: - # TODO: this needs to set the default for the given attribute (how?) + # parse out number, exponent and unit self.value = 0 - self.units = Unit.INVALID + unitBegin = 0 + scinum = scinumber.match(str) + if scinum != None: + # this will always match, no need to check it + numMatch = number.match(str) + expMatch = sciExponent.search(str, numMatch.start(0)) + self.value = (float(numMatch.group(0)) * + 10 ** float(expMatch.group(1))) + unitBegin = expMatch.end(1) + else: + # unit or invalid + numMatch = number.match(str) + if numMatch != None: + self.value = float(numMatch.group(0)) + unitBegin = numMatch.end(0) + + if int(self.value) == self.value: + self.value = int(self.value) + + if unitBegin != 0: + unitMatch = unit.search(str, unitBegin) + if unitMatch != None: + self.units = Unit.get(unitMatch.group(0)) + + # invalid + else: + # TODO: this needs to set the default for the given attribute (how?) + self.value = 0 + self.units = Unit.INVALID + def findElementsWithId(node, elems=None): - """ - Returns all elements with id attributes - """ - if elems is None: - elems = {} - id = node.getAttribute('id') - if id != '': - elems[id] = node - if node.hasChildNodes(): - for child in node.childNodes: - # from http://www.w3.org/TR/DOM-Level-2-Core/idl-definitions.html - # we are only really interested in nodes of type Element (1) - if child.nodeType == 1: - findElementsWithId(child, elems) - return elems + """ + Returns all elements with id attributes + """ + if elems is None: + elems = {} + id = node.getAttribute('id') + if id != '': + elems[id] = node + if node.hasChildNodes(): + for child in node.childNodes: + # from http://www.w3.org/TR/DOM-Level-2-Core/idl-definitions.html + # we are only really interested in nodes of type Element (1) + if child.nodeType == 1: + findElementsWithId(child, elems) + return elems referencingProps = ['fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', - 'marker-end', 'marker-mid'] + 'marker-end', 'marker-mid'] + def findReferencedElements(node, ids=None): - """ - Returns the number of times an ID is referenced as well as all elements - that reference it. node is the node at which to start the search. The - return value is a map which has the id as key and each value is an array - where the first value is a count and the second value is a list of nodes - that referenced it. + """ + Returns the number of times an ID is referenced as well as all elements + that reference it. node is the node at which to start the search. The + return value is a map which has the id as key and each value is an array + where the first value is a count and the second value is a list of nodes + that referenced it. - Currently looks at fill, stroke, clip-path, mask, marker, and - xlink:href attributes. - """ - global referencingProps - if ids is None: - ids = {} - # TODO: input argument ids is clunky here (see below how it is called) - # GZ: alternative to passing dict, use **kwargs + Currently looks at fill, stroke, clip-path, mask, marker, and + xlink:href attributes. + """ + global referencingProps + if ids is None: + ids = {} + # TODO: input argument ids is clunky here (see below how it is called) + # GZ: alternative to passing dict, use **kwargs - # if this node is a style element, parse its text into CSS - if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: - # one stretch of text, please! (we could use node.normalize(), but - # this actually modifies the node, and we don't want to keep - # whitespace around if there's any) - stylesheet = "".join([child.nodeValue for child in node.childNodes]) - if stylesheet != '': - cssRules = parseCssString(stylesheet) - for rule in cssRules: - for propname in rule['properties']: - propval = rule['properties'][propname] - findReferencingProperty(node, propname, propval, ids) - return ids + # if this node is a style element, parse its text into CSS + if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: + # one stretch of text, please! (we could use node.normalize(), but + # this actually modifies the node, and we don't want to keep + # whitespace around if there's any) + stylesheet = "".join([child.nodeValue for child in node.childNodes]) + if stylesheet != '': + cssRules = parseCssString(stylesheet) + for rule in cssRules: + for propname in rule['properties']: + propval = rule['properties'][propname] + findReferencingProperty(node, propname, propval, ids) + return ids - # else if xlink:href is set, then grab the id - href = node.getAttributeNS(NS['XLINK'],'href') - if href != '' and len(href) > 1 and href[0] == '#': - # we remove the hash mark from the beginning of the id - id = href[1:] - if id in ids: - ids[id][0] += 1 - ids[id][1].append(node) - else: - ids[id] = [1,[node]] - - # now get all style properties and the fill, stroke, filter attributes - styles = node.getAttribute('style').split(';') - for attr in referencingProps: - styles.append(':'.join([attr, node.getAttribute(attr)])) - - for style in styles: - propval = style.split(':') - if len(propval) == 2: - prop = propval[0].strip() - val = propval[1].strip() - findReferencingProperty(node, prop, val, ids) - - if node.hasChildNodes(): - for child in node.childNodes: - if child.nodeType == 1: - findReferencedElements(child, ids) - return ids - -def findReferencingProperty(node, prop, val, ids): - global referencingProps - if prop in referencingProps and val != '': - if len(val) >= 7 and val[0:5] == 'url(#': - id = val[5:val.find(')')] - if id in ids: + # else if xlink:href is set, then grab the id + href = node.getAttributeNS(NS['XLINK'], 'href') + if href != '' and len(href) > 1 and href[0] == '#': + # we remove the hash mark from the beginning of the id + id = href[1:] + if id in ids: ids[id][0] += 1 ids[id][1].append(node) - else: - ids[id] = [1,[node]] - # if the url has a quote in it, we need to compensate - elif len(val) >= 8: - id = None - # double-quote - if val[0:6] == 'url("#': - id = val[6:val.find('")')] - # single-quote - elif val[0:6] == "url('#": - id = val[6:val.find("')")] - if id != None: + else: + ids[id] = [1, [node]] + + # now get all style properties and the fill, stroke, filter attributes + styles = node.getAttribute('style').split(';') + for attr in referencingProps: + styles.append(':'.join([attr, node.getAttribute(attr)])) + + for style in styles: + propval = style.split(':') + if len(propval) == 2: + prop = propval[0].strip() + val = propval[1].strip() + findReferencingProperty(node, prop, val, ids) + + if node.hasChildNodes(): + for child in node.childNodes: + if child.nodeType == 1: + findReferencedElements(child, ids) + return ids + + +def findReferencingProperty(node, prop, val, ids): + global referencingProps + if prop in referencingProps and val != '': + if len(val) >= 7 and val[0:5] == 'url(#': + id = val[5:val.find(')')] if id in ids: - ids[id][0] += 1 - ids[id][1].append(node) + ids[id][0] += 1 + ids[id][1].append(node) else: - ids[id] = [1,[node]] + ids[id] = [1, [node]] + # if the url has a quote in it, we need to compensate + elif len(val) >= 8: + id = None + # double-quote + if val[0:6] == 'url("#': + id = val[6:val.find('")')] + # single-quote + elif val[0:6] == "url('#": + id = val[6:val.find("')")] + if id != None: + if id in ids: + ids[id][0] += 1 + ids[id][1].append(node) + else: + ids[id] = [1, [node]] numIDsRemoved = 0 numElemsRemoved = 0 @@ -629,1059 +637,1094 @@ numBytesSavedInTransforms = 0 numPointsRemovedFromPolygon = 0 numCommentBytes = 0 + def removeUnusedDefs(doc, defElem, elemsToRemove=None): - if elemsToRemove is None: - elemsToRemove = [] + if elemsToRemove is None: + elemsToRemove = [] - identifiedElements = findElementsWithId(doc.documentElement) - referencedIDs = findReferencedElements(doc.documentElement) + identifiedElements = findElementsWithId(doc.documentElement) + referencedIDs = findReferencedElements(doc.documentElement) - keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] - for elem in defElem.childNodes: - # only look at it if an element and not referenced anywhere else - if elem.nodeType == 1 and (elem.getAttribute('id') == '' or \ - (not elem.getAttribute('id') in referencedIDs)): + keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] + for elem in defElem.childNodes: + # only look at it if an element and not referenced anywhere else + if elem.nodeType == 1 and (elem.getAttribute('id') == '' or + (not elem.getAttribute('id') in referencedIDs)): + + # we only inspect the children of a group in a defs if the group + # is not referenced anywhere else + if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: + elemsToRemove = removeUnusedDefs(doc, elem, elemsToRemove) + # we only remove if it is not one of our tags we always keep (see above) + elif not elem.nodeName in keepTags: + elemsToRemove.append(elem) + return elemsToRemove - # we only inspect the children of a group in a defs if the group - # is not referenced anywhere else - if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: - elemsToRemove = removeUnusedDefs(doc, elem, elemsToRemove) - # we only remove if it is not one of our tags we always keep (see above) - elif not elem.nodeName in keepTags: - elemsToRemove.append(elem) - return elemsToRemove def removeUnreferencedElements(doc, keepDefs): - """ - Removes all unreferenced elements except for , , , , and . - Also vacuums the defs of any non-referenced renderable elements. + """ + Removes all unreferenced elements except for , , , , and . + Also vacuums the defs of any non-referenced renderable elements. - Returns the number of unreferenced elements removed from the document. - """ - global numElemsRemoved - num = 0 + Returns the number of unreferenced elements removed from the document. + """ + global numElemsRemoved + num = 0 - # Remove certain unreferenced elements outside of defs - removeTags = ['linearGradient', 'radialGradient', 'pattern'] - identifiedElements = findElementsWithId(doc.documentElement) - referencedIDs = findReferencedElements(doc.documentElement) + # Remove certain unreferenced elements outside of defs + removeTags = ['linearGradient', 'radialGradient', 'pattern'] + identifiedElements = findElementsWithId(doc.documentElement) + referencedIDs = findReferencedElements(doc.documentElement) - for id in identifiedElements: - if not id in referencedIDs: - goner = identifiedElements[id] - if (goner != None and goner.nodeName in removeTags - and goner.parentNode != None - and goner.parentNode.tagName != 'defs'): - goner.parentNode.removeChild(goner) - num += 1 - numElemsRemoved += 1 + for id in identifiedElements: + if not id in referencedIDs: + goner = identifiedElements[id] + if (goner != None and goner.nodeName in removeTags + and goner.parentNode != None + and goner.parentNode.tagName != 'defs'): + goner.parentNode.removeChild(goner) + num += 1 + numElemsRemoved += 1 + + if not keepDefs: + # Remove most unreferenced elements inside defs + defs = doc.documentElement.getElementsByTagName('defs') + for aDef in defs: + elemsToRemove = removeUnusedDefs(doc, aDef) + for elem in elemsToRemove: + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + num += 1 + return num - if not keepDefs: - # Remove most unreferenced elements inside defs - defs = doc.documentElement.getElementsByTagName('defs') - for aDef in defs: - elemsToRemove = removeUnusedDefs(doc, aDef) - for elem in elemsToRemove: - elem.parentNode.removeChild(elem) - numElemsRemoved += 1 - num += 1 - return num def shortenIDs(doc, prefix, unprotectedElements=None): - """ - Shortens ID names used in the document. ID names referenced the most often are assigned the - shortest ID names. - If the list unprotectedElements is provided, only IDs from this list will be shortened. + """ + Shortens ID names used in the document. ID names referenced the most often are assigned the + shortest ID names. + If the list unprotectedElements is provided, only IDs from this list will be shortened. - Returns the number of bytes saved by shortening ID names in the document. - """ - num = 0 + Returns the number of bytes saved by shortening ID names in the document. + """ + num = 0 - identifiedElements = findElementsWithId(doc.documentElement) - if unprotectedElements is None: - unprotectedElements = identifiedElements - referencedIDs = findReferencedElements(doc.documentElement) + identifiedElements = findElementsWithId(doc.documentElement) + if unprotectedElements is None: + unprotectedElements = identifiedElements + referencedIDs = findReferencedElements(doc.documentElement) - # Make idList (list of idnames) sorted by reference count - # descending, so the highest reference count is first. - # First check that there's actually a defining element for the current ID name. - # (Cyn: I've seen documents with #id references but no element with that ID!) - idList = [(referencedIDs[rid][0], rid) for rid in referencedIDs - if rid in unprotectedElements] - idList.sort(reverse=True) - idList = [rid for count, rid in idList] + # Make idList (list of idnames) sorted by reference count + # descending, so the highest reference count is first. + # First check that there's actually a defining element for the current ID name. + # (Cyn: I've seen documents with #id references but no element with that ID!) + idList = [(referencedIDs[rid][0], rid) for rid in referencedIDs + if rid in unprotectedElements] + idList.sort(reverse=True) + idList = [rid for count, rid in idList] - # Add unreferenced IDs to end of idList in arbitrary order - idList.extend([rid for rid in unprotectedElements if not rid in idList]) + # Add unreferenced IDs to end of idList in arbitrary order + idList.extend([rid for rid in unprotectedElements if not rid in idList]) - curIdNum = 1 + curIdNum = 1 - for rid in idList: - curId = intToID(curIdNum, prefix) - # First make sure that *this* element isn't already using - # the ID name we want to give it. - if curId != rid: - # Then, skip ahead if the new ID is already in identifiedElement. - while curId in identifiedElements: - curIdNum += 1 - curId = intToID(curIdNum, prefix) - # Then go rename it. - num += renameID(doc, rid, curId, identifiedElements, referencedIDs) - curIdNum += 1 + for rid in idList: + curId = intToID(curIdNum, prefix) + # First make sure that *this* element isn't already using + # the ID name we want to give it. + if curId != rid: + # Then, skip ahead if the new ID is already in identifiedElement. + while curId in identifiedElements: + curIdNum += 1 + curId = intToID(curIdNum, prefix) + # Then go rename it. + num += renameID(doc, rid, curId, identifiedElements, referencedIDs) + curIdNum += 1 + + return num - return num def intToID(idnum, prefix): - """ - Returns the ID name for the given ID number, spreadsheet-style, i.e. from a to z, - then from aa to az, ba to bz, etc., until zz. - """ - rid = '' + """ + Returns the ID name for the given ID number, spreadsheet-style, i.e. from a to z, + then from aa to az, ba to bz, etc., until zz. + """ + rid = '' - while idnum > 0: - idnum -= 1 - rid = chr((idnum % 26) + ord('a')) + rid - idnum = int(idnum / 26) + while idnum > 0: + idnum -= 1 + rid = chr((idnum % 26) + ord('a')) + rid + idnum = int(idnum / 26) + + return prefix + rid - return prefix + rid def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): - """ - Changes the ID name from idFrom to idTo, on the declaring element - as well as all references in the document doc. + """ + Changes the ID name from idFrom to idTo, on the declaring element + as well as all references in the document doc. - Updates identifiedElements and referencedIDs. - Does not handle the case where idTo is already the ID name - of another element in doc. + Updates identifiedElements and referencedIDs. + Does not handle the case where idTo is already the ID name + of another element in doc. - Returns the number of bytes saved by this replacement. - """ + Returns the number of bytes saved by this replacement. + """ - num = 0 + num = 0 - definingNode = identifiedElements[idFrom] - definingNode.setAttribute("id", idTo) - del identifiedElements[idFrom] - identifiedElements[idTo] = definingNode - num += len(idFrom) - len(idTo) + definingNode = identifiedElements[idFrom] + definingNode.setAttribute("id", idTo) + del identifiedElements[idFrom] + identifiedElements[idTo] = definingNode + num += len(idFrom) - len(idTo) - # Update references to renamed node - referringNodes = referencedIDs.get(idFrom) - if referringNodes is not None: + # Update references to renamed node + referringNodes = referencedIDs.get(idFrom) + if referringNodes is not None: - # Look for the idFrom ID name in each of the referencing elements, - # exactly like findReferencedElements would. - # Cyn: Duplicated processing! + # Look for the idFrom ID name in each of the referencing elements, + # exactly like findReferencedElements would. + # Cyn: Duplicated processing! - for node in referringNodes[1]: - # if this node is a style element, parse its text into CSS - if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: - # node.firstChild will be either a CDATA or a Text node now - if node.firstChild != None: - # concatenate the value of all children, in case - # there's a CDATASection node surrounded by whitespace - # nodes - # (node.normalize() will NOT work here, it only acts on Text nodes) - oldValue = "".join([child.nodeValue for child in node.childNodes]) - # not going to reparse the whole thing - newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url(#'" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url(#"' + idFrom + '")', 'url(#' + idTo + ')') - # and now replace all the children with this new stylesheet. - # again, this is in case the stylesheet was a CDATASection - node.childNodes[:] = [node.ownerDocument.createTextNode(newValue)] - num += len(oldValue) - len(newValue) + for node in referringNodes[1]: + # if this node is a style element, parse its text into CSS + if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: + # node.firstChild will be either a CDATA or a Text node now + if node.firstChild != None: + # concatenate the value of all children, in case + # there's a CDATASection node surrounded by whitespace + # nodes + # (node.normalize() will NOT work here, it only acts on Text nodes) + oldValue = "".join([child.nodeValue for child in node.childNodes]) + # not going to reparse the whole thing + newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url(#'" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url(#"' + idFrom + '")', 'url(#' + idTo + ')') + # and now replace all the children with this new stylesheet. + # again, this is in case the stylesheet was a CDATASection + node.childNodes[:] = [node.ownerDocument.createTextNode(newValue)] + num += len(oldValue) - len(newValue) - # if xlink:href is set to #idFrom, then change the id - href = node.getAttributeNS(NS['XLINK'],'href') - if href == '#' + idFrom: - node.setAttributeNS(NS['XLINK'],'href', '#' + idTo) - num += len(idFrom) - len(idTo) + # if xlink:href is set to #idFrom, then change the id + href = node.getAttributeNS(NS['XLINK'], 'href') + if href == '#' + idFrom: + node.setAttributeNS(NS['XLINK'], 'href', '#' + idTo) + num += len(idFrom) - len(idTo) - # if the style has url(#idFrom), then change the id - styles = node.getAttribute('style') - if styles != '': - newValue = styles.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') - node.setAttribute('style', newValue) - num += len(styles) - len(newValue) + # if the style has url(#idFrom), then change the id + styles = node.getAttribute('style') + if styles != '': + newValue = styles.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') + node.setAttribute('style', newValue) + num += len(styles) - len(newValue) - # now try the fill, stroke, filter attributes - for attr in referencingProps: - oldValue = node.getAttribute(attr) - if oldValue != '': - newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') - newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') - newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') - node.setAttribute(attr, newValue) - num += len(oldValue) - len(newValue) + # now try the fill, stroke, filter attributes + for attr in referencingProps: + oldValue = node.getAttribute(attr) + if oldValue != '': + newValue = oldValue.replace('url(#' + idFrom + ')', 'url(#' + idTo + ')') + newValue = newValue.replace("url('#" + idFrom + "')", 'url(#' + idTo + ')') + newValue = newValue.replace('url("#' + idFrom + '")', 'url(#' + idTo + ')') + node.setAttribute(attr, newValue) + num += len(oldValue) - len(newValue) - del referencedIDs[idFrom] - referencedIDs[idTo] = referringNodes + del referencedIDs[idFrom] + referencedIDs[idTo] = referringNodes + + return num - return num def unprotected_ids(doc, options): - u"""Returns a list of unprotected IDs within the document doc.""" - identifiedElements = findElementsWithId(doc.documentElement) - if not (options.protect_ids_noninkscape or - options.protect_ids_list or - options.protect_ids_prefix): - return identifiedElements - if options.protect_ids_list: - protect_ids_list = options.protect_ids_list.split(",") - if options.protect_ids_prefix: - protect_ids_prefixes = options.protect_ids_prefix.split(",") - for id in list(identifiedElements.keys()): - protected = False - if options.protect_ids_noninkscape and not id[-1].isdigit(): - protected = True - if options.protect_ids_list and id in protect_ids_list: - protected = True - if options.protect_ids_prefix: - for prefix in protect_ids_prefixes: - if id.startswith(prefix): - protected = True - if protected: - del identifiedElements[id] - return identifiedElements + u"""Returns a list of unprotected IDs within the document doc.""" + identifiedElements = findElementsWithId(doc.documentElement) + if not (options.protect_ids_noninkscape or + options.protect_ids_list or + options.protect_ids_prefix): + return identifiedElements + if options.protect_ids_list: + protect_ids_list = options.protect_ids_list.split(",") + if options.protect_ids_prefix: + protect_ids_prefixes = options.protect_ids_prefix.split(",") + for id in list(identifiedElements.keys()): + protected = False + if options.protect_ids_noninkscape and not id[-1].isdigit(): + protected = True + if options.protect_ids_list and id in protect_ids_list: + protected = True + if options.protect_ids_prefix: + for prefix in protect_ids_prefixes: + if id.startswith(prefix): + protected = True + if protected: + del identifiedElements[id] + return identifiedElements + def removeUnreferencedIDs(referencedIDs, identifiedElements): - """ - Removes the unreferenced ID attributes. + """ + Removes the unreferenced ID attributes. + + Returns the number of ID attributes removed + """ + global numIDsRemoved + keepTags = ['font'] + num = 0 + for id in list(identifiedElements.keys()): + node = identifiedElements[id] + if (id in referencedIDs) == False and not node.nodeName in keepTags: + node.removeAttribute('id') + numIDsRemoved += 1 + num += 1 + return num - Returns the number of ID attributes removed - """ - global numIDsRemoved - keepTags = ['font'] - num = 0; - for id in list(identifiedElements.keys()): - node = identifiedElements[id] - if (id in referencedIDs) == False and not node.nodeName in keepTags: - node.removeAttribute('id') - numIDsRemoved += 1 - num += 1 - return num def removeNamespacedAttributes(node, namespaces): - global numAttrsRemoved - num = 0 - if node.nodeType == 1: - # remove all namespace'd attributes from this element - attrList = node.attributes - attrsToRemove = [] - for attrNum in range(attrList.length): - attr = attrList.item(attrNum) - if attr != None and attr.namespaceURI in namespaces: - attrsToRemove.append(attr.nodeName) - for attrName in attrsToRemove: - num += 1 - numAttrsRemoved += 1 - node.removeAttribute(attrName) + global numAttrsRemoved + num = 0 + if node.nodeType == 1: + # remove all namespace'd attributes from this element + attrList = node.attributes + attrsToRemove = [] + for attrNum in range(attrList.length): + attr = attrList.item(attrNum) + if attr != None and attr.namespaceURI in namespaces: + attrsToRemove.append(attr.nodeName) + for attrName in attrsToRemove: + num += 1 + numAttrsRemoved += 1 + node.removeAttribute(attrName) + + # now recurse for children + for child in node.childNodes: + num += removeNamespacedAttributes(child, namespaces) + return num - # now recurse for children - for child in node.childNodes: - num += removeNamespacedAttributes(child, namespaces) - return num def removeNamespacedElements(node, namespaces): - global numElemsRemoved - num = 0 - if node.nodeType == 1: - # remove all namespace'd child nodes from this element - childList = node.childNodes - childrenToRemove = [] - for child in childList: - if child != None and child.namespaceURI in namespaces: - childrenToRemove.append(child) - for child in childrenToRemove: - num += 1 - numElemsRemoved += 1 - node.removeChild(child) - - # now recurse for children - for child in node.childNodes: - num += removeNamespacedElements(child, namespaces) - return num - -def removeDescriptiveElements(doc, options): - elementTypes = [] - if options.remove_descriptive_elements: - elementTypes.extend(("title", "desc", "metadata")) - else: - if options.remove_titles: - elementTypes.append("title") - if options.remove_descriptions: - elementTypes.append("desc") - if options.remove_metadata: - elementTypes.append("metadata") - if not elementTypes: - return - - global numElemsRemoved - num = 0 - elementsToRemove = [] - for elementType in elementTypes: - elementsToRemove.extend(doc.documentElement.getElementsByTagName(elementType)) - - for element in elementsToRemove: - element.parentNode.removeChild(element) - num += 1 - numElemsRemoved += 1 - - return num - -def removeNestedGroups(node): - """ - This walks further and further down the tree, removing groups - which do not have any attributes or a title/desc child and - promoting their children up one level - """ - global numElemsRemoved - num = 0 - - groupsToRemove = [] - # Only consider elements for promotion if this element isn't a . - # (partial fix for bug 594930, required by the SVG spec however) - if not (node.nodeType == 1 and node.nodeName == 'switch'): - for child in node.childNodes: - if child.nodeName == 'g' and child.namespaceURI == NS['SVG'] and len(child.attributes) == 0: - # only collapse group if it does not have a title or desc as a direct descendant, - for grandchild in child.childNodes: - if grandchild.nodeType == 1 and grandchild.namespaceURI == NS['SVG'] and \ - grandchild.nodeName in ['title','desc']: - break - else: - groupsToRemove.append(child) - - for g in groupsToRemove: - while g.childNodes.length > 0: - g.parentNode.insertBefore(g.firstChild, g) - g.parentNode.removeChild(g) - numElemsRemoved += 1 - num += 1 - - # now recurse for children - for child in node.childNodes: - if child.nodeType == 1: - num += removeNestedGroups(child) - return num - -def moveCommonAttributesToParentGroup(elem, referencedElements): - """ - This recursively calls this function on all children of the passed in element - and then iterates over all child elements and removes common inheritable attributes - from the children and places them in the parent group. But only if the parent contains - nothing but element children and whitespace. The attributes are only removed from the - children if the children are not referenced by other elements in the document. - """ - num = 0 - - childElements = [] - # recurse first into the children (depth-first) - for child in elem.childNodes: - if child.nodeType == 1: - # only add and recurse if the child is not referenced elsewhere - if not child.getAttribute('id') in referencedElements: - childElements.append(child) - num += moveCommonAttributesToParentGroup(child, referencedElements) - # else if the parent has non-whitespace text children, do not - # try to move common attributes - elif child.nodeType == 3 and child.nodeValue.strip(): - return num - - # only process the children if there are more than one element - if len(childElements) <= 1: return num - - commonAttrs = {} - # add all inheritable properties of the first child element - # FIXME: Note there is a chance that the first child is a set/animate in which case - # its fill attribute is not what we want to look at, we should look for the first - # non-animate/set element - attrList = childElements[0].attributes - for index in range(attrList.length): - attr = attrList.item(index) - # this is most of the inheritable properties from http://www.w3.org/TR/SVG11/propidx.html - # and http://www.w3.org/TR/SVGTiny12/attributeTable.html - if attr.nodeName in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - # we just add all the attributes from the first child - commonAttrs[attr.nodeName] = attr.nodeValue - - # for each subsequent child element - for childNum in range(len(childElements)): - # skip first child - if childNum == 0: - continue - - child = childElements[childNum] - # if we are on an animateXXX/set element, ignore it (due to the 'fill' attribute) - if child.localName in ['set', 'animate', 'animateColor', 'animateTransform', 'animateMotion']: - continue - - distinctAttrs = [] - # loop through all current 'common' attributes - for name in list(commonAttrs.keys()): - # if this child doesn't match that attribute, schedule it for removal - if child.getAttribute(name) != commonAttrs[name]: - distinctAttrs.append(name) - # remove those attributes which are not common - for name in distinctAttrs: - del commonAttrs[name] - - # commonAttrs now has all the inheritable attributes which are common among all child elements - for name in list(commonAttrs.keys()): - for child in childElements: - child.removeAttribute(name) - elem.setAttribute(name, commonAttrs[name]) - - # update our statistic (we remove N*M attributes and add back in M attributes) - num += (len(childElements)-1) * len(commonAttrs) - return num - -def createGroupsForCommonAttributes(elem): - """ - Creates elements to contain runs of 3 or more - consecutive child elements having at least one common attribute. - - Common attributes are not promoted to the by this function. - This is handled by moveCommonAttributesToParentGroup. - - If all children have a common attribute, an extra is not created. - - This function acts recursively on the given element. - """ - num = 0 - global numElemsRemoved - - # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement - # could be added here - # Cyn: These attributes are the same as in moveAttributesToParentGroup, and must always be - for curAttr in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - # Iterate through the children in reverse order, so item(i) for - # items we have yet to visit still returns the correct nodes. - curChild = elem.childNodes.length - 1 - while curChild >= 0: - childNode = elem.childNodes.item(curChild) - - if childNode.nodeType == 1 and childNode.getAttribute(curAttr) != '' and \ - childNode.nodeName in [ - # only attempt to group elements that the content model allows to be children of a - - # SVG 1.1 (see https://www.w3.org/TR/SVG/struct.html#GElement) - 'animate', 'animateColor', 'animateMotion', 'animateTransform', 'set', # animation elements - 'desc', 'metadata', 'title', # descriptive elements - 'circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect', # shape elements - 'defs', 'g', 'svg', 'symbol', 'use', # structural elements - 'linearGradient', 'radialGradient', # gradient elements - 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', - 'font', 'font-face', 'foreignObject', 'image', 'marker', 'mask', - 'pattern', 'script', 'style', 'switch', 'text', 'view', - - # SVG 1.2 (see https://www.w3.org/TR/SVGTiny12/elementTable.html) - 'animation', 'audio', 'discard', 'handler', 'listener', - 'prefetch', 'solidColor', 'textArea', 'video' - ]: - # We're in a possible run! Track the value and run length. - value = childNode.getAttribute(curAttr) - runStart, runEnd = curChild, curChild - # Run elements includes only element tags, no whitespace/comments/etc. - # Later, we calculate a run length which includes these. - runElements = 1 - - # Backtrack to get all the nodes having the same - # attribute value, preserving any nodes in-between. - while runStart > 0: - nextNode = elem.childNodes.item(runStart - 1) - if nextNode.nodeType == 1: - if nextNode.getAttribute(curAttr) != value: break - else: - runElements += 1 - runStart -= 1 - else: runStart -= 1 - - if runElements >= 3: - # Include whitespace/comment/etc. nodes in the run. - while runEnd < elem.childNodes.length - 1: - if elem.childNodes.item(runEnd + 1).nodeType == 1: break - else: runEnd += 1 - - runLength = runEnd - runStart + 1 - if runLength == elem.childNodes.length: # Every child has this - # If the current parent is a already, - if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: - # do not act altogether on this attribute; all the - # children have it in common. - # Let moveCommonAttributesToParentGroup do it. - curChild = -1 - continue - # otherwise, it might be an element, and - # even if all children have the same attribute value, - # it's going to be worth making the since - # doesn't support attributes like 'stroke'. - # Fall through. - - # Create a element from scratch. - # We need the Document for this. - document = elem.ownerDocument - group = document.createElementNS(NS['SVG'], 'g') - # Move the run of elements to the group. - # a) ADD the nodes to the new group. - group.childNodes[:] = elem.childNodes[runStart:runEnd + 1] - for child in group.childNodes: - child.parentNode = group - # b) REMOVE the nodes from the element. - elem.childNodes[runStart:runEnd + 1] = [] - # Include the group in elem's children. - elem.childNodes.insert(runStart, group) - group.parentNode = elem - num += 1 - curChild = runStart - 1 - numElemsRemoved -= 1 - else: - curChild -= 1 - else: - curChild -= 1 - - # each child gets the same treatment, recursively - for childNode in elem.childNodes: - if childNode.nodeType == 1: - num += createGroupsForCommonAttributes(childNode) - - return num - -def removeUnusedAttributesOnParent(elem): - """ - This recursively calls this function on all children of the element passed in, - then removes any unused attributes on this elem if none of the children inherit it - """ - num = 0 - - childElements = [] - # recurse first into the children (depth-first) - for child in elem.childNodes: - if child.nodeType == 1: - childElements.append(child) - num += removeUnusedAttributesOnParent(child) - - # only process the children if there are more than one element - if len(childElements) <= 1: return num - - # get all attribute values on this parent - attrList = elem.attributes - unusedAttrs = {} - for index in range(attrList.length): - attr = attrList.item(index) - if attr.nodeName in ['clip-rule', - 'display-align', - 'fill', 'fill-opacity', 'fill-rule', - 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', - 'pointer-events', 'shape-rendering', - 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', - 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', - 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', - 'word-spacing', 'writing-mode']: - unusedAttrs[attr.nodeName] = attr.nodeValue - - # for each child, if at least one child inherits the parent's attribute, then remove - for childNum in range(len(childElements)): - child = childElements[childNum] - inheritedAttrs = [] - for name in list(unusedAttrs.keys()): - val = child.getAttribute(name) - if val == '' or val == None or val == 'inherit': - inheritedAttrs.append(name) - for a in inheritedAttrs: - del unusedAttrs[a] - - # unusedAttrs now has all the parent attributes that are unused - for name in list(unusedAttrs.keys()): - elem.removeAttribute(name) - num += 1 - - return num - -def removeDuplicateGradientStops(doc): - global numElemsRemoved - num = 0 - - for gradType in ['linearGradient', 'radialGradient']: - for grad in doc.getElementsByTagName(gradType): - stops = {} - stopsToRemove = [] - for stop in grad.getElementsByTagName('stop'): - # convert percentages into a floating point number - offsetU = SVGLength(stop.getAttribute('offset')) - if offsetU.units == Unit.PCT: - offset = offsetU.value / 100.0 - elif offsetU.units == Unit.NONE: - offset = offsetU.value - else: - offset = 0 - # set the stop offset value to the integer or floating point equivalent - if int(offset) == offset: stop.setAttribute('offset', str(int(offset))) - else: stop.setAttribute('offset', str(offset)) - - color = stop.getAttribute('stop-color') - opacity = stop.getAttribute('stop-opacity') - style = stop.getAttribute('style') - if offset in stops: - oldStop = stops[offset] - if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: - stopsToRemove.append(stop) - stops[offset] = [color, opacity, style] - - for stop in stopsToRemove: - stop.parentNode.removeChild(stop) + global numElemsRemoved + num = 0 + if node.nodeType == 1: + # remove all namespace'd child nodes from this element + childList = node.childNodes + childrenToRemove = [] + for child in childList: + if child != None and child.namespaceURI in namespaces: + childrenToRemove.append(child) + for child in childrenToRemove: num += 1 numElemsRemoved += 1 + node.removeChild(child) - # linear gradients - return num + # now recurse for children + for child in node.childNodes: + num += removeNamespacedElements(child, namespaces) + return num -def collapseSinglyReferencedGradients(doc): - global numElemsRemoved - num = 0 - identifiedElements = findElementsWithId(doc.documentElement) +def removeDescriptiveElements(doc, options): + elementTypes = [] + if options.remove_descriptive_elements: + elementTypes.extend(("title", "desc", "metadata")) + else: + if options.remove_titles: + elementTypes.append("title") + if options.remove_descriptions: + elementTypes.append("desc") + if options.remove_metadata: + elementTypes.append("metadata") + if not elementTypes: + return - # make sure to reset the ref'ed ids for when we are running this in testscour - for rid,nodeCount in six.iteritems(findReferencedElements(doc.documentElement)): - count = nodeCount[0] - nodes = nodeCount[1] - # Make sure that there's actually a defining element for the current ID name. - # (Cyn: I've seen documents with #id references but no element with that ID!) - if count == 1 and rid in identifiedElements: - elem = identifiedElements[rid] - if elem != None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \ - and elem.namespaceURI == NS['SVG']: - # found a gradient that is referenced by only 1 other element - refElem = nodes[0] - if refElem.nodeType == 1 and refElem.nodeName in ['linearGradient', 'radialGradient'] \ - and refElem.namespaceURI == NS['SVG']: - # elem is a gradient referenced by only one other gradient (refElem) + global numElemsRemoved + num = 0 + elementsToRemove = [] + for elementType in elementTypes: + elementsToRemove.extend(doc.documentElement.getElementsByTagName(elementType)) - # add the stops to the referencing gradient (this removes them from elem) - if len(refElem.getElementsByTagName('stop')) == 0: - stopsToAdd = elem.getElementsByTagName('stop') - for stop in stopsToAdd: - refElem.appendChild(stop) + for element in elementsToRemove: + element.parentNode.removeChild(element) + num += 1 + numElemsRemoved += 1 - # adopt the gradientUnits, spreadMethod, gradientTransform attributes if - # they are unspecified on refElem - for attr in ['gradientUnits','spreadMethod','gradientTransform']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) + return num - # if both are radialGradients, adopt elem's fx,fy,cx,cy,r attributes if - # they are unspecified on refElem - if elem.nodeName == 'radialGradient' and refElem.nodeName == 'radialGradient': - for attr in ['fx','fy','cx','cy','r']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) - # if both are linearGradients, adopt elem's x1,y1,x2,y2 attributes if - # they are unspecified on refElem - if elem.nodeName == 'linearGradient' and refElem.nodeName == 'linearGradient': - for attr in ['x1','y1','x2','y2']: - if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': - refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) +def removeNestedGroups(node): + """ + This walks further and further down the tree, removing groups + which do not have any attributes or a title/desc child and + promoting their children up one level + """ + global numElemsRemoved + num = 0 - # now remove the xlink:href from refElem - refElem.removeAttributeNS(NS['XLINK'], 'href') + groupsToRemove = [] + # Only consider elements for promotion if this element isn't a . + # (partial fix for bug 594930, required by the SVG spec however) + if not (node.nodeType == 1 and node.nodeName == 'switch'): + for child in node.childNodes: + if child.nodeName == 'g' and child.namespaceURI == NS['SVG'] and len(child.attributes) == 0: + # only collapse group if it does not have a title or desc as a direct descendant, + for grandchild in child.childNodes: + if grandchild.nodeType == 1 and grandchild.namespaceURI == NS['SVG'] and \ + grandchild.nodeName in ['title', 'desc']: + break + else: + groupsToRemove.append(child) - # now delete elem - elem.parentNode.removeChild(elem) - numElemsRemoved += 1 - num += 1 - return num + for g in groupsToRemove: + while g.childNodes.length > 0: + g.parentNode.insertBefore(g.firstChild, g) + g.parentNode.removeChild(g) + numElemsRemoved += 1 + num += 1 -def removeDuplicateGradients(doc): - global numElemsRemoved - num = 0 + # now recurse for children + for child in node.childNodes: + if child.nodeType == 1: + num += removeNestedGroups(child) + return num - gradientsToRemove = {} - duplicateToMaster = {} - for gradType in ['linearGradient', 'radialGradient']: - grads = doc.getElementsByTagName(gradType) - for grad in grads: - # TODO: should slice grads from 'grad' here to optimize - for ograd in grads: - # do not compare gradient to itself - if grad == ograd: continue +def moveCommonAttributesToParentGroup(elem, referencedElements): + """ + This recursively calls this function on all children of the passed in element + and then iterates over all child elements and removes common inheritable attributes + from the children and places them in the parent group. But only if the parent contains + nothing but element children and whitespace. The attributes are only removed from the + children if the children are not referenced by other elements in the document. + """ + num = 0 - # compare grad to ograd (all properties, then all stops) - # if attributes do not match, go to next gradient - someGradAttrsDoNotMatch = False - for attr in ['gradientUnits','spreadMethod','gradientTransform','x1','y1','x2','y2','cx','cy','fx','fy','r']: - if grad.getAttribute(attr) != ograd.getAttribute(attr): - someGradAttrsDoNotMatch = True - break; + childElements = [] + # recurse first into the children (depth-first) + for child in elem.childNodes: + if child.nodeType == 1: + # only add and recurse if the child is not referenced elsewhere + if not child.getAttribute('id') in referencedElements: + childElements.append(child) + num += moveCommonAttributesToParentGroup(child, referencedElements) + # else if the parent has non-whitespace text children, do not + # try to move common attributes + elif child.nodeType == 3 and child.nodeValue.strip(): + return num - if someGradAttrsDoNotMatch: continue + # only process the children if there are more than one element + if len(childElements) <= 1: + return num - # compare xlink:href values too - if grad.getAttributeNS(NS['XLINK'], 'href') != ograd.getAttributeNS(NS['XLINK'], 'href'): - continue + commonAttrs = {} + # add all inheritable properties of the first child element + # FIXME: Note there is a chance that the first child is a set/animate in which case + # its fill attribute is not what we want to look at, we should look for the first + # non-animate/set element + attrList = childElements[0].attributes + for index in range(attrList.length): + attr = attrList.item(index) + # this is most of the inheritable properties from http://www.w3.org/TR/SVG11/propidx.html + # and http://www.w3.org/TR/SVGTiny12/attributeTable.html + if attr.nodeName in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + # we just add all the attributes from the first child + commonAttrs[attr.nodeName] = attr.nodeValue - # all gradient properties match, now time to compare stops - stops = grad.getElementsByTagName('stop') - ostops = ograd.getElementsByTagName('stop') - - if stops.length != ostops.length: continue - - # now compare stops - stopsNotEqual = False - for i in range(stops.length): - if stopsNotEqual: break - stop = stops.item(i) - ostop = ostops.item(i) - for attr in ['offset', 'stop-color', 'stop-opacity', 'style']: - if stop.getAttribute(attr) != ostop.getAttribute(attr): - stopsNotEqual = True - break - if stopsNotEqual: continue - - # ograd is a duplicate of grad, we schedule it to be removed UNLESS - # ograd is ALREADY considered a 'master' element - if ograd not in gradientsToRemove: - if ograd not in duplicateToMaster: - if grad not in gradientsToRemove: - gradientsToRemove[grad] = [] - gradientsToRemove[grad].append( ograd ) - duplicateToMaster[ograd] = grad - - # get a collection of all elements that are referenced and their referencing elements - referencedIDs = findReferencedElements(doc.documentElement) - for masterGrad in list(gradientsToRemove.keys()): - master_id = masterGrad.getAttribute('id') - for dupGrad in gradientsToRemove[masterGrad]: - # if the duplicate gradient no longer has a parent that means it was - # already re-mapped to another master gradient - if not dupGrad.parentNode: + # for each subsequent child element + for childNum in range(len(childElements)): + # skip first child + if childNum == 0: continue - # for each element that referenced the gradient we are going to replace dup_id with master_id - dup_id = dupGrad.getAttribute('id') - funcIRI = re.compile('url\([\'"]?#' + dup_id + '[\'"]?\)') # matches url(#a), url('#a') and url("#a") - for elem in referencedIDs[dup_id][1]: - # find out which attribute referenced the duplicate gradient - for attr in ['fill', 'stroke']: - v = elem.getAttribute(attr) - (v_new, n) = funcIRI.subn('url(#'+master_id+')', v) - if n > 0: - elem.setAttribute(attr, v_new) - if elem.getAttributeNS(NS['XLINK'], 'href') == '#'+dup_id: - elem.setAttributeNS(NS['XLINK'], 'href', '#'+master_id) - styles = _getStyle(elem) - for style in styles: - v = styles[style] - (v_new, n) = funcIRI.subn('url(#'+master_id+')', v) - if n > 0: - styles[style] = v_new - _setStyle(elem, styles) + child = childElements[childNum] + # if we are on an animateXXX/set element, ignore it (due to the 'fill' attribute) + if child.localName in ['set', 'animate', 'animateColor', 'animateTransform', 'animateMotion']: + continue + + distinctAttrs = [] + # loop through all current 'common' attributes + for name in list(commonAttrs.keys()): + # if this child doesn't match that attribute, schedule it for removal + if child.getAttribute(name) != commonAttrs[name]: + distinctAttrs.append(name) + # remove those attributes which are not common + for name in distinctAttrs: + del commonAttrs[name] + + # commonAttrs now has all the inheritable attributes which are common among all child elements + for name in list(commonAttrs.keys()): + for child in childElements: + child.removeAttribute(name) + elem.setAttribute(name, commonAttrs[name]) + + # update our statistic (we remove N*M attributes and add back in M attributes) + num += (len(childElements) - 1) * len(commonAttrs) + return num + + +def createGroupsForCommonAttributes(elem): + """ + Creates elements to contain runs of 3 or more + consecutive child elements having at least one common attribute. + + Common attributes are not promoted to the by this function. + This is handled by moveCommonAttributesToParentGroup. + + If all children have a common attribute, an extra is not created. + + This function acts recursively on the given element. + """ + num = 0 + global numElemsRemoved + + # TODO perhaps all of the Presentation attributes in http://www.w3.org/TR/SVG/struct.html#GElement + # could be added here + # Cyn: These attributes are the same as in moveAttributesToParentGroup, and must always be + for curAttr in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + # Iterate through the children in reverse order, so item(i) for + # items we have yet to visit still returns the correct nodes. + curChild = elem.childNodes.length - 1 + while curChild >= 0: + childNode = elem.childNodes.item(curChild) + + if childNode.nodeType == 1 and childNode.getAttribute(curAttr) != '' and \ + childNode.nodeName in [ + # only attempt to group elements that the content model allows to be children of a + + # SVG 1.1 (see https://www.w3.org/TR/SVG/struct.html#GElement) + 'animate', 'animateColor', 'animateMotion', 'animateTransform', 'set', # animation elements + 'desc', 'metadata', 'title', # descriptive elements + 'circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect', # shape elements + 'defs', 'g', 'svg', 'symbol', 'use', # structural elements + 'linearGradient', 'radialGradient', # gradient elements + 'a', 'altGlyphDef', 'clipPath', 'color-profile', 'cursor', 'filter', + 'font', 'font-face', 'foreignObject', 'image', 'marker', 'mask', + 'pattern', 'script', 'style', 'switch', 'text', 'view', + + # SVG 1.2 (see https://www.w3.org/TR/SVGTiny12/elementTable.html) + 'animation', 'audio', 'discard', 'handler', 'listener', + 'prefetch', 'solidColor', 'textArea', 'video' + ]: + # We're in a possible run! Track the value and run length. + value = childNode.getAttribute(curAttr) + runStart, runEnd = curChild, curChild + # Run elements includes only element tags, no whitespace/comments/etc. + # Later, we calculate a run length which includes these. + runElements = 1 + + # Backtrack to get all the nodes having the same + # attribute value, preserving any nodes in-between. + while runStart > 0: + nextNode = elem.childNodes.item(runStart - 1) + if nextNode.nodeType == 1: + if nextNode.getAttribute(curAttr) != value: + break + else: + runElements += 1 + runStart -= 1 + else: + runStart -= 1 + + if runElements >= 3: + # Include whitespace/comment/etc. nodes in the run. + while runEnd < elem.childNodes.length - 1: + if elem.childNodes.item(runEnd + 1).nodeType == 1: + break + else: + runEnd += 1 + + runLength = runEnd - runStart + 1 + if runLength == elem.childNodes.length: # Every child has this + # If the current parent is a already, + if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: + # do not act altogether on this attribute; all the + # children have it in common. + # Let moveCommonAttributesToParentGroup do it. + curChild = -1 + continue + # otherwise, it might be an element, and + # even if all children have the same attribute value, + # it's going to be worth making the since + # doesn't support attributes like 'stroke'. + # Fall through. + + # Create a element from scratch. + # We need the Document for this. + document = elem.ownerDocument + group = document.createElementNS(NS['SVG'], 'g') + # Move the run of elements to the group. + # a) ADD the nodes to the new group. + group.childNodes[:] = elem.childNodes[runStart:runEnd + 1] + for child in group.childNodes: + child.parentNode = group + # b) REMOVE the nodes from the element. + elem.childNodes[runStart:runEnd + 1] = [] + # Include the group in elem's children. + elem.childNodes.insert(runStart, group) + group.parentNode = elem + num += 1 + curChild = runStart - 1 + numElemsRemoved -= 1 + else: + curChild -= 1 + else: + curChild -= 1 + + # each child gets the same treatment, recursively + for childNode in elem.childNodes: + if childNode.nodeType == 1: + num += createGroupsForCommonAttributes(childNode) + + return num + + +def removeUnusedAttributesOnParent(elem): + """ + This recursively calls this function on all children of the element passed in, + then removes any unused attributes on this elem if none of the children inherit it + """ + num = 0 + + childElements = [] + # recurse first into the children (depth-first) + for child in elem.childNodes: + if child.nodeType == 1: + childElements.append(child) + num += removeUnusedAttributesOnParent(child) + + # only process the children if there are more than one element + if len(childElements) <= 1: + return num + + # get all attribute values on this parent + attrList = elem.attributes + unusedAttrs = {} + for index in range(attrList.length): + attr = attrList.item(index) + if attr.nodeName in ['clip-rule', + 'display-align', + 'fill', 'fill-opacity', 'fill-rule', + 'font', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', + 'pointer-events', 'shape-rendering', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', + 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', + 'text-anchor', 'text-decoration', 'text-rendering', 'visibility', + 'word-spacing', 'writing-mode']: + unusedAttrs[attr.nodeName] = attr.nodeValue + + # for each child, if at least one child inherits the parent's attribute, then remove + for childNum in range(len(childElements)): + child = childElements[childNum] + inheritedAttrs = [] + for name in list(unusedAttrs.keys()): + val = child.getAttribute(name) + if val == '' or val == None or val == 'inherit': + inheritedAttrs.append(name) + for a in inheritedAttrs: + del unusedAttrs[a] + + # unusedAttrs now has all the parent attributes that are unused + for name in list(unusedAttrs.keys()): + elem.removeAttribute(name) + num += 1 + + return num + + +def removeDuplicateGradientStops(doc): + global numElemsRemoved + num = 0 + + for gradType in ['linearGradient', 'radialGradient']: + for grad in doc.getElementsByTagName(gradType): + stops = {} + stopsToRemove = [] + for stop in grad.getElementsByTagName('stop'): + # convert percentages into a floating point number + offsetU = SVGLength(stop.getAttribute('offset')) + if offsetU.units == Unit.PCT: + offset = offsetU.value / 100.0 + elif offsetU.units == Unit.NONE: + offset = offsetU.value + else: + offset = 0 + # set the stop offset value to the integer or floating point equivalent + if int(offset) == offset: + stop.setAttribute('offset', str(int(offset))) + else: + stop.setAttribute('offset', str(offset)) + + color = stop.getAttribute('stop-color') + opacity = stop.getAttribute('stop-opacity') + style = stop.getAttribute('style') + if offset in stops: + oldStop = stops[offset] + if oldStop[0] == color and oldStop[1] == opacity and oldStop[2] == style: + stopsToRemove.append(stop) + stops[offset] = [color, opacity, style] + + for stop in stopsToRemove: + stop.parentNode.removeChild(stop) + num += 1 + numElemsRemoved += 1 + + # linear gradients + return num + + +def collapseSinglyReferencedGradients(doc): + global numElemsRemoved + num = 0 + + identifiedElements = findElementsWithId(doc.documentElement) + + # make sure to reset the ref'ed ids for when we are running this in testscour + for rid, nodeCount in six.iteritems(findReferencedElements(doc.documentElement)): + count = nodeCount[0] + nodes = nodeCount[1] + # Make sure that there's actually a defining element for the current ID name. + # (Cyn: I've seen documents with #id references but no element with that ID!) + if count == 1 and rid in identifiedElements: + elem = identifiedElements[rid] + if elem != None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \ + and elem.namespaceURI == NS['SVG']: + # found a gradient that is referenced by only 1 other element + refElem = nodes[0] + if refElem.nodeType == 1 and refElem.nodeName in ['linearGradient', 'radialGradient'] \ + and refElem.namespaceURI == NS['SVG']: + # elem is a gradient referenced by only one other gradient (refElem) + + # add the stops to the referencing gradient (this removes them from elem) + if len(refElem.getElementsByTagName('stop')) == 0: + stopsToAdd = elem.getElementsByTagName('stop') + for stop in stopsToAdd: + refElem.appendChild(stop) + + # adopt the gradientUnits, spreadMethod, gradientTransform attributes if + # they are unspecified on refElem + for attr in ['gradientUnits', 'spreadMethod', 'gradientTransform']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) + + # if both are radialGradients, adopt elem's fx,fy,cx,cy,r attributes if + # they are unspecified on refElem + if elem.nodeName == 'radialGradient' and refElem.nodeName == 'radialGradient': + for attr in ['fx', 'fy', 'cx', 'cy', 'r']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) + + # if both are linearGradients, adopt elem's x1,y1,x2,y2 attributes if + # they are unspecified on refElem + if elem.nodeName == 'linearGradient' and refElem.nodeName == 'linearGradient': + for attr in ['x1', 'y1', 'x2', 'y2']: + if refElem.getAttribute(attr) == '' and not elem.getAttribute(attr) == '': + refElem.setAttributeNS(None, attr, elem.getAttribute(attr)) + + # now remove the xlink:href from refElem + refElem.removeAttributeNS(NS['XLINK'], 'href') + + # now delete elem + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + num += 1 + return num + + +def removeDuplicateGradients(doc): + global numElemsRemoved + num = 0 + + gradientsToRemove = {} + duplicateToMaster = {} + + for gradType in ['linearGradient', 'radialGradient']: + grads = doc.getElementsByTagName(gradType) + for grad in grads: + # TODO: should slice grads from 'grad' here to optimize + for ograd in grads: + # do not compare gradient to itself + if grad == ograd: + continue + + # compare grad to ograd (all properties, then all stops) + # if attributes do not match, go to next gradient + someGradAttrsDoNotMatch = False + for attr in ['gradientUnits', 'spreadMethod', 'gradientTransform', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy', 'r']: + if grad.getAttribute(attr) != ograd.getAttribute(attr): + someGradAttrsDoNotMatch = True + break + + if someGradAttrsDoNotMatch: + continue + + # compare xlink:href values too + if grad.getAttributeNS(NS['XLINK'], 'href') != ograd.getAttributeNS(NS['XLINK'], 'href'): + continue + + # all gradient properties match, now time to compare stops + stops = grad.getElementsByTagName('stop') + ostops = ograd.getElementsByTagName('stop') + + if stops.length != ostops.length: + continue + + # now compare stops + stopsNotEqual = False + for i in range(stops.length): + if stopsNotEqual: + break + stop = stops.item(i) + ostop = ostops.item(i) + for attr in ['offset', 'stop-color', 'stop-opacity', 'style']: + if stop.getAttribute(attr) != ostop.getAttribute(attr): + stopsNotEqual = True + break + if stopsNotEqual: + continue + + # ograd is a duplicate of grad, we schedule it to be removed UNLESS + # ograd is ALREADY considered a 'master' element + if ograd not in gradientsToRemove: + if ograd not in duplicateToMaster: + if grad not in gradientsToRemove: + gradientsToRemove[grad] = [] + gradientsToRemove[grad].append(ograd) + duplicateToMaster[ograd] = grad + + # get a collection of all elements that are referenced and their referencing elements + referencedIDs = findReferencedElements(doc.documentElement) + for masterGrad in list(gradientsToRemove.keys()): + master_id = masterGrad.getAttribute('id') + for dupGrad in gradientsToRemove[masterGrad]: + # if the duplicate gradient no longer has a parent that means it was + # already re-mapped to another master gradient + if not dupGrad.parentNode: + continue + + # for each element that referenced the gradient we are going to replace dup_id with master_id + dup_id = dupGrad.getAttribute('id') + funcIRI = re.compile('url\([\'"]?#' + dup_id + '[\'"]?\)') # matches url(#a), url('#a') and url("#a") + for elem in referencedIDs[dup_id][1]: + # find out which attribute referenced the duplicate gradient + for attr in ['fill', 'stroke']: + v = elem.getAttribute(attr) + (v_new, n) = funcIRI.subn('url(#' + master_id + ')', v) + if n > 0: + elem.setAttribute(attr, v_new) + if elem.getAttributeNS(NS['XLINK'], 'href') == '#' + dup_id: + elem.setAttributeNS(NS['XLINK'], 'href', '#' + master_id) + styles = _getStyle(elem) + for style in styles: + v = styles[style] + (v_new, n) = funcIRI.subn('url(#' + master_id + ')', v) + if n > 0: + styles[style] = v_new + _setStyle(elem, styles) + + # now that all referencing elements have been re-mapped to the master + # it is safe to remove this gradient from the document + dupGrad.parentNode.removeChild(dupGrad) + numElemsRemoved += 1 + num += 1 + return num - # now that all referencing elements have been re-mapped to the master - # it is safe to remove this gradient from the document - dupGrad.parentNode.removeChild(dupGrad) - numElemsRemoved += 1 - num += 1 - return num def _getStyle(node): - u"""Returns the style attribute of a node as a dictionary.""" - if node.nodeType == 1 and len(node.getAttribute('style')) > 0: - styleMap = { } - rawStyles = node.getAttribute('style').split(';') - for style in rawStyles: - propval = style.split(':') - if len(propval) == 2: - styleMap[propval[0].strip()] = propval[1].strip() - return styleMap - else: - return {} + u"""Returns the style attribute of a node as a dictionary.""" + if node.nodeType == 1 and len(node.getAttribute('style')) > 0: + styleMap = {} + rawStyles = node.getAttribute('style').split(';') + for style in rawStyles: + propval = style.split(':') + if len(propval) == 2: + styleMap[propval[0].strip()] = propval[1].strip() + return styleMap + else: + return {} + def _setStyle(node, styleMap): - u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" - fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in list(styleMap.keys())]) - if fixedStyle != '': - node.setAttribute('style', fixedStyle) - elif node.getAttribute('style'): - node.removeAttribute('style') - return node + u"""Sets the style attribute of a node to the dictionary ``styleMap``.""" + fixedStyle = ';'.join([prop + ':' + styleMap[prop] for prop in list(styleMap.keys())]) + if fixedStyle != '': + node.setAttribute('style', fixedStyle) + elif node.getAttribute('style'): + node.removeAttribute('style') + return node + def repairStyle(node, options): - num = 0 - styleMap = _getStyle(node) - if styleMap: + num = 0 + styleMap = _getStyle(node) + if styleMap: - # I've seen this enough to know that I need to correct it: - # fill: url(#linearGradient4918) rgb(0, 0, 0); - for prop in ['fill', 'stroke']: - if prop in styleMap: - chunk = styleMap[prop].split(') ') - if len(chunk) == 2 and (chunk[0][:5] == 'url(#' or chunk[0][:6] == 'url("#' or chunk[0][:6] == "url('#") and chunk[1] == 'rgb(0, 0, 0)': - styleMap[prop] = chunk[0] + ')' - num += 1 + # I've seen this enough to know that I need to correct it: + # fill: url(#linearGradient4918) rgb(0, 0, 0); + for prop in ['fill', 'stroke']: + if prop in styleMap: + chunk = styleMap[prop].split(') ') + if len(chunk) == 2 and (chunk[0][:5] == 'url(#' or chunk[0][:6] == 'url("#' or chunk[0][:6] == "url('#") and chunk[1] == 'rgb(0, 0, 0)': + styleMap[prop] = chunk[0] + ')' + num += 1 - # Here is where we can weed out unnecessary styles like: - # opacity:1 - if 'opacity' in styleMap: - opacity = float(styleMap['opacity']) - # if opacity='0' then all fill and stroke properties are useless, remove them - if opacity == 0.0: - for uselessStyle in ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-linejoin', - 'stroke-opacity', 'stroke-miterlimit', 'stroke-linecap', 'stroke-dasharray', - 'stroke-dashoffset', 'stroke-opacity']: - if uselessStyle in styleMap and not styleInheritedByChild(node, uselessStyle): - del styleMap[uselessStyle] - num += 1 + # Here is where we can weed out unnecessary styles like: + # opacity:1 + if 'opacity' in styleMap: + opacity = float(styleMap['opacity']) + # if opacity='0' then all fill and stroke properties are useless, remove them + if opacity == 0.0: + for uselessStyle in ['fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-linejoin', + 'stroke-opacity', 'stroke-miterlimit', 'stroke-linecap', 'stroke-dasharray', + 'stroke-dashoffset', 'stroke-opacity']: + if uselessStyle in styleMap and not styleInheritedByChild(node, uselessStyle): + del styleMap[uselessStyle] + num += 1 - # if stroke:none, then remove all stroke-related properties (stroke-width, etc) - # TODO: should also detect if the computed value of this element is stroke="none" - if 'stroke' in styleMap and styleMap['stroke'] == 'none': - for strokestyle in [ 'stroke-width', 'stroke-linejoin', 'stroke-miterlimit', - 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity']: - if strokestyle in styleMap and not styleInheritedByChild(node, strokestyle): - del styleMap[strokestyle] - num += 1 - # we need to properly calculate computed values - if not styleInheritedByChild(node, 'stroke'): - if styleInheritedFromParent(node, 'stroke') in [None, 'none']: - del styleMap['stroke'] - num += 1 + # if stroke:none, then remove all stroke-related properties (stroke-width, etc) + # TODO: should also detect if the computed value of this element is stroke="none" + if 'stroke' in styleMap and styleMap['stroke'] == 'none': + for strokestyle in ['stroke-width', 'stroke-linejoin', 'stroke-miterlimit', + 'stroke-linecap', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity']: + if strokestyle in styleMap and not styleInheritedByChild(node, strokestyle): + del styleMap[strokestyle] + num += 1 + # we need to properly calculate computed values + if not styleInheritedByChild(node, 'stroke'): + if styleInheritedFromParent(node, 'stroke') in [None, 'none']: + del styleMap['stroke'] + num += 1 - # if fill:none, then remove all fill-related properties (fill-rule, etc) - if 'fill' in styleMap and styleMap['fill'] == 'none': - for fillstyle in [ 'fill-rule', 'fill-opacity' ]: - if fillstyle in styleMap and not styleInheritedByChild(node, fillstyle): - del styleMap[fillstyle] - num += 1 + # if fill:none, then remove all fill-related properties (fill-rule, etc) + if 'fill' in styleMap and styleMap['fill'] == 'none': + for fillstyle in ['fill-rule', 'fill-opacity']: + if fillstyle in styleMap and not styleInheritedByChild(node, fillstyle): + del styleMap[fillstyle] + num += 1 - # fill-opacity: 0 - if 'fill-opacity' in styleMap: - fillOpacity = float(styleMap['fill-opacity']) - if fillOpacity == 0.0: - for uselessFillStyle in [ 'fill', 'fill-rule' ]: - if uselessFillStyle in styleMap and not styleInheritedByChild(node, uselessFillStyle): - del styleMap[uselessFillStyle] - num += 1 + # fill-opacity: 0 + if 'fill-opacity' in styleMap: + fillOpacity = float(styleMap['fill-opacity']) + if fillOpacity == 0.0: + for uselessFillStyle in ['fill', 'fill-rule']: + if uselessFillStyle in styleMap and not styleInheritedByChild(node, uselessFillStyle): + del styleMap[uselessFillStyle] + num += 1 - # stroke-opacity: 0 - if 'stroke-opacity' in styleMap: - strokeOpacity = float(styleMap['stroke-opacity']) - if strokeOpacity == 0.0: - for uselessStrokeStyle in [ 'stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset' ]: - if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): - del styleMap[uselessStrokeStyle] - num += 1 + # stroke-opacity: 0 + if 'stroke-opacity' in styleMap: + strokeOpacity = float(styleMap['stroke-opacity']) + if strokeOpacity == 0.0: + for uselessStrokeStyle in ['stroke', 'stroke-width', 'stroke-linejoin', 'stroke-linecap', + 'stroke-dasharray', 'stroke-dashoffset']: + if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): + del styleMap[uselessStrokeStyle] + num += 1 - # stroke-width: 0 - if 'stroke-width' in styleMap: - strokeWidth = SVGLength(styleMap['stroke-width']) - if strokeWidth.value == 0.0: - for uselessStrokeStyle in [ 'stroke', 'stroke-linejoin', 'stroke-linecap', - 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity' ]: - if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): - del styleMap[uselessStrokeStyle] - num += 1 + # stroke-width: 0 + if 'stroke-width' in styleMap: + strokeWidth = SVGLength(styleMap['stroke-width']) + if strokeWidth.value == 0.0: + for uselessStrokeStyle in ['stroke', 'stroke-linejoin', 'stroke-linecap', + 'stroke-dasharray', 'stroke-dashoffset', 'stroke-opacity']: + if uselessStrokeStyle in styleMap and not styleInheritedByChild(node, uselessStrokeStyle): + del styleMap[uselessStrokeStyle] + num += 1 - # remove font properties for non-text elements - # I've actually observed this in real SVG content - if not mayContainTextNodes(node): - for fontstyle in [ 'font-family', 'font-size', 'font-stretch', 'font-size-adjust', - 'font-style', 'font-variant', 'font-weight', - 'letter-spacing', 'line-height', 'kerning', - 'text-align', 'text-anchor', 'text-decoration', - 'text-rendering', 'unicode-bidi', - 'word-spacing', 'writing-mode']: - if fontstyle in styleMap: - del styleMap[fontstyle] - num += 1 + # remove font properties for non-text elements + # I've actually observed this in real SVG content + if not mayContainTextNodes(node): + for fontstyle in ['font-family', 'font-size', 'font-stretch', 'font-size-adjust', + 'font-style', 'font-variant', 'font-weight', + 'letter-spacing', 'line-height', 'kerning', + 'text-align', 'text-anchor', 'text-decoration', + 'text-rendering', 'unicode-bidi', + 'word-spacing', 'writing-mode']: + if fontstyle in styleMap: + del styleMap[fontstyle] + num += 1 - # remove inkscape-specific styles - # TODO: need to get a full list of these - for inkscapeStyle in ['-inkscape-font-specification']: - if inkscapeStyle in styleMap: - del styleMap[inkscapeStyle] - num += 1 + # remove inkscape-specific styles + # TODO: need to get a full list of these + for inkscapeStyle in ['-inkscape-font-specification']: + if inkscapeStyle in styleMap: + del styleMap[inkscapeStyle] + num += 1 - if 'overflow' in styleMap: - # remove overflow from elements to which it does not apply, - # see https://www.w3.org/TR/SVG/masking.html#OverflowProperty - if not node.nodeName in ['svg','symbol','image','foreignObject','marker','pattern']: - del styleMap['overflow'] - num += 1 - # if the node is not the root element the SVG's user agent style sheet - # overrides the initial (i.e. default) value with the value 'hidden', which can consequently be removed - # (see last bullet point in the link above) - elif node != node.ownerDocument.documentElement: - if styleMap['overflow'] == 'hidden': - del styleMap['overflow'] - num += 1 - # on the root element the CSS2 default overflow="visible" is the initial value and we can remove it - elif styleMap['overflow'] == 'visible': - del styleMap['overflow'] - num += 1 + if 'overflow' in styleMap: + # remove overflow from elements to which it does not apply, + # see https://www.w3.org/TR/SVG/masking.html#OverflowProperty + if not node.nodeName in ['svg', 'symbol', 'image', 'foreignObject', 'marker', 'pattern']: + del styleMap['overflow'] + num += 1 + # if the node is not the root element the SVG's user agent style sheet + # overrides the initial (i.e. default) value with the value 'hidden', which can consequently be removed + # (see last bullet point in the link above) + elif node != node.ownerDocument.documentElement: + if styleMap['overflow'] == 'hidden': + del styleMap['overflow'] + num += 1 + # on the root element the CSS2 default overflow="visible" is the initial value and we can remove it + elif styleMap['overflow'] == 'visible': + del styleMap['overflow'] + num += 1 - # now if any of the properties match known SVG attributes we prefer attributes - # over style so emit them and remove them from the style map - if options.style_to_xml: - for propName in list(styleMap.keys()): - if propName in svgAttributes: - node.setAttribute(propName, styleMap[propName]) - del styleMap[propName] + # now if any of the properties match known SVG attributes we prefer attributes + # over style so emit them and remove them from the style map + if options.style_to_xml: + for propName in list(styleMap.keys()): + if propName in svgAttributes: + node.setAttribute(propName, styleMap[propName]) + del styleMap[propName] - _setStyle(node, styleMap) + _setStyle(node, styleMap) - # recurse for our child elements - for child in node.childNodes: - num += repairStyle(child,options) + # recurse for our child elements + for child in node.childNodes: + num += repairStyle(child, options) + + return num - return num def styleInheritedFromParent(node, style): - """ - Returns the value of 'style' that is inherited from the parents of the passed-in node + """ + Returns the value of 'style' that is inherited from the parents of the passed-in node - Warning: This method only considers presentation attributes and inline styles, - any style sheets are ignored! - """ - parentNode = node.parentNode; + Warning: This method only considers presentation attributes and inline styles, + any style sheets are ignored! + """ + parentNode = node.parentNode - # return None if we reached the Document element - if parentNode.nodeType == 9: - return None + # return None if we reached the Document element + if parentNode.nodeType == 9: + return None - # check styles first (they take precedence over presentation attributes) - styles = _getStyle(parentNode) - if style in styles.keys(): - value = styles[style] - if not value == 'inherit': - return value + # check styles first (they take precedence over presentation attributes) + styles = _getStyle(parentNode) + if style in styles.keys(): + value = styles[style] + if not value == 'inherit': + return value - # check attributes - value = parentNode.getAttribute(style) - if value not in ['', 'inherit']: - return parentNode.getAttribute(style) + # check attributes + value = parentNode.getAttribute(style) + if value not in ['', 'inherit']: + return parentNode.getAttribute(style) + + # check the next parent recursively if we did not find a value yet + return styleInheritedFromParent(parentNode, style) - # check the next parent recursively if we did not find a value yet - return styleInheritedFromParent(parentNode, style) def styleInheritedByChild(node, style, nodeIsChild=False): - """ - Returns whether 'style' is inherited by any children of the passed-in node + """ + Returns whether 'style' is inherited by any children of the passed-in node - If False is returned, it is guaranteed that 'style' can safely be removed - from the passed-in node without influencing visual output of it's children + If False is returned, it is guaranteed that 'style' can safely be removed + from the passed-in node without influencing visual output of it's children - If True is returned, the passed-in node should not have its text-based - attributes removed. + If True is returned, the passed-in node should not have its text-based + attributes removed. - Warning: This method only considers presentation attributes and inline styles, - any style sheets are ignored! - """ - # Comment, text and CDATA nodes don't have attributes and aren't containers so they can't inherit attributes - if node.nodeType != 1: - return False + Warning: This method only considers presentation attributes and inline styles, + any style sheets are ignored! + """ + # Comment, text and CDATA nodes don't have attributes and aren't containers so they can't inherit attributes + if node.nodeType != 1: + return False + if nodeIsChild: + # if the current child node sets a new value for 'style' + # we can stop the search in the current branch of the DOM tree - if nodeIsChild: - # if the current child node sets a new value for 'style' - # we can stop the search in the current branch of the DOM tree + # check attributes + if node.getAttribute(style) not in ['', 'inherit']: + return False + # check styles + styles = _getStyle(node) + if (style in styles.keys()) and not (styles[style] == 'inherit'): + return False + else: + # if the passed-in node does not have any children 'style' can obviously not be inherited + if not node.childNodes: + return False - # check attributes - if node.getAttribute(style) not in ['', 'inherit']: - return False - # check styles - styles = _getStyle(node) - if (style in styles.keys()) and not (styles[style] == 'inherit'): - return False - else: - # if the passed-in node does not have any children 'style' can obviously not be inherited - if not node.childNodes: - return False + # If we have child nodes recursively check those + if node.childNodes: + for child in node.childNodes: + if styleInheritedByChild(child, style, True): + return True - # If we have child nodes recursively check those - if node.childNodes: - for child in node.childNodes: - if styleInheritedByChild(child, style, True): - return True + # If the current element is a container element the inherited style is meaningless + # (since we made sure it's not inherited by any of its children) + if node.nodeName in ['a', 'defs', 'glyph', 'g', 'marker', 'mask', 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol']: + return False - # If the current element is a container element the inherited style is meaningless - # (since we made sure it's not inherited by any of its children) - if node.nodeName in ['a', 'defs', 'glyph', 'g', 'marker', 'mask', 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol']: - return False + # in all other cases we have to assume the inherited value of 'style' is meaningfull and has to be kept + # (e.g nodes without children at the end of the DOM tree, text nodes, ...) + return True - # in all other cases we have to assume the inherited value of 'style' is meaningfull and has to be kept - # (e.g nodes without children at the end of the DOM tree, text nodes, ...) - return True def mayContainTextNodes(node): - """ - Returns True if the passed-in node is probably a text element, or at least - one of its descendants is probably a text element. + """ + Returns True if the passed-in node is probably a text element, or at least + one of its descendants is probably a text element. - If False is returned, it is guaranteed that the passed-in node has no - business having text-based attributes. + If False is returned, it is guaranteed that the passed-in node has no + business having text-based attributes. - If True is returned, the passed-in node should not have its text-based - attributes removed. - """ - # Cached result of a prior call? - try: - return node.mayContainTextNodes - except AttributeError: - pass + If True is returned, the passed-in node should not have its text-based + attributes removed. + """ + # Cached result of a prior call? + try: + return node.mayContainTextNodes + except AttributeError: + pass - result = True # Default value - # Comment, text and CDATA nodes don't have attributes and aren't containers - if node.nodeType != 1: - result = False - # Non-SVG elements? Unknown elements! - elif node.namespaceURI != NS['SVG']: - result = True - # Blacklisted elements. Those are guaranteed not to be text elements. - elif node.nodeName in ['rect', 'circle', 'ellipse', 'line', 'polygon', - 'polyline', 'path', 'image', 'stop']: - result = False - # Group elements. If we're missing any here, the default of True is used. - elif node.nodeName in ['g', 'clipPath', 'marker', 'mask', 'pattern', - 'linearGradient', 'radialGradient', 'symbol']: - result = False - for child in node.childNodes: - if mayContainTextNodes(child): - result = True - # Everything else should be considered a future SVG-version text element - # at best, or an unknown element at worst. result will stay True. + result = True # Default value + # Comment, text and CDATA nodes don't have attributes and aren't containers + if node.nodeType != 1: + result = False + # Non-SVG elements? Unknown elements! + elif node.namespaceURI != NS['SVG']: + result = True + # Blacklisted elements. Those are guaranteed not to be text elements. + elif node.nodeName in ['rect', 'circle', 'ellipse', 'line', 'polygon', + 'polyline', 'path', 'image', 'stop']: + result = False + # Group elements. If we're missing any here, the default of True is used. + elif node.nodeName in ['g', 'clipPath', 'marker', 'mask', 'pattern', + 'linearGradient', 'radialGradient', 'symbol']: + result = False + for child in node.childNodes: + if mayContainTextNodes(child): + result = True + # Everything else should be considered a future SVG-version text element + # at best, or an unknown element at worst. result will stay True. - # Cache this result before returning it. - node.mayContainTextNodes = result - return result + # Cache this result before returning it. + node.mayContainTextNodes = result + return result # A list of default attributes that are safe to remove if all conditions are fulfilled @@ -1703,1330 +1746,1330 @@ def mayContainTextNodes(node): DefaultAttribute = namedtuple('DefaultAttribute', ['name', 'value', 'units', 'elements', 'conditions']) DefaultAttribute.__new__.__defaults__ = (None,) * len(DefaultAttribute._fields) default_attributes = [ - # unit systems - DefaultAttribute('clipPathUnits', 'userSpaceOnUse', elements = 'clipPath'), - DefaultAttribute('filterUnits', 'objectBoundingBox', elements = 'filter'), - DefaultAttribute('gradientUnits', 'objectBoundingBox', elements = ['linearGradient', 'radialGradient']), - DefaultAttribute('maskUnits', 'objectBoundingBox', elements = 'mask'), - DefaultAttribute('maskContentUnits', 'userSpaceOnUse', elements = 'mask'), - DefaultAttribute('patternUnits', 'objectBoundingBox', elements = 'pattern'), - DefaultAttribute('patternContentUnits', 'userSpaceOnUse', elements = 'pattern'), - DefaultAttribute('primitiveUnits', 'userSpaceOnUse', elements = 'filter'), + # unit systems + DefaultAttribute('clipPathUnits', 'userSpaceOnUse', elements='clipPath'), + DefaultAttribute('filterUnits', 'objectBoundingBox', elements='filter'), + DefaultAttribute('gradientUnits', 'objectBoundingBox', elements=['linearGradient', 'radialGradient']), + DefaultAttribute('maskUnits', 'objectBoundingBox', elements='mask'), + DefaultAttribute('maskContentUnits', 'userSpaceOnUse', elements='mask'), + DefaultAttribute('patternUnits', 'objectBoundingBox', elements='pattern'), + DefaultAttribute('patternContentUnits', 'userSpaceOnUse', elements='pattern'), + DefaultAttribute('primitiveUnits', 'userSpaceOnUse', elements='filter'), - DefaultAttribute('externalResourcesRequired', 'false', elements = ['a', 'altGlyph', 'animate', 'animateColor', - 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'cursor', 'defs', 'ellipse', 'feImage', 'filter', - 'font', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker', 'mask', 'mpath', 'path', 'pattern', - 'polygon', 'polyline', 'radialGradient', 'rect', 'script', 'set', 'svg', 'switch', 'symbol', 'text', 'textPath', - 'tref', 'tspan', 'use', 'view']), + DefaultAttribute('externalResourcesRequired', 'false', elements=['a', 'altGlyph', 'animate', 'animateColor', + 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'cursor', 'defs', 'ellipse', 'feImage', 'filter', + 'font', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker', 'mask', 'mpath', 'path', 'pattern', + 'polygon', 'polyline', 'radialGradient', 'rect', 'script', 'set', 'svg', 'switch', 'symbol', 'text', 'textPath', + 'tref', 'tspan', 'use', 'view']), - # svg elements - DefaultAttribute('width', 100, Unit.PCT, elements = 'svg'), - DefaultAttribute('height', 100, Unit.PCT, elements = 'svg'), - DefaultAttribute('baseProfile', 'none', elements = 'svg'), - DefaultAttribute('preserveAspectRatio', 'xMidYMid meet', elements = ['feImage', 'image', 'marker', 'pattern', 'svg', 'symbol', 'view']), + # svg elements + DefaultAttribute('width', 100, Unit.PCT, elements='svg'), + DefaultAttribute('height', 100, Unit.PCT, elements='svg'), + DefaultAttribute('baseProfile', 'none', elements='svg'), + DefaultAttribute('preserveAspectRatio', 'xMidYMid meet', elements=['feImage', 'image', 'marker', 'pattern', 'svg', 'symbol', 'view']), - # common attributes / basic types - DefaultAttribute('x', 0, elements = ['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', 'image', 'pattern', 'rect', 'svg', 'text', 'use']), - DefaultAttribute('y', 0, elements = ['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', 'image', 'pattern', 'rect', 'svg', 'text', 'use']), - DefaultAttribute('z', 0, elements = ['fePointLight', 'feSpotLight']), - DefaultAttribute('x1', 0, elements = 'line'), - DefaultAttribute('y1', 0, elements = 'line'), - DefaultAttribute('x2', 0, elements = 'line'), - DefaultAttribute('y2', 0, elements = 'line'), - DefaultAttribute('cx', 0, elements = ['circle', 'ellipse']), - DefaultAttribute('cy', 0, elements = ['circle', 'ellipse']), + # common attributes / basic types + DefaultAttribute('x', 0, elements=['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', 'image', 'pattern', 'rect', 'svg', 'text', 'use']), + DefaultAttribute('y', 0, elements=['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', 'image', 'pattern', 'rect', 'svg', 'text', 'use']), + DefaultAttribute('z', 0, elements=['fePointLight', 'feSpotLight']), + DefaultAttribute('x1', 0, elements='line'), + DefaultAttribute('y1', 0, elements='line'), + DefaultAttribute('x2', 0, elements='line'), + DefaultAttribute('y2', 0, elements='line'), + DefaultAttribute('cx', 0, elements=['circle', 'ellipse']), + DefaultAttribute('cy', 0, elements=['circle', 'ellipse']), - # markers - DefaultAttribute('markerUnits', 'strokeWidth', elements = 'marker'), - DefaultAttribute('refX', 0, elements = 'marker'), - DefaultAttribute('refY', 0, elements = 'marker'), - DefaultAttribute('markerHeight', 3, elements = 'marker'), - DefaultAttribute('markerWidth', 3, elements = 'marker'), - DefaultAttribute('orient', 0, elements = 'marker'), + # markers + DefaultAttribute('markerUnits', 'strokeWidth', elements='marker'), + DefaultAttribute('refX', 0, elements='marker'), + DefaultAttribute('refY', 0, elements='marker'), + DefaultAttribute('markerHeight', 3, elements='marker'), + DefaultAttribute('markerWidth', 3, elements='marker'), + DefaultAttribute('orient', 0, elements='marker'), - # text / textPath / tspan / tref - DefaultAttribute('lengthAdjust', 'spacing', elements = ['text', 'textPath', 'tref', 'tspan']), - DefaultAttribute('startOffset', 0, elements = 'textPath'), - DefaultAttribute('method', 'align', elements = 'textPath'), - DefaultAttribute('spacing', 'exact', elements = 'textPath'), + # text / textPath / tspan / tref + DefaultAttribute('lengthAdjust', 'spacing', elements=['text', 'textPath', 'tref', 'tspan']), + DefaultAttribute('startOffset', 0, elements='textPath'), + DefaultAttribute('method', 'align', elements='textPath'), + DefaultAttribute('spacing', 'exact', elements='textPath'), - # filters and masks - DefaultAttribute('x', -10, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('x', -0.1, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('y', -10, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('y', -0.1, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('width', 120, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('width', 1.2, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('height', 120, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('height', 1.2, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + # filters and masks + DefaultAttribute('x', -10, Unit.PCT, ['filter', 'mask']), + DefaultAttribute('x', -0.1, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('y', -10, Unit.PCT, ['filter', 'mask']), + DefaultAttribute('y', -0.1, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('width', 120, Unit.PCT, ['filter', 'mask']), + DefaultAttribute('width', 1.2, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('height', 120, Unit.PCT, ['filter', 'mask']), + DefaultAttribute('height', 1.2, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - # gradients - DefaultAttribute('x1', 0, elements = 'linearGradient'), - DefaultAttribute('y1', 0, elements = 'linearGradient'), - DefaultAttribute('y2', 0, elements = 'linearGradient'), - DefaultAttribute('x2', 100, Unit.PCT, 'linearGradient'), - DefaultAttribute('x2', 1, Unit.NONE, 'linearGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - # remove fx/fy before cx/cy to catch the case where fx = cx = 50% or fy = cy = 50% respectively - DefaultAttribute('fx', elements = 'radialGradient', conditions = lambda node: node.getAttribute('fx') == node.getAttribute('cx')), - DefaultAttribute('fy', elements = 'radialGradient', conditions = lambda node: node.getAttribute('fy') == node.getAttribute('cy')), - DefaultAttribute('r', 50, Unit.PCT, 'radialGradient'), - DefaultAttribute('r', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('cx', 50, Unit.PCT, 'radialGradient'), - DefaultAttribute('cx', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('cy', 50, Unit.PCT, 'radialGradient'), - DefaultAttribute('cy', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), - DefaultAttribute('spreadMethod', 'pad'), + # gradients + DefaultAttribute('x1', 0, elements='linearGradient'), + DefaultAttribute('y1', 0, elements='linearGradient'), + DefaultAttribute('y2', 0, elements='linearGradient'), + DefaultAttribute('x2', 100, Unit.PCT, 'linearGradient'), + DefaultAttribute('x2', 1, Unit.NONE, 'linearGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + # remove fx/fy before cx/cy to catch the case where fx = cx = 50% or fy = cy = 50% respectively + DefaultAttribute('fx', elements='radialGradient', conditions=lambda node: node.getAttribute('fx') == node.getAttribute('cx')), + DefaultAttribute('fy', elements='radialGradient', conditions=lambda node: node.getAttribute('fy') == node.getAttribute('cy')), + DefaultAttribute('r', 50, Unit.PCT, 'radialGradient'), + DefaultAttribute('r', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('cx', 50, Unit.PCT, 'radialGradient'), + DefaultAttribute('cx', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('cy', 50, Unit.PCT, 'radialGradient'), + DefaultAttribute('cy', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('spreadMethod', 'pad'), - # filter effects - DefaultAttribute('amplitude', 1, elements = ['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('azimuth', 0, elements = 'feDistantLight'), - DefaultAttribute('baseFrequency', 0, elements = ['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('bias', 1, elements = 'feConvolveMatrix'), - DefaultAttribute('diffuseConstant', 1, elements = 'feDiffuseLighting'), - DefaultAttribute('edgeMode', 'duplicate', elements = 'feConvolveMatrix'), - DefaultAttribute('elevation', 0, elements = 'feDistantLight'), - DefaultAttribute('exponent', 1, elements = ['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('intercept', 0, elements = ['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('k1', 0, elements = 'feComposite'), - DefaultAttribute('k2', 0, elements = 'feComposite'), - DefaultAttribute('k3', 0, elements = 'feComposite'), - DefaultAttribute('k4', 0, elements = 'feComposite'), - DefaultAttribute('mode', 'normal', elements = 'feBlend'), - DefaultAttribute('numOctaves', 1, elements = 'feTurbulence'), - DefaultAttribute('offset', 0, elements = ['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), - DefaultAttribute('operator', 'over', elements = 'feComposite'), - DefaultAttribute('operator', 'erode', elements = 'feMorphology'), - DefaultAttribute('order', 3, elements = 'feConvolveMatrix'), - DefaultAttribute('pointsAtX', 0, elements = 'feSpotLight'), - DefaultAttribute('pointsAtY', 0, elements = 'feSpotLight'), - DefaultAttribute('pointsAtZ', 0, elements = 'feSpotLight'), - DefaultAttribute('preserveAlpha', 'false', elements = 'feConvolveMatrix'), - DefaultAttribute('scale', 0, elements = 'feDisplacementMap'), - DefaultAttribute('seed', 0, elements = 'feTurbulence'), - DefaultAttribute('specularConstant', 1, elements = 'feSpecularLighting'), - DefaultAttribute('specularExponent', 1, elements = ['feSpecularLighting', 'feSpotLight']), - DefaultAttribute('stdDeviation', 0, elements = 'feGaussianBlur'), - DefaultAttribute('stitchTiles', 'noStitch', elements = 'feTurbulence'), - DefaultAttribute('surfaceScale', 1, elements = ['feDiffuseLighting', 'feSpecularLighting']), - DefaultAttribute('type', 'matrix', elements = 'feColorMatrix'), - DefaultAttribute('type', 'turbulence', elements = 'feTurbulence'), - DefaultAttribute('xChannelSelector', 'A', elements = 'feDisplacementMap'), - DefaultAttribute('yChannelSelector', 'A', elements = 'feDisplacementMap') + # filter effects + DefaultAttribute('amplitude', 1, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), + DefaultAttribute('azimuth', 0, elements='feDistantLight'), + DefaultAttribute('baseFrequency', 0, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), + DefaultAttribute('bias', 1, elements='feConvolveMatrix'), + DefaultAttribute('diffuseConstant', 1, elements='feDiffuseLighting'), + DefaultAttribute('edgeMode', 'duplicate', elements='feConvolveMatrix'), + DefaultAttribute('elevation', 0, elements='feDistantLight'), + DefaultAttribute('exponent', 1, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), + DefaultAttribute('intercept', 0, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), + DefaultAttribute('k1', 0, elements='feComposite'), + DefaultAttribute('k2', 0, elements='feComposite'), + DefaultAttribute('k3', 0, elements='feComposite'), + DefaultAttribute('k4', 0, elements='feComposite'), + DefaultAttribute('mode', 'normal', elements='feBlend'), + DefaultAttribute('numOctaves', 1, elements='feTurbulence'), + DefaultAttribute('offset', 0, elements=['feFuncA', 'feFuncB', 'feFuncG', 'feFuncR']), + DefaultAttribute('operator', 'over', elements='feComposite'), + DefaultAttribute('operator', 'erode', elements='feMorphology'), + DefaultAttribute('order', 3, elements='feConvolveMatrix'), + DefaultAttribute('pointsAtX', 0, elements='feSpotLight'), + DefaultAttribute('pointsAtY', 0, elements='feSpotLight'), + DefaultAttribute('pointsAtZ', 0, elements='feSpotLight'), + DefaultAttribute('preserveAlpha', 'false', elements='feConvolveMatrix'), + DefaultAttribute('scale', 0, elements='feDisplacementMap'), + DefaultAttribute('seed', 0, elements='feTurbulence'), + DefaultAttribute('specularConstant', 1, elements='feSpecularLighting'), + DefaultAttribute('specularExponent', 1, elements=['feSpecularLighting', 'feSpotLight']), + DefaultAttribute('stdDeviation', 0, elements='feGaussianBlur'), + DefaultAttribute('stitchTiles', 'noStitch', elements='feTurbulence'), + DefaultAttribute('surfaceScale', 1, elements=['feDiffuseLighting', 'feSpecularLighting']), + DefaultAttribute('type', 'matrix', elements='feColorMatrix'), + DefaultAttribute('type', 'turbulence', elements='feTurbulence'), + DefaultAttribute('xChannelSelector', 'A', elements='feDisplacementMap'), + DefaultAttribute('yChannelSelector', 'A', elements='feDisplacementMap') ] -def taint(taintedSet, taintedAttribute): - u"""Adds an attribute to a set of attributes. - Related attributes are also included.""" - taintedSet.add(taintedAttribute) - if taintedAttribute == 'marker': - taintedSet |= set(['marker-start', 'marker-mid', 'marker-end']) - if taintedAttribute in ['marker-start', 'marker-mid', 'marker-end']: - taintedSet.add('marker') - return taintedSet +def taint(taintedSet, taintedAttribute): + u"""Adds an attribute to a set of attributes. + + Related attributes are also included.""" + taintedSet.add(taintedAttribute) + if taintedAttribute == 'marker': + taintedSet |= set(['marker-start', 'marker-mid', 'marker-end']) + if taintedAttribute in ['marker-start', 'marker-mid', 'marker-end']: + taintedSet.add('marker') + return taintedSet + def removeDefaultAttributeValue(node, attribute): - """ - Removes the DefaultAttribute 'attribute' from 'node' if specified conditions are fulfilled - """ - if not node.hasAttribute(attribute.name): - return 0 + """ + Removes the DefaultAttribute 'attribute' from 'node' if specified conditions are fulfilled + """ + if not node.hasAttribute(attribute.name): + return 0 - if (attribute.elements is not None) and (node.nodeName not in attribute.elements): - return 0 + if (attribute.elements is not None) and (node.nodeName not in attribute.elements): + return 0 - # differentiate between text and numeric values - if isinstance(attribute.value, str): - if node.getAttribute(attribute.name) == attribute.value: - if (attribute.conditions is None) or attribute.conditions(node): - node.removeAttribute(attribute.name) - return 1 - else: - nodeValue = SVGLength(node.getAttribute(attribute.name)) - if (attribute.value is None) or ((nodeValue.value == attribute.value) and not (nodeValue.units == Unit.INVALID)): - if (attribute.units is None) or (nodeValue.units == attribute.units) or (isinstance(attribute.units, list) and nodeValue.units in attribute.units): + # differentiate between text and numeric values + if isinstance(attribute.value, str): + if node.getAttribute(attribute.name) == attribute.value: if (attribute.conditions is None) or attribute.conditions(node): - node.removeAttribute(attribute.name) - return 1 + node.removeAttribute(attribute.name) + return 1 + else: + nodeValue = SVGLength(node.getAttribute(attribute.name)) + if (attribute.value is None) or ((nodeValue.value == attribute.value) and not (nodeValue.units == Unit.INVALID)): + if (attribute.units is None) or (nodeValue.units == attribute.units) or (isinstance(attribute.units, list) and nodeValue.units in attribute.units): + if (attribute.conditions is None) or attribute.conditions(node): + node.removeAttribute(attribute.name) + return 1 + + return 0 - return 0 def removeDefaultAttributeValues(node, options, tainted=set()): - u"""'tainted' keeps a set of attributes defined in parent nodes. + u"""'tainted' keeps a set of attributes defined in parent nodes. - For such attributes, we don't delete attributes with default values.""" - num = 0 - if node.nodeType != 1: return 0 + For such attributes, we don't delete attributes with default values.""" + num = 0 + if node.nodeType != 1: + return 0 - # Conditionally remove all default attributes defined in 'default_attributes' (a list of 'DefaultAttribute's) - for attribute in default_attributes: - num += removeDefaultAttributeValue(node, attribute) + # Conditionally remove all default attributes defined in 'default_attributes' (a list of 'DefaultAttribute's) + for attribute in default_attributes: + num += removeDefaultAttributeValue(node, attribute) - # Summarily get rid of default properties - attributes = [node.attributes.item(i).nodeName for i in range(node.attributes.length)] - for attribute in attributes: - if attribute not in tainted: - if attribute in list(default_properties.keys()): - if node.getAttribute(attribute) == default_properties[attribute]: - node.removeAttribute(attribute) - num += 1 - else: - tainted = taint(tainted, attribute) - # Properties might also occur as styles, remove them too - styles = _getStyle(node) - for attribute in list(styles.keys()): - if attribute not in tainted: - if attribute in list(default_properties.keys()): - if styles[attribute] == default_properties[attribute]: - del styles[attribute] - num += 1 - else: - tainted = taint(tainted, attribute) - _setStyle(node, styles) + # Summarily get rid of default properties + attributes = [node.attributes.item(i).nodeName for i in range(node.attributes.length)] + for attribute in attributes: + if attribute not in tainted: + if attribute in list(default_properties.keys()): + if node.getAttribute(attribute) == default_properties[attribute]: + node.removeAttribute(attribute) + num += 1 + else: + tainted = taint(tainted, attribute) + # Properties might also occur as styles, remove them too + styles = _getStyle(node) + for attribute in list(styles.keys()): + if attribute not in tainted: + if attribute in list(default_properties.keys()): + if styles[attribute] == default_properties[attribute]: + del styles[attribute] + num += 1 + else: + tainted = taint(tainted, attribute) + _setStyle(node, styles) - # recurse for our child elements - for child in node.childNodes: - num += removeDefaultAttributeValues(child, options, tainted.copy()) + # recurse for our child elements + for child in node.childNodes: + num += removeDefaultAttributeValues(child, options, tainted.copy()) - return num + return num rgb = re.compile(r"\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*") rgbp = re.compile(r"\s*rgb\(\s*(\d*\.?\d+)%\s*,\s*(\d*\.?\d+)%\s*,\s*(\d*\.?\d+)%\s*\)\s*") + + def convertColor(value): - """ - Converts the input color string and returns a #RRGGBB (or #RGB if possible) string - """ - s = value + """ + Converts the input color string and returns a #RRGGBB (or #RGB if possible) string + """ + s = value - if s in list(colors.keys()): - s = colors[s] + if s in list(colors.keys()): + s = colors[s] - rgbpMatch = rgbp.match(s) - if rgbpMatch != None: - r = int(float(rgbpMatch.group(1)) * 255.0 / 100.0) - g = int(float(rgbpMatch.group(2)) * 255.0 / 100.0) - b = int(float(rgbpMatch.group(3)) * 255.0 / 100.0) - s = '#%02x%02x%02x' % (r, g, b) - else: - rgbMatch = rgb.match(s) - if rgbMatch != None: - r = int( rgbMatch.group(1) ) - g = int( rgbMatch.group(2) ) - b = int( rgbMatch.group(3) ) - s = '#%02x%02x%02x' % (r, g, b) + rgbpMatch = rgbp.match(s) + if rgbpMatch != None: + r = int(float(rgbpMatch.group(1)) * 255.0 / 100.0) + g = int(float(rgbpMatch.group(2)) * 255.0 / 100.0) + b = int(float(rgbpMatch.group(3)) * 255.0 / 100.0) + s = '#%02x%02x%02x' % (r, g, b) + else: + rgbMatch = rgb.match(s) + if rgbMatch != None: + r = int(rgbMatch.group(1)) + g = int(rgbMatch.group(2)) + b = int(rgbMatch.group(3)) + s = '#%02x%02x%02x' % (r, g, b) - if s[0] == '#': - s = s.lower() - if len(s)==7 and s[1]==s[2] and s[3]==s[4] and s[5]==s[6]: - s = '#'+s[1]+s[3]+s[5] + if s[0] == '#': + s = s.lower() + if len(s) == 7 and s[1] == s[2] and s[3] == s[4] and s[5] == s[6]: + s = '#' + s[1] + s[3] + s[5] + + return s - return s def convertColors(element): - """ - Recursively converts all color properties into #RRGGBB format if shorter - """ - numBytes = 0 + """ + Recursively converts all color properties into #RRGGBB format if shorter + """ + numBytes = 0 - if element.nodeType != 1: return 0 + if element.nodeType != 1: + return 0 - # set up list of color attributes for each element type - attrsToConvert = [] - if element.nodeName in ['rect', 'circle', 'ellipse', 'polygon', \ - 'line', 'polyline', 'path', 'g', 'a']: - attrsToConvert = ['fill', 'stroke'] - elif element.nodeName in ['stop']: - attrsToConvert = ['stop-color'] - elif element.nodeName in ['solidColor']: - attrsToConvert = ['solid-color'] + # set up list of color attributes for each element type + attrsToConvert = [] + if element.nodeName in ['rect', 'circle', 'ellipse', 'polygon', + 'line', 'polyline', 'path', 'g', 'a']: + attrsToConvert = ['fill', 'stroke'] + elif element.nodeName in ['stop']: + attrsToConvert = ['stop-color'] + elif element.nodeName in ['solidColor']: + attrsToConvert = ['solid-color'] - # now convert all the color formats - styles = _getStyle(element) - for attr in attrsToConvert: - oldColorValue = element.getAttribute(attr) - if oldColorValue != '': - newColorValue = convertColor(oldColorValue) - oldBytes = len(oldColorValue) - newBytes = len(newColorValue) - if oldBytes > newBytes: - element.setAttribute(attr, newColorValue) - numBytes += (oldBytes - len(element.getAttribute(attr))) - # colors might also hide in styles - if attr in list(styles.keys()): - oldColorValue = styles[attr] - newColorValue = convertColor(oldColorValue) - oldBytes = len(oldColorValue) - newBytes = len(newColorValue) - if oldBytes > newBytes: - styles[attr] = newColorValue - numBytes += (oldBytes - len(element.getAttribute(attr))) - _setStyle(element, styles) + # now convert all the color formats + styles = _getStyle(element) + for attr in attrsToConvert: + oldColorValue = element.getAttribute(attr) + if oldColorValue != '': + newColorValue = convertColor(oldColorValue) + oldBytes = len(oldColorValue) + newBytes = len(newColorValue) + if oldBytes > newBytes: + element.setAttribute(attr, newColorValue) + numBytes += (oldBytes - len(element.getAttribute(attr))) + # colors might also hide in styles + if attr in list(styles.keys()): + oldColorValue = styles[attr] + newColorValue = convertColor(oldColorValue) + oldBytes = len(oldColorValue) + newBytes = len(newColorValue) + if oldBytes > newBytes: + styles[attr] = newColorValue + numBytes += (oldBytes - len(element.getAttribute(attr))) + _setStyle(element, styles) - # now recurse for our child elements - for child in element.childNodes: - numBytes += convertColors(child) + # now recurse for our child elements + for child in element.childNodes: + numBytes += convertColors(child) - return numBytes + return numBytes # TODO: go over what this method does and see if there is a way to optimize it # TODO: go over the performance of this method and see if I can save memory/speed by # reusing data structures, etc + + def cleanPath(element, options): - """ - Cleans the path string (d attribute) of the element - """ - global numBytesSavedInPathData - global numPathSegmentsReduced - global numCurvesStraightened + """ + Cleans the path string (d attribute) of the element + """ + global numBytesSavedInPathData + global numPathSegmentsReduced + global numCurvesStraightened - # this gets the parser object from svg_regex.py - oldPathStr = element.getAttribute('d') - path = svg_parser.parse(oldPathStr) + # this gets the parser object from svg_regex.py + oldPathStr = element.getAttribute('d') + path = svg_parser.parse(oldPathStr) - # This determines whether the stroke has round linecaps. If it does, - # we do not want to collapse empty segments, as they are actually rendered. - withRoundLineCaps = element.getAttribute('stroke-linecap') == 'round' + # This determines whether the stroke has round linecaps. If it does, + # we do not want to collapse empty segments, as they are actually rendered. + withRoundLineCaps = element.getAttribute('stroke-linecap') == 'round' - # The first command must be a moveto, and whether it's relative (m) - # or absolute (M), the first set of coordinates *is* absolute. So - # the first iteration of the loop below will get x,y and startx,starty. + # The first command must be a moveto, and whether it's relative (m) + # or absolute (M), the first set of coordinates *is* absolute. So + # the first iteration of the loop below will get x,y and startx,starty. - # convert absolute coordinates into relative ones. - # Reuse the data structure 'path', since we're not adding or removing subcommands. - # Also reuse the coordinate lists since we're not adding or removing any. - for pathIndex in range(0, len(path)): - cmd, data = path[pathIndex] # Changes to cmd don't get through to the data structure - i = 0 - # adjust abs to rel - # only the A command has some values that we don't want to adjust (radii, rotation, flags) - if cmd == 'A': - for i in range(i, len(data), 7): - data[i+5] -= x - data[i+6] -= y - x += data[i+5] - y += data[i+6] - path[pathIndex] = ('a', data) - elif cmd == 'a': - x += sum(data[5::7]) - y += sum(data[6::7]) - elif cmd == 'H': - for i in range(i, len(data)): - data[i] -= x - x += data[i] - path[pathIndex] = ('h', data) - elif cmd == 'h': - x += sum(data) - elif cmd == 'V': - for i in range(i, len(data)): - data[i] -= y - y += data[i] - path[pathIndex] = ('v', data) - elif cmd == 'v': - y += sum(data) - elif cmd == 'M': - startx, starty = data[0], data[1] - # If this is a path starter, don't convert its first - # coordinate to relative; that would just make it (0, 0) - if pathIndex != 0: - data[0] -= x - data[1] -= y - - x, y = startx, starty - i = 2 - for i in range(i, len(data), 2): - data[i] -= x - data[i+1] -= y - x += data[i] - y += data[i+1] - path[pathIndex] = ('m', data) - elif cmd in ['L','T']: - for i in range(i, len(data), 2): - data[i] -= x - data[i+1] -= y - x += data[i] - y += data[i+1] - path[pathIndex] = (cmd.lower(), data) - elif cmd in ['m']: - if pathIndex == 0: - # START OF PATH - this is an absolute moveto - # followed by relative linetos + # convert absolute coordinates into relative ones. + # Reuse the data structure 'path', since we're not adding or removing subcommands. + # Also reuse the coordinate lists since we're not adding or removing any. + for pathIndex in range(0, len(path)): + cmd, data = path[pathIndex] # Changes to cmd don't get through to the data structure + i = 0 + # adjust abs to rel + # only the A command has some values that we don't want to adjust (radii, rotation, flags) + if cmd == 'A': + for i in range(i, len(data), 7): + data[i + 5] -= x + data[i + 6] -= y + x += data[i + 5] + y += data[i + 6] + path[pathIndex] = ('a', data) + elif cmd == 'a': + x += sum(data[5::7]) + y += sum(data[6::7]) + elif cmd == 'H': + for i in range(i, len(data)): + data[i] -= x + x += data[i] + path[pathIndex] = ('h', data) + elif cmd == 'h': + x += sum(data) + elif cmd == 'V': + for i in range(i, len(data)): + data[i] -= y + y += data[i] + path[pathIndex] = ('v', data) + elif cmd == 'v': + y += sum(data) + elif cmd == 'M': startx, starty = data[0], data[1] + # If this is a path starter, don't convert its first + # coordinate to relative; that would just make it (0, 0) + if pathIndex != 0: + data[0] -= x + data[1] -= y + x, y = startx, starty i = 2 - else: - startx = x + data[0] - starty = y + data[1] - for i in range(i, len(data), 2): - x += data[i] - y += data[i+1] - elif cmd in ['l','t']: - x += sum(data[0::2]) - y += sum(data[1::2]) - elif cmd in ['S','Q']: - for i in range(i, len(data), 4): - data[i] -= x - data[i+1] -= y - data[i+2] -= x - data[i+3] -= y - x += data[i+2] - y += data[i+3] - path[pathIndex] = (cmd.lower(), data) - elif cmd in ['s','q']: - x += sum(data[2::4]) - y += sum(data[3::4]) - elif cmd == 'C': - for i in range(i, len(data), 6): - data[i] -= x - data[i+1] -= y - data[i+2] -= x - data[i+3] -= y - data[i+4] -= x - data[i+5] -= y - x += data[i+4] - y += data[i+5] - path[pathIndex] = ('c', data) - elif cmd == 'c': - x += sum(data[4::6]) - y += sum(data[5::6]) - elif cmd in ['z','Z']: - x, y = startx, starty - path[pathIndex] = ('z', data) + for i in range(i, len(data), 2): + data[i] -= x + data[i + 1] -= y + x += data[i] + y += data[i + 1] + path[pathIndex] = ('m', data) + elif cmd in ['L', 'T']: + for i in range(i, len(data), 2): + data[i] -= x + data[i + 1] -= y + x += data[i] + y += data[i + 1] + path[pathIndex] = (cmd.lower(), data) + elif cmd in ['m']: + if pathIndex == 0: + # START OF PATH - this is an absolute moveto + # followed by relative linetos + startx, starty = data[0], data[1] + x, y = startx, starty + i = 2 + else: + startx = x + data[0] + starty = y + data[1] + for i in range(i, len(data), 2): + x += data[i] + y += data[i + 1] + elif cmd in ['l', 't']: + x += sum(data[0::2]) + y += sum(data[1::2]) + elif cmd in ['S', 'Q']: + for i in range(i, len(data), 4): + data[i] -= x + data[i + 1] -= y + data[i + 2] -= x + data[i + 3] -= y + x += data[i + 2] + y += data[i + 3] + path[pathIndex] = (cmd.lower(), data) + elif cmd in ['s', 'q']: + x += sum(data[2::4]) + y += sum(data[3::4]) + elif cmd == 'C': + for i in range(i, len(data), 6): + data[i] -= x + data[i + 1] -= y + data[i + 2] -= x + data[i + 3] -= y + data[i + 4] -= x + data[i + 5] -= y + x += data[i + 4] + y += data[i + 5] + path[pathIndex] = ('c', data) + elif cmd == 'c': + x += sum(data[4::6]) + y += sum(data[5::6]) + elif cmd in ['z', 'Z']: + x, y = startx, starty + path[pathIndex] = ('z', data) - # remove empty segments - # Reuse the data structure 'path' and the coordinate lists, even if we're - # deleting items, because these deletions are relatively cheap. - if not withRoundLineCaps: - for pathIndex in range(0, len(path)): - cmd, data = path[pathIndex] - i = 0 - if cmd in ['m','l','t']: - if cmd == 'm': - # remove m0,0 segments - if pathIndex > 0 and data[0] == data[i+1] == 0: - # 'm0,0 x,y' can be replaces with 'lx,y', - # except the first m which is a required absolute moveto - path[pathIndex] = ('l', data[2:]) - numPathSegmentsReduced += 1 - else: # else skip move coordinate - i = 2 + # remove empty segments + # Reuse the data structure 'path' and the coordinate lists, even if we're + # deleting items, because these deletions are relatively cheap. + if not withRoundLineCaps: + for pathIndex in range(0, len(path)): + cmd, data = path[pathIndex] + i = 0 + if cmd in ['m', 'l', 't']: + if cmd == 'm': + # remove m0,0 segments + if pathIndex > 0 and data[0] == data[i + 1] == 0: + # 'm0,0 x,y' can be replaces with 'lx,y', + # except the first m which is a required absolute moveto + path[pathIndex] = ('l', data[2:]) + numPathSegmentsReduced += 1 + else: # else skip move coordinate + i = 2 + while i < len(data): + if data[i] == data[i + 1] == 0: + del data[i:i + 2] + numPathSegmentsReduced += 1 + else: + i += 2 + elif cmd == 'c': + while i < len(data): + if data[i] == data[i + 1] == data[i + 2] == data[i + 3] == data[i + 4] == data[i + 5] == 0: + del data[i:i + 6] + numPathSegmentsReduced += 1 + else: + i += 6 + elif cmd == 'a': + while i < len(data): + if data[i + 5] == data[i + 6] == 0: + del data[i:i + 7] + numPathSegmentsReduced += 1 + else: + i += 7 + elif cmd == 'q': + while i < len(data): + if data[i] == data[i + 1] == data[i + 2] == data[i + 3] == 0: + del data[i:i + 4] + numPathSegmentsReduced += 1 + else: + i += 4 + elif cmd in ['h', 'v']: + oldLen = len(data) + path[pathIndex] = (cmd, [coord for coord in data if coord != 0]) + numPathSegmentsReduced += len(path[pathIndex][1]) - oldLen + + # fixup: Delete subcommands having no coordinates. + path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z'] + + # convert straight curves into lines + newPath = [path[0]] + for (cmd, data) in path[1:]: + i = 0 + newData = data + if cmd == 'c': + newData = [] while i < len(data): - if data[i] == data[i+1] == 0: - del data[i:i+2] - numPathSegmentsReduced += 1 - else: - i += 2 - elif cmd == 'c': - while i < len(data): - if data[i] == data[i+1] == data[i+2] == data[i+3] == data[i+4] == data[i+5] == 0: - del data[i:i+6] - numPathSegmentsReduced += 1 - else: - i += 6 - elif cmd == 'a': - while i < len(data): - if data[i+5] == data[i+6] == 0: - del data[i:i+7] - numPathSegmentsReduced += 1 - else: - i += 7 - elif cmd == 'q': - while i < len(data): - if data[i] == data[i+1] == data[i+2] == data[i+3] == 0: - del data[i:i+4] - numPathSegmentsReduced += 1 - else: - i += 4 - elif cmd in ['h','v']: - oldLen = len(data) - path[pathIndex] = (cmd, [coord for coord in data if coord != 0]) - numPathSegmentsReduced += len(path[pathIndex][1]) - oldLen + # since all commands are now relative, we can think of previous point as (0,0) + # and new point (dx,dy) is (data[i+4],data[i+5]) + # eqn of line will be y = (dy/dx)*x or if dx=0 then eqn of line is x=0 + (p1x, p1y) = (data[i], data[i + 1]) + (p2x, p2y) = (data[i + 2], data[i + 3]) + dx = data[i + 4] + dy = data[i + 5] - # fixup: Delete subcommands having no coordinates. - path = [elem for elem in path if len(elem[1]) > 0 or elem[0] == 'z'] + foundStraightCurve = False - # convert straight curves into lines - newPath = [path[0]] - for (cmd,data) in path[1:]: - i = 0 - newData = data - if cmd == 'c': - newData = [] - while i < len(data): - # since all commands are now relative, we can think of previous point as (0,0) - # and new point (dx,dy) is (data[i+4],data[i+5]) - # eqn of line will be y = (dy/dx)*x or if dx=0 then eqn of line is x=0 - (p1x,p1y) = (data[i],data[i+1]) - (p2x,p2y) = (data[i+2],data[i+3]) - dx = data[i+4] - dy = data[i+5] + if dx == 0: + if p1x == 0 and p2x == 0: + foundStraightCurve = True + else: + m = dy / dx + if p1y == m * p1x and p2y == m * p2x: + foundStraightCurve = True - foundStraightCurve = False + if foundStraightCurve: + # flush any existing curve coords first + if newData: + newPath.append((cmd, newData)) + newData = [] + # now create a straight line segment + newPath.append(('l', [dx, dy])) + numCurvesStraightened += 1 + else: + newData.extend(data[i:i + 6]) - if dx == 0: - if p1x == 0 and p2x == 0: - foundStraightCurve = True - else: - m = dy/dx - if p1y == m*p1x and p2y == m*p2x: - foundStraightCurve = True + i += 6 + if newData or cmd == 'z' or cmd == 'Z': + newPath.append((cmd, newData)) + path = newPath - if foundStraightCurve: - # flush any existing curve coords first - if newData: - newPath.append( (cmd,newData) ) - newData = [] - # now create a straight line segment - newPath.append( ('l', [dx,dy]) ) - numCurvesStraightened += 1 - else: - newData.extend(data[i:i+6]) + # collapse all consecutive commands of the same type into one command + prevCmd = '' + prevData = [] + newPath = [] + for (cmd, data) in path: + # flush the previous command if it is not the same type as the current command + if prevCmd != '': + if cmd != prevCmd or cmd == 'm': + newPath.append((prevCmd, prevData)) + prevCmd = '' + prevData = [] - i += 6 - if newData or cmd == 'z' or cmd == 'Z': - newPath.append( (cmd,newData) ) - path = newPath - - # collapse all consecutive commands of the same type into one command - prevCmd = '' - prevData = [] - newPath = [] - for (cmd,data) in path: - # flush the previous command if it is not the same type as the current command - if prevCmd != '': - if cmd != prevCmd or cmd == 'm': - newPath.append( (prevCmd, prevData) ) - prevCmd = '' - prevData = [] - - # if the previous and current commands are the same type, - # or the previous command is moveto and the current is lineto, collapse, - # but only if they are not move commands (since move can contain implicit lineto commands) - if (cmd == prevCmd or (cmd == 'l' and prevCmd == 'm')) and cmd != 'm': - prevData.extend(data) - - # save last command and data - else: - prevCmd = cmd - prevData = data - # flush last command and data - if prevCmd != '': - newPath.append( (prevCmd, prevData) ) - path = newPath - - # convert to shorthand path segments where possible - newPath = [] - for (cmd,data) in path: - # convert line segments into h,v where possible - if cmd == 'l': - i = 0 - lineTuples = [] - while i < len(data): - if data[i] == 0: - # vertical - if lineTuples: - # flush the existing line command - newPath.append( ('l', lineTuples) ) - lineTuples = [] - # append the v and then the remaining line coords - newPath.append( ('v', [data[i+1]]) ) - numPathSegmentsReduced += 1 - elif data[i+1] == 0: - if lineTuples: - # flush the line command, then append the h and then the remaining line coords - newPath.append( ('l', lineTuples) ) - lineTuples = [] - newPath.append( ('h', [data[i]]) ) - numPathSegmentsReduced += 1 - else: - lineTuples.extend(data[i:i+2]) - i += 2 - if lineTuples: - newPath.append( ('l', lineTuples) ) - # also handle implied relative linetos - elif cmd == 'm': - i = 2 - lineTuples = [data[0], data[1]] - while i < len(data): - if data[i] == 0: - # vertical - if lineTuples: - # flush the existing m/l command - newPath.append( (cmd, lineTuples) ) - lineTuples = [] - cmd = 'l' # dealing with linetos now - # append the v and then the remaining line coords - newPath.append( ('v', [data[i+1]]) ) - numPathSegmentsReduced += 1 - elif data[i+1] == 0: - if lineTuples: - # flush the m/l command, then append the h and then the remaining line coords - newPath.append( (cmd, lineTuples) ) - lineTuples = [] - cmd = 'l' # dealing with linetos now - newPath.append( ('h', [data[i]]) ) - numPathSegmentsReduced += 1 - else: - lineTuples.extend(data[i:i+2]) - i += 2 - if lineTuples: - newPath.append( (cmd, lineTuples) ) - # convert Bézier curve segments into s where possible - elif cmd == 'c': - # set up the assumed bezier control point as the current point, i.e. (0,0) since we're using relative coords - bez_ctl_pt = (0, 0) - # however if the previous command was 's' the assumed control point is a reflection of the previous control point at the current point - if len(newPath): - (prevCmd, prevData) = newPath[-1] - if prevCmd == 's': - bez_ctl_pt = (prevData[-2]-prevData[-4], prevData[-1]-prevData[-3]) - i = 0 - curveTuples = [] - while i < len(data): - # rotate by 180deg means negate both coordinates - # if the previous control point is equal then we can substitute a - # shorthand bezier command - if bez_ctl_pt[0] == data[i] and bez_ctl_pt[1] == data[i+1]: - if curveTuples: - newPath.append( ('c', curveTuples) ) - curveTuples = [] - # append the s command - newPath.append( ('s', [data[i+2], data[i+3], data[i+4], data[i+5]]) ) - numPathSegmentsReduced += 1 - else: - j = 0 - while j <= 5: - curveTuples.append(data[i+j]) - j += 1 - - # set up control point for next curve segment - bez_ctl_pt = (data[i+4]-data[i+2], data[i+5]-data[i+3]) - i += 6 - - if curveTuples: - newPath.append( ('c', curveTuples) ) - # convert quadratic curve segments into t where possible - elif cmd == 'q': - quad_ctl_pt = (0,0) - i = 0 - curveTuples = [] - while i < len(data): - if quad_ctl_pt[0] == data[i] and quad_ctl_pt[1] == data[i+1]: - if curveTuples: - newPath.append( ('q', curveTuples) ) - curveTuples = [] - # append the t command - newPath.append( ('t', [data[i+2], data[i+3]]) ) - numPathSegmentsReduced += 1 - else: - j = 0; - while j <= 3: - curveTuples.append(data[i+j]) - j += 1 - - quad_ctl_pt = (data[i+2]-data[i], data[i+3]-data[i+1]) - i += 4 - - if curveTuples: - newPath.append( ('q', curveTuples) ) - else: - newPath.append( (cmd, data) ) - path = newPath - - # for each h or v, collapse unnecessary coordinates that run in the same direction - # i.e. "h-100-100" becomes "h-200" but "h300-100" does not change - # Reuse the data structure 'path', since we're not adding or removing subcommands. - # Also reuse the coordinate lists, even if we're deleting items, because these - # deletions are relatively cheap. - for pathIndex in range(1, len(path)): - cmd, data = path[pathIndex] - if cmd in ['h','v'] and len(data) > 1: - coordIndex = 1 - while coordIndex < len(data): - if isSameSign(data[coordIndex - 1], data[coordIndex]): - data[coordIndex - 1] += data[coordIndex] - del data[coordIndex] - numPathSegmentsReduced += 1 - else: - coordIndex += 1 - - # it is possible that we have consecutive h, v, c, t commands now - # so again collapse all consecutive commands of the same type into one command - prevCmd = '' - prevData = [] - newPath = [path[0]] - for (cmd,data) in path[1:]: - # flush the previous command if it is not the same type as the current command - if prevCmd != '': - if cmd != prevCmd or cmd == 'm': - newPath.append( (prevCmd, prevData) ) - prevCmd = '' - prevData = [] - - # if the previous and current commands are the same type, collapse - if cmd == prevCmd and cmd != 'm': + # if the previous and current commands are the same type, + # or the previous command is moveto and the current is lineto, collapse, + # but only if they are not move commands (since move can contain implicit lineto commands) + if (cmd == prevCmd or (cmd == 'l' and prevCmd == 'm')) and cmd != 'm': prevData.extend(data) - # save last command and data - else: - prevCmd = cmd - prevData = data - # flush last command and data - if prevCmd != '': - newPath.append( (prevCmd, prevData) ) - path = newPath + # save last command and data + else: + prevCmd = cmd + prevData = data + # flush last command and data + if prevCmd != '': + newPath.append((prevCmd, prevData)) + path = newPath - newPathStr = serializePath(path, options) + # convert to shorthand path segments where possible + newPath = [] + for (cmd, data) in path: + # convert line segments into h,v where possible + if cmd == 'l': + i = 0 + lineTuples = [] + while i < len(data): + if data[i] == 0: + # vertical + if lineTuples: + # flush the existing line command + newPath.append(('l', lineTuples)) + lineTuples = [] + # append the v and then the remaining line coords + newPath.append(('v', [data[i + 1]])) + numPathSegmentsReduced += 1 + elif data[i + 1] == 0: + if lineTuples: + # flush the line command, then append the h and then the remaining line coords + newPath.append(('l', lineTuples)) + lineTuples = [] + newPath.append(('h', [data[i]])) + numPathSegmentsReduced += 1 + else: + lineTuples.extend(data[i:i + 2]) + i += 2 + if lineTuples: + newPath.append(('l', lineTuples)) + # also handle implied relative linetos + elif cmd == 'm': + i = 2 + lineTuples = [data[0], data[1]] + while i < len(data): + if data[i] == 0: + # vertical + if lineTuples: + # flush the existing m/l command + newPath.append((cmd, lineTuples)) + lineTuples = [] + cmd = 'l' # dealing with linetos now + # append the v and then the remaining line coords + newPath.append(('v', [data[i + 1]])) + numPathSegmentsReduced += 1 + elif data[i + 1] == 0: + if lineTuples: + # flush the m/l command, then append the h and then the remaining line coords + newPath.append((cmd, lineTuples)) + lineTuples = [] + cmd = 'l' # dealing with linetos now + newPath.append(('h', [data[i]])) + numPathSegmentsReduced += 1 + else: + lineTuples.extend(data[i:i + 2]) + i += 2 + if lineTuples: + newPath.append((cmd, lineTuples)) + # convert Bézier curve segments into s where possible + elif cmd == 'c': + # set up the assumed bezier control point as the current point, i.e. (0,0) since we're using relative coords + bez_ctl_pt = (0, 0) + # however if the previous command was 's' the assumed control point is a reflection of the previous control point at the current point + if len(newPath): + (prevCmd, prevData) = newPath[-1] + if prevCmd == 's': + bez_ctl_pt = (prevData[-2] - prevData[-4], prevData[-1] - prevData[-3]) + i = 0 + curveTuples = [] + while i < len(data): + # rotate by 180deg means negate both coordinates + # if the previous control point is equal then we can substitute a + # shorthand bezier command + if bez_ctl_pt[0] == data[i] and bez_ctl_pt[1] == data[i + 1]: + if curveTuples: + newPath.append(('c', curveTuples)) + curveTuples = [] + # append the s command + newPath.append(('s', [data[i + 2], data[i + 3], data[i + 4], data[i + 5]])) + numPathSegmentsReduced += 1 + else: + j = 0 + while j <= 5: + curveTuples.append(data[i + j]) + j += 1 - # if for whatever reason we actually made the path longer don't use it - # TODO: maybe we could compare path lengths after each optimization step and use the shortest - if len(newPathStr) <= len(oldPathStr): - numBytesSavedInPathData += ( len(oldPathStr) - len(newPathStr) ) - element.setAttribute('d', newPathStr) + # set up control point for next curve segment + bez_ctl_pt = (data[i + 4] - data[i + 2], data[i + 5] - data[i + 3]) + i += 6 + if curveTuples: + newPath.append(('c', curveTuples)) + # convert quadratic curve segments into t where possible + elif cmd == 'q': + quad_ctl_pt = (0, 0) + i = 0 + curveTuples = [] + while i < len(data): + if quad_ctl_pt[0] == data[i] and quad_ctl_pt[1] == data[i + 1]: + if curveTuples: + newPath.append(('q', curveTuples)) + curveTuples = [] + # append the t command + newPath.append(('t', [data[i + 2], data[i + 3]])) + numPathSegmentsReduced += 1 + else: + j = 0 + while j <= 3: + curveTuples.append(data[i + j]) + j += 1 + + quad_ctl_pt = (data[i + 2] - data[i], data[i + 3] - data[i + 1]) + i += 4 + + if curveTuples: + newPath.append(('q', curveTuples)) + else: + newPath.append((cmd, data)) + path = newPath + + # for each h or v, collapse unnecessary coordinates that run in the same direction + # i.e. "h-100-100" becomes "h-200" but "h300-100" does not change + # Reuse the data structure 'path', since we're not adding or removing subcommands. + # Also reuse the coordinate lists, even if we're deleting items, because these + # deletions are relatively cheap. + for pathIndex in range(1, len(path)): + cmd, data = path[pathIndex] + if cmd in ['h', 'v'] and len(data) > 1: + coordIndex = 1 + while coordIndex < len(data): + if isSameSign(data[coordIndex - 1], data[coordIndex]): + data[coordIndex - 1] += data[coordIndex] + del data[coordIndex] + numPathSegmentsReduced += 1 + else: + coordIndex += 1 + + # it is possible that we have consecutive h, v, c, t commands now + # so again collapse all consecutive commands of the same type into one command + prevCmd = '' + prevData = [] + newPath = [path[0]] + for (cmd, data) in path[1:]: + # flush the previous command if it is not the same type as the current command + if prevCmd != '': + if cmd != prevCmd or cmd == 'm': + newPath.append((prevCmd, prevData)) + prevCmd = '' + prevData = [] + + # if the previous and current commands are the same type, collapse + if cmd == prevCmd and cmd != 'm': + prevData.extend(data) + + # save last command and data + else: + prevCmd = cmd + prevData = data + # flush last command and data + if prevCmd != '': + newPath.append((prevCmd, prevData)) + path = newPath + + newPathStr = serializePath(path, options) + + # if for whatever reason we actually made the path longer don't use it + # TODO: maybe we could compare path lengths after each optimization step and use the shortest + if len(newPathStr) <= len(oldPathStr): + numBytesSavedInPathData += (len(oldPathStr) - len(newPathStr)) + element.setAttribute('d', newPathStr) def parseListOfPoints(s): - """ - Parse string into a list of points. + """ + Parse string into a list of points. - Returns a list containing an even number of coordinate strings - """ - i = 0 + Returns a list containing an even number of coordinate strings + """ + i = 0 - # (wsp)? comma-or-wsp-separated coordinate pairs (wsp)? - # coordinate-pair = coordinate comma-or-wsp coordinate - # coordinate = sign? integer - # comma-wsp: (wsp+ comma? wsp*) | (comma wsp*) - ws_nums = re.split(r"\s*[\s,]\s*", s.strip()) - nums = [] + # (wsp)? comma-or-wsp-separated coordinate pairs (wsp)? + # coordinate-pair = coordinate comma-or-wsp coordinate + # coordinate = sign? integer + # comma-wsp: (wsp+ comma? wsp*) | (comma wsp*) + ws_nums = re.split(r"\s*[\s,]\s*", s.strip()) + nums = [] - # also, if 100-100 is found, split it into two also - # - for i in range(len(ws_nums)): - negcoords = ws_nums[i].split("-") + # also, if 100-100 is found, split it into two also + # + for i in range(len(ws_nums)): + negcoords = ws_nums[i].split("-") - # this string didn't have any negative coordinates - if len(negcoords) == 1: - nums.append(negcoords[0]) - # we got negative coords - else: - for j in range(len(negcoords)): - # first number could be positive - if j == 0: - if negcoords[0] != '': - nums.append(negcoords[0]) - # otherwise all other strings will be negative - else: - # unless we accidentally split a number that was in scientific notation - # and had a negative exponent (500.00e-1) - prev = ""; - if len(nums): - prev = nums[len(nums)-1] - if prev and prev[len(prev)-1] in ['e', 'E']: - nums[len(nums)-1] = prev + '-' + negcoords[j] - else: - nums.append( '-'+negcoords[j] ) + # this string didn't have any negative coordinates + if len(negcoords) == 1: + nums.append(negcoords[0]) + # we got negative coords + else: + for j in range(len(negcoords)): + # first number could be positive + if j == 0: + if negcoords[0] != '': + nums.append(negcoords[0]) + # otherwise all other strings will be negative + else: + # unless we accidentally split a number that was in scientific notation + # and had a negative exponent (500.00e-1) + prev = "" + if len(nums): + prev = nums[len(nums) - 1] + if prev and prev[len(prev) - 1] in ['e', 'E']: + nums[len(nums) - 1] = prev + '-' + negcoords[j] + else: + nums.append('-' + negcoords[j]) - # if we have an odd number of points, return empty - if len(nums) % 2 != 0: return [] + # if we have an odd number of points, return empty + if len(nums) % 2 != 0: + return [] - # now resolve into Decimal values - i = 0 - while i < len(nums): - try: - nums[i] = getcontext().create_decimal(nums[i]) - nums[i + 1] = getcontext().create_decimal(nums[i + 1]) - except InvalidOperation: # one of the lengths had a unit or is an invalid number - return [] + # now resolve into Decimal values + i = 0 + while i < len(nums): + try: + nums[i] = getcontext().create_decimal(nums[i]) + nums[i + 1] = getcontext().create_decimal(nums[i + 1]) + except InvalidOperation: # one of the lengths had a unit or is an invalid number + return [] - i += 2 - - return nums + i += 2 + return nums def cleanPolygon(elem, options): - """ - Remove unnecessary closing point of polygon points attribute - """ - global numPointsRemovedFromPolygon - - pts = parseListOfPoints(elem.getAttribute('points')) - N = len(pts)/2 - if N >= 2: - (startx,starty) = pts[:2] - (endx,endy) = pts[-2:] - if startx == endx and starty == endy: - del pts[-2:] - numPointsRemovedFromPolygon += 1 - elem.setAttribute('points', scourCoordinates(pts, options, True)) + """ + Remove unnecessary closing point of polygon points attribute + """ + global numPointsRemovedFromPolygon + pts = parseListOfPoints(elem.getAttribute('points')) + N = len(pts) / 2 + if N >= 2: + (startx, starty) = pts[:2] + (endx, endy) = pts[-2:] + if startx == endx and starty == endy: + del pts[-2:] + numPointsRemovedFromPolygon += 1 + elem.setAttribute('points', scourCoordinates(pts, options, True)) def cleanPolyline(elem, options): - """ - Scour the polyline points attribute - """ - pts = parseListOfPoints(elem.getAttribute('points')) - elem.setAttribute('points', scourCoordinates(pts, options, True)) - + """ + Scour the polyline points attribute + """ + pts = parseListOfPoints(elem.getAttribute('points')) + elem.setAttribute('points', scourCoordinates(pts, options, True)) def serializePath(pathObj, options): - """ - Reserializes the path data with some cleanups. - """ - # elliptical arc commands must have comma/wsp separating the coordinates - # this fixes an issue outlined in Fix https://bugs.launchpad.net/scour/+bug/412754 - return ''.join([cmd + scourCoordinates(data, options, (cmd == 'a')) for cmd, data in pathObj]) - + """ + Reserializes the path data with some cleanups. + """ + # elliptical arc commands must have comma/wsp separating the coordinates + # this fixes an issue outlined in Fix https://bugs.launchpad.net/scour/+bug/412754 + return ''.join([cmd + scourCoordinates(data, options, (cmd == 'a')) for cmd, data in pathObj]) def serializeTransform(transformObj): - """ - Reserializes the transform data with some cleanups. - """ - return ' '.join( - [command + '(' + ' '.join( - [scourUnitlessLength(number) for number in numbers] - ) + ')' - for command, numbers in transformObj] - ) + """ + Reserializes the transform data with some cleanups. + """ + return ' '.join( + [command + '(' + ' '.join( + [scourUnitlessLength(number) for number in numbers] + ) + ')' + for command, numbers in transformObj] + ) +def scourCoordinates(data, options, forceCommaWsp=False): + """ + Serializes coordinate data with some cleanups: + - removes all trailing zeros after the decimal + - integerize coordinates if possible + - removes extraneous whitespace + - adds spaces between values in a subcommand if required (or if forceCommaWsp is True) + """ + if data != None: + newData = [] + c = 0 + previousCoord = '' + for coord in data: + scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround) + # only need the comma if the current number starts with a digit + # (numbers can start with - without needing a comma before) + # or if forceCommaWsp is True + # or if this number starts with a dot and the previous number + # had *no* dot or exponent (so we can go like -5.5.5 for -5.5,0.5 + # and 4e4.5 for 40000,0.5) + if c > 0 and (forceCommaWsp + or scouredCoord[0].isdigit() + or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord)) + ): + newData.append(' ') -def scourCoordinates(data, options, forceCommaWsp = False): - """ - Serializes coordinate data with some cleanups: - - removes all trailing zeros after the decimal - - integerize coordinates if possible - - removes extraneous whitespace - - adds spaces between values in a subcommand if required (or if forceCommaWsp is True) - """ - if data != None: - newData = [] - c = 0 - previousCoord = '' - for coord in data: - scouredCoord = scourUnitlessLength(coord, needsRendererWorkaround=options.renderer_workaround) - # only need the comma if the current number starts with a digit - # (numbers can start with - without needing a comma before) - # or if forceCommaWsp is True - # or if this number starts with a dot and the previous number - # had *no* dot or exponent (so we can go like -5.5.5 for -5.5,0.5 - # and 4e4.5 for 40000,0.5) - if c > 0 and (forceCommaWsp - or scouredCoord[0].isdigit() - or (scouredCoord[0] == '.' and not ('.' in previousCoord or 'e' in previousCoord)) - ): - newData.append( ' ' ) + # add the scoured coordinate to the path string + newData.append(scouredCoord) + previousCoord = scouredCoord + c += 1 - # add the scoured coordinate to the path string - newData.append( scouredCoord ) - previousCoord = scouredCoord - c += 1 - - # What we need to do to work around GNOME bugs 548494, 563933 and - # 620565, which are being fixed and unfixed in Ubuntu, is - # to make sure that a dot doesn't immediately follow a command - # (so 'h50' and 'h0.5' are allowed, but not 'h.5'). - # Then, we need to add a space character after any coordinates - # having an 'e' (scientific notation), so as to have the exponent - # separate from the next number. - if options.renderer_workaround: - if len(newData) > 0: - for i in range(1, len(newData)): - if newData[i][0] == '-' and 'e' in newData[i - 1]: - newData[i - 1] += ' ' + # What we need to do to work around GNOME bugs 548494, 563933 and + # 620565, which are being fixed and unfixed in Ubuntu, is + # to make sure that a dot doesn't immediately follow a command + # (so 'h50' and 'h0.5' are allowed, but not 'h.5'). + # Then, we need to add a space character after any coordinates + # having an 'e' (scientific notation), so as to have the exponent + # separate from the next number. + if options.renderer_workaround: + if len(newData) > 0: + for i in range(1, len(newData)): + if newData[i][0] == '-' and 'e' in newData[i - 1]: + newData[i - 1] += ' ' + return ''.join(newData) + else: return ''.join(newData) - else: - return ''.join(newData) - - return '' + return '' def scourLength(length): - """ - Scours a length. Accepts units. - """ - length = SVGLength(length) + """ + Scours a length. Accepts units. + """ + length = SVGLength(length) - return scourUnitlessLength(length.value) + Unit.str(length.units) + return scourUnitlessLength(length.value) + Unit.str(length.units) +def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a numeric type + """ + Scours the numeric part of a length only. Does not accept units. -def scourUnitlessLength(length, needsRendererWorkaround=False): # length is of a numeric type - """ - Scours the numeric part of a length only. Does not accept units. + This is faster than scourLength on elements guaranteed not to + contain units. + """ + if not isinstance(length, Decimal): + length = getcontext().create_decimal(str(length)) - This is faster than scourLength on elements guaranteed not to - contain units. - """ - if not isinstance(length, Decimal): - length = getcontext().create_decimal(str(length)) + # reduce numeric precision + # plus() corresponds to the unary prefix plus operator and applies context precision and rounding + length = scouringContext.plus(length) - # reduce numeric precision - # plus() corresponds to the unary prefix plus operator and applies context precision and rounding - length = scouringContext.plus(length) + # remove trailing zeroes as we do not care for significance + intLength = length.to_integral_value() + if length == intLength: + length = Decimal(intLength) + else: + length = length.normalize() - # remove trailing zeroes as we do not care for significance - intLength = length.to_integral_value() - if length == intLength: - length = Decimal(intLength) - else: - length = length.normalize() + # gather the non-scientific notation version of the coordinate. + # this may actually be in scientific notation if the value is + # sufficiently large or small, so this is a misnomer. + nonsci = six.text_type(length).lower().replace("e+", "e") + if not needsRendererWorkaround: + if len(nonsci) > 2 and nonsci[:2] == '0.': + nonsci = nonsci[1:] # remove the 0, leave the dot + elif len(nonsci) > 3 and nonsci[:3] == '-0.': + nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot - # gather the non-scientific notation version of the coordinate. - # this may actually be in scientific notation if the value is - # sufficiently large or small, so this is a misnomer. - nonsci = six.text_type(length).lower().replace("e+", "e") - if not needsRendererWorkaround: - if len(nonsci) > 2 and nonsci[:2] == '0.': - nonsci = nonsci[1:] # remove the 0, leave the dot - elif len(nonsci) > 3 and nonsci[:3] == '-0.': - nonsci = '-' + nonsci[2:] # remove the 0, leave the minus and dot + # Gather the scientific notation version of the coordinate which + # can only be shorter if the length of the number is at least 4 characters (e.g. 1000 = 1e3). + if len(nonsci) > 3: + # We have to implement this ourselves since both 'normalize()' and 'to_sci_string()' + # don't handle negative exponents in a reasonable way (e.g. 0.000001 remains unchanged) + exponent = length.adjusted() # how far do we have to shift the dot? + length = length.scaleb(-exponent).normalize() # shift the dot and remove potential trailing zeroes - # Gather the scientific notation version of the coordinate which - # can only be shorter if the length of the number is at least 4 characters (e.g. 1000 = 1e3). - if len(nonsci) > 3: - # We have to implement this ourselves since both 'normalize()' and 'to_sci_string()' - # don't handle negative exponents in a reasonable way (e.g. 0.000001 remains unchanged) - exponent = length.adjusted() # how far do we have to shift the dot? - length = length.scaleb(-exponent).normalize() # shift the dot and remove potential trailing zeroes - - sci = six.text_type(length) + 'e' + six.text_type(exponent) - - if len(sci) < len(nonsci): - return sci - else: - return nonsci - else: - return nonsci + sci = six.text_type(length) + 'e' + six.text_type(exponent) + if len(sci) < len(nonsci): + return sci + else: + return nonsci + else: + return nonsci def reducePrecision(element): - """ - Because opacities, letter spacings, stroke widths and all that don't need - to be preserved in SVG files with 9 digits of precision. + """ + Because opacities, letter spacings, stroke widths and all that don't need + to be preserved in SVG files with 9 digits of precision. - Takes all of these attributes, in the given element node and its children, - and reduces their precision to the current Decimal context's precision. - Also checks for the attributes actually being lengths, not 'inherit', 'none' - or anything that isn't an SVGLength. + Takes all of these attributes, in the given element node and its children, + and reduces their precision to the current Decimal context's precision. + Also checks for the attributes actually being lengths, not 'inherit', 'none' + or anything that isn't an SVGLength. - Returns the number of bytes saved after performing these reductions. - """ - num = 0 + Returns the number of bytes saved after performing these reductions. + """ + num = 0 - styles = _getStyle(element) - for lengthAttr in ['opacity', 'flood-opacity', 'fill-opacity', - 'stroke-opacity', 'stop-opacity', 'stroke-miterlimit', - 'stroke-dashoffset', 'letter-spacing', 'word-spacing', - 'kerning', 'font-size-adjust', 'font-size', - 'stroke-width']: - val = element.getAttribute(lengthAttr) - if val != '': - valLen = SVGLength(val) - if valLen.units != Unit.INVALID: # not an absolute/relative size or inherit, can be % though - newVal = scourLength(val) - if len(newVal) < len(val): - num += len(val) - len(newVal) - element.setAttribute(lengthAttr, newVal) - # repeat for attributes hidden in styles - if lengthAttr in list(styles.keys()): - val = styles[lengthAttr] - valLen = SVGLength(val) - if valLen.units != Unit.INVALID: - newVal = scourLength(val) - if len(newVal) < len(val): - num += len(val) - len(newVal) - styles[lengthAttr] = newVal - _setStyle(element, styles) + styles = _getStyle(element) + for lengthAttr in ['opacity', 'flood-opacity', 'fill-opacity', + 'stroke-opacity', 'stop-opacity', 'stroke-miterlimit', + 'stroke-dashoffset', 'letter-spacing', 'word-spacing', + 'kerning', 'font-size-adjust', 'font-size', + 'stroke-width']: + val = element.getAttribute(lengthAttr) + if val != '': + valLen = SVGLength(val) + if valLen.units != Unit.INVALID: # not an absolute/relative size or inherit, can be % though + newVal = scourLength(val) + if len(newVal) < len(val): + num += len(val) - len(newVal) + element.setAttribute(lengthAttr, newVal) + # repeat for attributes hidden in styles + if lengthAttr in list(styles.keys()): + val = styles[lengthAttr] + valLen = SVGLength(val) + if valLen.units != Unit.INVALID: + newVal = scourLength(val) + if len(newVal) < len(val): + num += len(val) - len(newVal) + styles[lengthAttr] = newVal + _setStyle(element, styles) - for child in element.childNodes: - if child.nodeType == 1: - num += reducePrecision(child) - - return num + for child in element.childNodes: + if child.nodeType == 1: + num += reducePrecision(child) + return num def optimizeAngle(angle): - """ - Because any rotation can be expressed within 360 degrees - of any given number, and since negative angles sometimes - are one character longer than corresponding positive angle, - we shorten the number to one in the range to [-90, 270[. - """ - # First, we put the new angle in the range ]-360, 360[. - # The modulo operator yields results with the sign of the - # divisor, so for negative dividends, we preserve the sign - # of the angle. - if angle < 0: angle %= -360 - else: angle %= 360 - # 720 degrees is unneccessary, as 360 covers all angles. - # As "-x" is shorter than "35x" and "-xxx" one character - # longer than positive angles <= 260, we constrain angle - # range to [-90, 270[ (or, equally valid: ]-100, 260]). - if angle >= 270: angle -= 360 - elif angle < -90: angle += 360 - return angle - + """ + Because any rotation can be expressed within 360 degrees + of any given number, and since negative angles sometimes + are one character longer than corresponding positive angle, + we shorten the number to one in the range to [-90, 270[. + """ + # First, we put the new angle in the range ]-360, 360[. + # The modulo operator yields results with the sign of the + # divisor, so for negative dividends, we preserve the sign + # of the angle. + if angle < 0: + angle %= -360 + else: + angle %= 360 + # 720 degrees is unneccessary, as 360 covers all angles. + # As "-x" is shorter than "35x" and "-xxx" one character + # longer than positive angles <= 260, we constrain angle + # range to [-90, 270[ (or, equally valid: ]-100, 260]). + if angle >= 270: + angle -= 360 + elif angle < -90: + angle += 360 + return angle def optimizeTransform(transform): - """ - Optimises a series of transformations parsed from a single - transform="" attribute. + """ + Optimises a series of transformations parsed from a single + transform="" attribute. - The transformation list is modified in-place. - """ - # FIXME: reordering these would optimize even more cases: - # first: Fold consecutive runs of the same transformation - # extra: Attempt to cast between types to create sameness: - # "matrix(0 1 -1 0 0 0) rotate(180) scale(-1)" all - # are rotations (90, 180, 180) -- thus "rotate(90)" - # second: Simplify transforms where numbers are optional. - # third: Attempt to simplify any single remaining matrix() - # - # if there's only one transformation and it's a matrix, - # try to make it a shorter non-matrix transformation - # NOTE: as matrix(a b c d e f) in SVG means the matrix: - # |¯ a c e ¯| make constants |¯ A1 A2 A3 ¯| - # | b d f | translating them | B1 B2 B3 | - # |_ 0 0 1 _| to more readable |_ 0 0 1 _| - if len(transform) == 1 and transform[0][0] == 'matrix': - matrix = A1, B1, A2, B2, A3, B3 = transform[0][1] - # |¯ 1 0 0 ¯| - # | 0 1 0 | Identity matrix (no transformation) - # |_ 0 0 1 _| - if matrix == [1, 0, 0, 1, 0, 0]: - del transform[0] - # |¯ 1 0 X ¯| - # | 0 1 Y | Translation by (X, Y). - # |_ 0 0 1 _| - elif (A1 == 1 and A2 == 0 - and B1 == 0 and B2 == 1): - transform[0] = ('translate', [A3, B3]) - # |¯ X 0 0 ¯| - # | 0 Y 0 | Scaling by (X, Y). - # |_ 0 0 1 _| - elif ( A2 == 0 and A3 == 0 - and B1 == 0 and B3 == 0): - transform[0] = ('scale', [A1, B2]) - # |¯ cos(A) -sin(A) 0 ¯| Rotation by angle A, - # | sin(A) cos(A) 0 | clockwise, about the origin. - # |_ 0 0 1 _| A is in degrees, [-180...180]. - elif (A1 == B2 and -1 <= A1 <= 1 and A3 == 0 - and -B1 == A2 and -1 <= B1 <= 1 and B3 == 0 - # as cos² A + sin² A == 1 and as decimal trig is approximate: - # FIXME: the "epsilon" term here should really be some function - # of the precision of the (sin|cos)_A terms, not 1e-15: - and abs((B1 ** 2) + (A1 ** 2) - 1) < Decimal("1e-15")): - sin_A, cos_A = B1, A1 - # while asin(A) and acos(A) both only have an 180° range - # the sign of sin(A) and cos(A) varies across quadrants, - # letting us hone in on the angle the matrix represents: - # -- => < -90 | -+ => -90..0 | ++ => 0..90 | +- => >= 90 - # - # http://en.wikipedia.org/wiki/File:Sine_cosine_plot.svg - # shows asin has the correct angle the middle quadrants: - A = Decimal(str(math.degrees(math.asin(float(sin_A))))) - if cos_A < 0: # otherwise needs adjusting from the edges - if sin_A < 0: - A = -180 - A - else: - A = 180 - A - transform[0] = ('rotate', [A]) + The transformation list is modified in-place. + """ + # FIXME: reordering these would optimize even more cases: + # first: Fold consecutive runs of the same transformation + # extra: Attempt to cast between types to create sameness: + # "matrix(0 1 -1 0 0 0) rotate(180) scale(-1)" all + # are rotations (90, 180, 180) -- thus "rotate(90)" + # second: Simplify transforms where numbers are optional. + # third: Attempt to simplify any single remaining matrix() + # + # if there's only one transformation and it's a matrix, + # try to make it a shorter non-matrix transformation + # NOTE: as matrix(a b c d e f) in SVG means the matrix: + # |¯ a c e ¯| make constants |¯ A1 A2 A3 ¯| + # | b d f | translating them | B1 B2 B3 | + # |_ 0 0 1 _| to more readable |_ 0 0 1 _| + if len(transform) == 1 and transform[0][0] == 'matrix': + matrix = A1, B1, A2, B2, A3, B3 = transform[0][1] + # |¯ 1 0 0 ¯| + # | 0 1 0 | Identity matrix (no transformation) + # |_ 0 0 1 _| + if matrix == [1, 0, 0, 1, 0, 0]: + del transform[0] + # |¯ 1 0 X ¯| + # | 0 1 Y | Translation by (X, Y). + # |_ 0 0 1 _| + elif (A1 == 1 and A2 == 0 + and B1 == 0 and B2 == 1): + transform[0] = ('translate', [A3, B3]) + # |¯ X 0 0 ¯| + # | 0 Y 0 | Scaling by (X, Y). + # |_ 0 0 1 _| + elif (A2 == 0 and A3 == 0 + and B1 == 0 and B3 == 0): + transform[0] = ('scale', [A1, B2]) + # |¯ cos(A) -sin(A) 0 ¯| Rotation by angle A, + # | sin(A) cos(A) 0 | clockwise, about the origin. + # |_ 0 0 1 _| A is in degrees, [-180...180]. + elif (A1 == B2 and -1 <= A1 <= 1 and A3 == 0 + and -B1 == A2 and -1 <= B1 <= 1 and B3 == 0 + # as cos² A + sin² A == 1 and as decimal trig is approximate: + # FIXME: the "epsilon" term here should really be some function + # of the precision of the (sin|cos)_A terms, not 1e-15: + and abs((B1 ** 2) + (A1 ** 2) - 1) < Decimal("1e-15")): + sin_A, cos_A = B1, A1 + # while asin(A) and acos(A) both only have an 180° range + # the sign of sin(A) and cos(A) varies across quadrants, + # letting us hone in on the angle the matrix represents: + # -- => < -90 | -+ => -90..0 | ++ => 0..90 | +- => >= 90 + # + # http://en.wikipedia.org/wiki/File:Sine_cosine_plot.svg + # shows asin has the correct angle the middle quadrants: + A = Decimal(str(math.degrees(math.asin(float(sin_A))))) + if cos_A < 0: # otherwise needs adjusting from the edges + if sin_A < 0: + A = -180 - A + else: + A = 180 - A + transform[0] = ('rotate', [A]) - # Simplify transformations where numbers are optional. - for type, args in transform: - if type == 'translate': - # Only the X coordinate is required for translations. - # If the Y coordinate is unspecified, it's 0. - if len(args) == 2 and args[1] == 0: - del args[1] - elif type == 'rotate': - args[0] = optimizeAngle(args[0]) # angle - # Only the angle is required for rotations. - # If the coordinates are unspecified, it's the origin (0, 0). - if len(args) == 3 and args[1] == args[2] == 0: - del args[1:] - elif type == 'scale': - # Only the X scaling factor is required. - # If the Y factor is unspecified, it's the same as X. - if len(args) == 2 and args[0] == args[1]: - del args[1] + # Simplify transformations where numbers are optional. + for type, args in transform: + if type == 'translate': + # Only the X coordinate is required for translations. + # If the Y coordinate is unspecified, it's 0. + if len(args) == 2 and args[1] == 0: + del args[1] + elif type == 'rotate': + args[0] = optimizeAngle(args[0]) # angle + # Only the angle is required for rotations. + # If the coordinates are unspecified, it's the origin (0, 0). + if len(args) == 3 and args[1] == args[2] == 0: + del args[1:] + elif type == 'scale': + # Only the X scaling factor is required. + # If the Y factor is unspecified, it's the same as X. + if len(args) == 2 and args[0] == args[1]: + del args[1] - # Attempt to coalesce runs of the same transformation. - # Translations followed immediately by other translations, - # rotations followed immediately by other rotations, - # scaling followed immediately by other scaling, - # are safe to add. - # Identity skewX/skewY are safe to remove, but how do they accrete? - # |¯ 1 0 0 ¯| - # | tan(A) 1 0 | skews X coordinates by angle A - # |_ 0 0 1 _| - # - # |¯ 1 tan(A) 0 ¯| - # | 0 1 0 | skews Y coordinates by angle A - # |_ 0 0 1 _| - # - # FIXME: A matrix followed immediately by another matrix - # would be safe to multiply together, too. - i = 1 - while i < len(transform): - currType, currArgs = transform[i] - prevType, prevArgs = transform[i - 1] - if currType == prevType == 'translate': - prevArgs[0] += currArgs[0] # x - # for y, only add if the second translation has an explicit y - if len(currArgs) == 2: - if len(prevArgs) == 2: - prevArgs[1] += currArgs[1] # y - elif len(prevArgs) == 1: - prevArgs.append(currArgs[1]) # y - del transform[i] - if prevArgs[0] == prevArgs[1] == 0: - # Identity translation! - i -= 1 + # Attempt to coalesce runs of the same transformation. + # Translations followed immediately by other translations, + # rotations followed immediately by other rotations, + # scaling followed immediately by other scaling, + # are safe to add. + # Identity skewX/skewY are safe to remove, but how do they accrete? + # |¯ 1 0 0 ¯| + # | tan(A) 1 0 | skews X coordinates by angle A + # |_ 0 0 1 _| + # + # |¯ 1 tan(A) 0 ¯| + # | 0 1 0 | skews Y coordinates by angle A + # |_ 0 0 1 _| + # + # FIXME: A matrix followed immediately by another matrix + # would be safe to multiply together, too. + i = 1 + while i < len(transform): + currType, currArgs = transform[i] + prevType, prevArgs = transform[i - 1] + if currType == prevType == 'translate': + prevArgs[0] += currArgs[0] # x + # for y, only add if the second translation has an explicit y + if len(currArgs) == 2: + if len(prevArgs) == 2: + prevArgs[1] += currArgs[1] # y + elif len(prevArgs) == 1: + prevArgs.append(currArgs[1]) # y del transform[i] - elif (currType == prevType == 'rotate' - and len(prevArgs) == len(currArgs) == 1): - # Only coalesce if both rotations are from the origin. - prevArgs[0] = optimizeAngle(prevArgs[0] + currArgs[0]) - del transform[i] - elif currType == prevType == 'scale': - prevArgs[0] *= currArgs[0] # x - # handle an implicit y - if len(prevArgs) == 2 and len(currArgs) == 2: - # y1 * y2 - prevArgs[1] *= currArgs[1] - elif len(prevArgs) == 1 and len(currArgs) == 2: - # create y2 = uniformscalefactor1 * y2 - prevArgs.append(prevArgs[0] * currArgs[1]) - elif len(prevArgs) == 2 and len(currArgs) == 1: - # y1 * uniformscalefactor2 - prevArgs[1] *= currArgs[0] - del transform[i] - if prevArgs[0] == prevArgs[1] == 1: - # Identity scale! - i -= 1 + if prevArgs[0] == prevArgs[1] == 0: + # Identity translation! + i -= 1 + del transform[i] + elif (currType == prevType == 'rotate' + and len(prevArgs) == len(currArgs) == 1): + # Only coalesce if both rotations are from the origin. + prevArgs[0] = optimizeAngle(prevArgs[0] + currArgs[0]) del transform[i] - else: - i += 1 - - # Some fixups are needed for single-element transformation lists, since - # the loop above was to coalesce elements with their predecessors in the - # list, and thus it required 2 elements. - i = 0 - while i < len(transform): - currType, currArgs = transform[i] - if ((currType == 'skewX' or currType == 'skewY') - and len(currArgs) == 1 and currArgs[0] == 0): - # Identity skew! - del transform[i] - elif ((currType == 'rotate') - and len(currArgs) == 1 and currArgs[0] == 0): - # Identity rotation! - del transform[i] - else: - i += 1 + elif currType == prevType == 'scale': + prevArgs[0] *= currArgs[0] # x + # handle an implicit y + if len(prevArgs) == 2 and len(currArgs) == 2: + # y1 * y2 + prevArgs[1] *= currArgs[1] + elif len(prevArgs) == 1 and len(currArgs) == 2: + # create y2 = uniformscalefactor1 * y2 + prevArgs.append(prevArgs[0] * currArgs[1]) + elif len(prevArgs) == 2 and len(currArgs) == 1: + # y1 * uniformscalefactor2 + prevArgs[1] *= currArgs[0] + del transform[i] + if prevArgs[0] == prevArgs[1] == 1: + # Identity scale! + i -= 1 + del transform[i] + else: + i += 1 + # Some fixups are needed for single-element transformation lists, since + # the loop above was to coalesce elements with their predecessors in the + # list, and thus it required 2 elements. + i = 0 + while i < len(transform): + currType, currArgs = transform[i] + if ((currType == 'skewX' or currType == 'skewY') + and len(currArgs) == 1 and currArgs[0] == 0): + # Identity skew! + del transform[i] + elif ((currType == 'rotate') + and len(currArgs) == 1 and currArgs[0] == 0): + # Identity rotation! + del transform[i] + else: + i += 1 def optimizeTransforms(element, options): - """ - Attempts to optimise transform specifications on the given node and its children. + """ + Attempts to optimise transform specifications on the given node and its children. - Returns the number of bytes saved after performing these reductions. - """ - num = 0 + Returns the number of bytes saved after performing these reductions. + """ + num = 0 - for transformAttr in ['transform', 'patternTransform', 'gradientTransform']: - val = element.getAttribute(transformAttr) - if val != '': - transform = svg_transform_parser.parse(val) + for transformAttr in ['transform', 'patternTransform', 'gradientTransform']: + val = element.getAttribute(transformAttr) + if val != '': + transform = svg_transform_parser.parse(val) - optimizeTransform(transform) + optimizeTransform(transform) - newVal = serializeTransform(transform) + newVal = serializeTransform(transform) - if len(newVal) < len(val): - if len(newVal): - element.setAttribute(transformAttr, newVal) - else: - element.removeAttribute(transformAttr) - num += len(val) - len(newVal) + if len(newVal) < len(val): + if len(newVal): + element.setAttribute(transformAttr, newVal) + else: + element.removeAttribute(transformAttr) + num += len(val) - len(newVal) - for child in element.childNodes: - if child.nodeType == 1: - num += optimizeTransforms(child, options) - - return num + for child in element.childNodes: + if child.nodeType == 1: + num += optimizeTransforms(child, options) + return num def removeComments(element): - """ - Removes comments from the element and its children. - """ - global numCommentBytes - - if isinstance(element, xml.dom.minidom.Comment): - numCommentBytes += len(element.data) - element.parentNode.removeChild(element) - else: - for subelement in element.childNodes[:]: - removeComments(subelement) + """ + Removes comments from the element and its children. + """ + global numCommentBytes + if isinstance(element, xml.dom.minidom.Comment): + numCommentBytes += len(element.data) + element.parentNode.removeChild(element) + else: + for subelement in element.childNodes[:]: + removeComments(subelement) def embedRasters(element, options): - import base64 - import urllib - """ + import base64 + import urllib + """ Converts raster references to inline images. NOTE: there are size limits to base64-encoding handling in browsers """ - global numRastersEmbedded + global numRastersEmbedded - href = element.getAttributeNS(NS['XLINK'],'href') + href = element.getAttributeNS(NS['XLINK'], 'href') - # if xlink:href is set, then grab the id - if href != '' and len(href) > 1: - # find if href value has filename ext - ext = os.path.splitext(os.path.basename(href))[1].lower()[1:] + # if xlink:href is set, then grab the id + if href != '' and len(href) > 1: + # find if href value has filename ext + ext = os.path.splitext(os.path.basename(href))[1].lower()[1:] - # look for 'png', 'jpg', and 'gif' extensions - if ext == 'png' or ext == 'jpg' or ext == 'gif': + # look for 'png', 'jpg', and 'gif' extensions + if ext == 'png' or ext == 'jpg' or ext == 'gif': - # file:// URLs denote files on the local system too - if href[:7] == 'file://': - href = href[7:] - # does the file exist? - if os.path.isfile(href): - # if this is not an absolute path, set path relative - # to script file based on input arg - infilename = '.' - if options.infilename: infilename = options.infilename - href = os.path.join(os.path.dirname(infilename), href) + # file:// URLs denote files on the local system too + if href[:7] == 'file://': + href = href[7:] + # does the file exist? + if os.path.isfile(href): + # if this is not an absolute path, set path relative + # to script file based on input arg + infilename = '.' + if options.infilename: + infilename = options.infilename + href = os.path.join(os.path.dirname(infilename), href) - rasterdata = '' - # test if file exists locally - if os.path.isfile(href): - # open raster file as raw binary - raster = open( href, "rb") - rasterdata = raster.read() - elif href[:7] == 'http://': - webFile = urllib.urlopen( href ) - rasterdata = webFile.read() - webFile.close() + rasterdata = '' + # test if file exists locally + if os.path.isfile(href): + # open raster file as raw binary + raster = open(href, "rb") + rasterdata = raster.read() + elif href[:7] == 'http://': + webFile = urllib.urlopen(href) + rasterdata = webFile.read() + webFile.close() - # ... should we remove all images which don't resolve? - if rasterdata != '': - # base64-encode raster - b64eRaster = base64.b64encode( rasterdata ) + # ... should we remove all images which don't resolve? + if rasterdata != '': + # base64-encode raster + b64eRaster = base64.b64encode(rasterdata) - # set href attribute to base64-encoded equivalent - if b64eRaster != '': - # PNG and GIF both have MIME Type 'image/[ext]', but - # JPEG has MIME Type 'image/jpeg' - if ext == 'jpg': - ext = 'jpeg' - - element.setAttributeNS(NS['XLINK'], 'href', 'data:image/' + ext + ';base64,' + b64eRaster) - numRastersEmbedded += 1 - del b64eRaster + # set href attribute to base64-encoded equivalent + if b64eRaster != '': + # PNG and GIF both have MIME Type 'image/[ext]', but + # JPEG has MIME Type 'image/jpeg' + if ext == 'jpg': + ext = 'jpeg' + element.setAttributeNS(NS['XLINK'], 'href', 'data:image/' + ext + ';base64,' + b64eRaster) + numRastersEmbedded += 1 + del b64eRaster def properlySizeDoc(docElement, options): - # get doc width and height - w = SVGLength(docElement.getAttribute('width')) - h = SVGLength(docElement.getAttribute('height')) + # get doc width and height + w = SVGLength(docElement.getAttribute('width')) + h = SVGLength(docElement.getAttribute('height')) - # if width/height are not unitless or px then it is not ok to rewrite them into a viewBox. - # well, it may be OK for Web browsers and vector editors, but not for librsvg. - if options.renderer_workaround: - if ((w.units != Unit.NONE and w.units != Unit.PX) or - (h.units != Unit.NONE and h.units != Unit.PX)): - return - - # else we have a statically sized image and we should try to remedy that - - # parse viewBox attribute - vbSep = re.split("\\s*\\,?\\s*", docElement.getAttribute('viewBox'), 3) - # if we have a valid viewBox we need to check it - vbWidth,vbHeight = 0,0 - if len(vbSep) == 4: - try: - # if x or y are specified and non-zero then it is not ok to overwrite it - vbX = float(vbSep[0]) - vbY = float(vbSep[1]) - if vbX != 0 or vbY != 0: + # if width/height are not unitless or px then it is not ok to rewrite them into a viewBox. + # well, it may be OK for Web browsers and vector editors, but not for librsvg. + if options.renderer_workaround: + if ((w.units != Unit.NONE and w.units != Unit.PX) or + (h.units != Unit.NONE and h.units != Unit.PX)): return - # if width or height are not equal to doc width/height then it is not ok to overwrite it - vbWidth = float(vbSep[2]) - vbHeight = float(vbSep[3]) - if vbWidth != w.value or vbHeight != h.value: - return - # if the viewBox did not parse properly it is invalid and ok to overwrite it - except ValueError: - pass + # else we have a statically sized image and we should try to remedy that - # at this point it's safe to set the viewBox and remove width/height - docElement.setAttribute('viewBox', '0 0 %s %s' % (w.value, h.value)) - docElement.removeAttribute('width') - docElement.removeAttribute('height') + # parse viewBox attribute + vbSep = re.split("\\s*\\,?\\s*", docElement.getAttribute('viewBox'), 3) + # if we have a valid viewBox we need to check it + vbWidth, vbHeight = 0, 0 + if len(vbSep) == 4: + try: + # if x or y are specified and non-zero then it is not ok to overwrite it + vbX = float(vbSep[0]) + vbY = float(vbSep[1]) + if vbX != 0 or vbY != 0: + return + # if width or height are not equal to doc width/height then it is not ok to overwrite it + vbWidth = float(vbSep[2]) + vbHeight = float(vbSep[3]) + if vbWidth != w.value or vbHeight != h.value: + return + # if the viewBox did not parse properly it is invalid and ok to overwrite it + except ValueError: + pass + + # at this point it's safe to set the viewBox and remove width/height + docElement.setAttribute('viewBox', '0 0 %s %s' % (w.value, h.value)) + docElement.removeAttribute('width') + docElement.removeAttribute('height') def remapNamespacePrefix(node, oldprefix, newprefix): - if node == None or node.nodeType != 1: return + if node == None or node.nodeType != 1: + return - if node.prefix == oldprefix: - localName = node.localName - namespace = node.namespaceURI - doc = node.ownerDocument - parent = node.parentNode + if node.prefix == oldprefix: + localName = node.localName + namespace = node.namespaceURI + doc = node.ownerDocument + parent = node.parentNode - # create a replacement node - newNode = None - if newprefix != '': - newNode = doc.createElementNS(namespace, newprefix+":"+localName) - else: - newNode = doc.createElement(localName); + # create a replacement node + newNode = None + if newprefix != '': + newNode = doc.createElementNS(namespace, newprefix + ":" + localName) + else: + newNode = doc.createElement(localName) - # add all the attributes - attrList = node.attributes - for i in range(attrList.length): - attr = attrList.item(i) - newNode.setAttributeNS( attr.namespaceURI, attr.localName, attr.nodeValue) + # add all the attributes + attrList = node.attributes + for i in range(attrList.length): + attr = attrList.item(i) + newNode.setAttributeNS(attr.namespaceURI, attr.localName, attr.nodeValue) - # clone and add all the child nodes - for child in node.childNodes: - newNode.appendChild(child.cloneNode(True)) + # clone and add all the child nodes + for child in node.childNodes: + newNode.appendChild(child.cloneNode(True)) - # replace old node with new node - parent.replaceChild( newNode, node ) - # set the node to the new node in the remapped namespace prefix - node = newNode - - # now do all child nodes - for child in node.childNodes: - remapNamespacePrefix(child, oldprefix, newprefix) + # replace old node with new node + parent.replaceChild(newNode, node) + # set the node to the new node in the remapped namespace prefix + node = newNode + # now do all child nodes + for child in node.childNodes: + remapNamespacePrefix(child, oldprefix, newprefix) def makeWellFormed(str): - # Don't escape quotation marks for now as they are fine in text nodes - # as well as in attributes if used reciprocally - # xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} - xml_ents = { '<':'<', '>':'>', '&':'&'} + # Don't escape quotation marks for now as they are fine in text nodes + # as well as in attributes if used reciprocally + # xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} + xml_ents = {'<': '<', '>': '>', '&': '&'} # starr = [] # for c in str: @@ -3035,687 +3078,683 @@ def makeWellFormed(str): # else: # starr.append(c) - # this list comprehension is short-form for the above for-loop: - return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) - + # this list comprehension is short-form for the above for-loop: + return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) # hand-rolled serialization function that has the following benefits: # - pretty printing # - somewhat judicious use of whitespace # - ensure id attributes are first -def serializeXML(element, options, ind = 0, preserveWhitespace = False): - outParts = [] +def serializeXML(element, options, ind=0, preserveWhitespace=False): + outParts = [] - indent = ind - I='' - newline = '' - if options.newlines: - if options.indent_type == 'tab': I='\t' - elif options.indent_type == 'space': I=' ' - I *= options.indent_depth - newline = '\n' + indent = ind + I = '' + newline = '' + if options.newlines: + if options.indent_type == 'tab': + I = '\t' + elif options.indent_type == 'space': + I = ' ' + I *= options.indent_depth + newline = '\n' - outParts.extend([(I * ind), '<', element.nodeName]) + outParts.extend([(I * ind), '<', element.nodeName]) - # always serialize the id or xml:id attributes first - if element.getAttribute('id') != '': - id = element.getAttribute('id') - quot = '"' - if id.find('"') != -1: - quot = "'" - outParts.extend([' id=', quot, id, quot]) - if element.getAttribute('xml:id') != '': - id = element.getAttribute('xml:id') - quot = '"' - if id.find('"') != -1: - quot = "'" - outParts.extend([' xml:id=', quot, id, quot]) + # always serialize the id or xml:id attributes first + if element.getAttribute('id') != '': + id = element.getAttribute('id') + quot = '"' + if id.find('"') != -1: + quot = "'" + outParts.extend([' id=', quot, id, quot]) + if element.getAttribute('xml:id') != '': + id = element.getAttribute('xml:id') + quot = '"' + if id.find('"') != -1: + quot = "'" + outParts.extend([' xml:id=', quot, id, quot]) - # now serialize the other attributes - known_attr = [ - # TODO: Maybe update with full list from https://www.w3.org/TR/SVG/attindex.html - # (but should be kept inuitively ordered) - 'id', 'class', - 'transform', - 'x', 'y', 'z', 'width', 'height', 'x1', 'x2', 'y1', 'y2', - 'dx', 'dy', 'rotate', 'startOffset', 'method', 'spacing', - 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy', - 'd', 'points', - ] + sorted(svgAttributes) + [ - 'style', - ] - attrList = element.attributes - attrName2Index = dict([(attrList.item(i).nodeName, i) for i in range(attrList.length)]) - # use custom order for known attributes and alphabetical order for the rest - attrIndices = [] - for name in known_attr: - if name in attrName2Index: - attrIndices.append(attrName2Index[name]) - del attrName2Index[name] - attrIndices += [attrName2Index[name] for name in sorted(attrName2Index.keys())] - for index in attrIndices: - attr = attrList.item(index) - if attr.nodeName == 'id' or attr.nodeName == 'xml:id': continue - # if the attribute value contains a double-quote, use single-quotes - quot = '"' - if attr.nodeValue.find('"') != -1: - quot = "'" + # now serialize the other attributes + known_attr = [ + # TODO: Maybe update with full list from https://www.w3.org/TR/SVG/attindex.html + # (but should be kept inuitively ordered) + 'id', 'class', + 'transform', + 'x', 'y', 'z', 'width', 'height', 'x1', 'x2', 'y1', 'y2', + 'dx', 'dy', 'rotate', 'startOffset', 'method', 'spacing', + 'cx', 'cy', 'r', 'rx', 'ry', 'fx', 'fy', + 'd', 'points', + ] + sorted(svgAttributes) + [ + 'style', + ] + attrList = element.attributes + attrName2Index = dict([(attrList.item(i).nodeName, i) for i in range(attrList.length)]) + # use custom order for known attributes and alphabetical order for the rest + attrIndices = [] + for name in known_attr: + if name in attrName2Index: + attrIndices.append(attrName2Index[name]) + del attrName2Index[name] + attrIndices += [attrName2Index[name] for name in sorted(attrName2Index.keys())] + for index in attrIndices: + attr = attrList.item(index) + if attr.nodeName == 'id' or attr.nodeName == 'xml:id': + continue + # if the attribute value contains a double-quote, use single-quotes + quot = '"' + if attr.nodeValue.find('"') != -1: + quot = "'" - attrValue = makeWellFormed( attr.nodeValue ) - if attr.nodeName == 'style': - # sort declarations - attrValue = ';'.join([p for p in sorted(attrValue.split(';'))]) + attrValue = makeWellFormed(attr.nodeValue) + if attr.nodeName == 'style': + # sort declarations + attrValue = ';'.join([p for p in sorted(attrValue.split(';'))]) - outParts.append(' ') - # preserve xmlns: if it is a namespace prefix declaration - if attr.prefix != None: - outParts.extend([attr.prefix, ':']) - elif attr.namespaceURI != None: - if attr.namespaceURI == 'http://www.w3.org/2000/xmlns/' and attr.nodeName.find('xmlns') == -1: - outParts.append('xmlns:') - elif attr.namespaceURI == 'http://www.w3.org/1999/xlink': - outParts.append('xlink:') - outParts.extend([attr.localName, '=', quot, attrValue, quot]) + outParts.append(' ') + # preserve xmlns: if it is a namespace prefix declaration + if attr.prefix != None: + outParts.extend([attr.prefix, ':']) + elif attr.namespaceURI != None: + if attr.namespaceURI == 'http://www.w3.org/2000/xmlns/' and attr.nodeName.find('xmlns') == -1: + outParts.append('xmlns:') + elif attr.namespaceURI == 'http://www.w3.org/1999/xlink': + outParts.append('xlink:') + outParts.extend([attr.localName, '=', quot, attrValue, quot]) - if attr.nodeName == 'xml:space': - if attrValue == 'preserve': - preserveWhitespace = True - elif attrValue == 'default': - preserveWhitespace = False + if attr.nodeName == 'xml:space': + if attrValue == 'preserve': + preserveWhitespace = True + elif attrValue == 'default': + preserveWhitespace = False - # if no children, self-close - children = element.childNodes - if children.length > 0: - outParts.append('>') + # if no children, self-close + children = element.childNodes + if children.length > 0: + outParts.append('>') - onNewLine = False - for child in element.childNodes: - # element node - if child.nodeType == 1: - if preserveWhitespace: - outParts.append(serializeXML(child, options, 0, preserveWhitespace)) - else: - outParts.extend([newline, serializeXML(child, options, indent + 1, preserveWhitespace)]) - onNewLine = True - # text node - elif child.nodeType == 3: - # trim it only in the case of not being a child of an element - # where whitespace might be important - if preserveWhitespace: - outParts.append(makeWellFormed(child.nodeValue)) - else: - outParts.append(makeWellFormed(child.nodeValue.strip())) - # CDATA node - elif child.nodeType == 4: - outParts.extend(['', child.nodeValue, '']) - # Comment node - elif child.nodeType == 8: - outParts.extend(['']) - # TODO: entities, processing instructions, what else? - else: # ignore the rest - pass + onNewLine = False + for child in element.childNodes: + # element node + if child.nodeType == 1: + if preserveWhitespace: + outParts.append(serializeXML(child, options, 0, preserveWhitespace)) + else: + outParts.extend([newline, serializeXML(child, options, indent + 1, preserveWhitespace)]) + onNewLine = True + # text node + elif child.nodeType == 3: + # trim it only in the case of not being a child of an element + # where whitespace might be important + if preserveWhitespace: + outParts.append(makeWellFormed(child.nodeValue)) + else: + outParts.append(makeWellFormed(child.nodeValue.strip())) + # CDATA node + elif child.nodeType == 4: + outParts.extend(['', child.nodeValue, '']) + # Comment node + elif child.nodeType == 8: + outParts.extend(['']) + # TODO: entities, processing instructions, what else? + else: # ignore the rest + pass - if onNewLine: outParts.append(I * ind) - outParts.extend(['']) - if indent > 0: outParts.append(newline) - else: - outParts.append('/>') - if indent > 0: outParts.append(newline) - - return "".join(outParts) + if onNewLine: + outParts.append(I * ind) + outParts.extend(['']) + if indent > 0: + outParts.append(newline) + else: + outParts.append('/>') + if indent > 0: + outParts.append(newline) + return "".join(outParts) # this is the main method # input is a string representation of the input XML # returns a string representation of the output XML def scourString(in_string, options=None): - # sanitize options (take missing attributes from defaults, discard unknown attributes) - options = sanitizeOptions(options) + # sanitize options (take missing attributes from defaults, discard unknown attributes) + options = sanitizeOptions(options) - # create decimal context with reduced precision for scouring numbers - # calculations should be done in the default context (precision defaults to 28 significant digits) to minimize errors - global scouringContext - scouringContext = Context(prec = options.digits) + # create decimal context with reduced precision for scouring numbers + # calculations should be done in the default context (precision defaults to 28 significant digits) to minimize errors + global scouringContext + scouringContext = Context(prec=options.digits) - global numAttrsRemoved - global numStylePropsFixed - global numElemsRemoved - global numBytesSavedInColors - global numCommentsRemoved - global numBytesSavedInIDs - global numBytesSavedInLengths - global numBytesSavedInTransforms - doc = xml.dom.minidom.parseString(in_string) + global numAttrsRemoved + global numStylePropsFixed + global numElemsRemoved + global numBytesSavedInColors + global numCommentsRemoved + global numBytesSavedInIDs + global numBytesSavedInLengths + global numBytesSavedInTransforms + doc = xml.dom.minidom.parseString(in_string) - # determine number of flowRoot elements in input document - # flowRoot elements don't render at all on current browsers (04/2016) - cnt_flowText_el = len(doc.getElementsByTagName('flowRoot')) - if cnt_flowText_el: - errmsg = "SVG input document uses {} flow text elements, which won't render on browsers!".format(cnt_flowText_el) - if options.error_on_flowtext: - raise Exception(errmsg) - else: - print("WARNING: {}".format(errmsg), file = options.ensure_value("stdout", sys.stdout)) + # determine number of flowRoot elements in input document + # flowRoot elements don't render at all on current browsers (04/2016) + cnt_flowText_el = len(doc.getElementsByTagName('flowRoot')) + if cnt_flowText_el: + errmsg = "SVG input document uses {} flow text elements, which won't render on browsers!".format(cnt_flowText_el) + if options.error_on_flowtext: + raise Exception(errmsg) + else: + print("WARNING: {}".format(errmsg), file=options.ensure_value("stdout", sys.stdout)) - # remove descriptive elements - removeDescriptiveElements(doc, options) + # remove descriptive elements + removeDescriptiveElements(doc, options) - # for whatever reason this does not always remove all inkscape/sodipodi attributes/elements - # on the first pass, so we do it multiple times - # does it have to do with removal of children affecting the childlist? - if options.keep_editor_data == False: - while removeNamespacedElements( doc.documentElement, unwanted_ns ) > 0: - pass - while removeNamespacedAttributes( doc.documentElement, unwanted_ns ) > 0: - pass + # for whatever reason this does not always remove all inkscape/sodipodi attributes/elements + # on the first pass, so we do it multiple times + # does it have to do with removal of children affecting the childlist? + if options.keep_editor_data == False: + while removeNamespacedElements(doc.documentElement, unwanted_ns) > 0: + pass + while removeNamespacedAttributes(doc.documentElement, unwanted_ns) > 0: + pass - # remove the xmlns: declarations now - xmlnsDeclsToRemove = [] - attrList = doc.documentElement.attributes - for index in range(attrList.length): - if attrList.item(index).nodeValue in unwanted_ns: - xmlnsDeclsToRemove.append(attrList.item(index).nodeName) + # remove the xmlns: declarations now + xmlnsDeclsToRemove = [] + attrList = doc.documentElement.attributes + for index in range(attrList.length): + if attrList.item(index).nodeValue in unwanted_ns: + xmlnsDeclsToRemove.append(attrList.item(index).nodeName) - for attr in xmlnsDeclsToRemove: - doc.documentElement.removeAttribute(attr) - numAttrsRemoved += 1 + for attr in xmlnsDeclsToRemove: + doc.documentElement.removeAttribute(attr) + numAttrsRemoved += 1 - # ensure namespace for SVG is declared - # TODO: what if the default namespace is something else (i.e. some valid namespace)? - if doc.documentElement.getAttribute('xmlns') != 'http://www.w3.org/2000/svg': - doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') - # TODO: throw error or warning? + # ensure namespace for SVG is declared + # TODO: what if the default namespace is something else (i.e. some valid namespace)? + if doc.documentElement.getAttribute('xmlns') != 'http://www.w3.org/2000/svg': + doc.documentElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg') + # TODO: throw error or warning? - # check for redundant and unused SVG namespace declarations - def xmlnsUnused(prefix, namespace): - if doc.getElementsByTagNameNS(namespace, "*"): - return False - else: - for element in doc.getElementsByTagName("*"): - for attribute in element.attributes.values(): - if attribute.name.startswith(prefix): - return False - return True + # check for redundant and unused SVG namespace declarations + def xmlnsUnused(prefix, namespace): + if doc.getElementsByTagNameNS(namespace, "*"): + return False + else: + for element in doc.getElementsByTagName("*"): + for attribute in element.attributes.values(): + if attribute.name.startswith(prefix): + return False + return True - attrList = doc.documentElement.attributes - xmlnsDeclsToRemove = [] - redundantPrefixes = [] - for i in range(attrList.length): - attr = attrList.item(i) - name = attr.nodeName - val = attr.nodeValue - if name[0:6] == 'xmlns:': - if val == 'http://www.w3.org/2000/svg': - redundantPrefixes.append(name[6:]) - xmlnsDeclsToRemove.append(name) - elif xmlnsUnused(name[6:], val): - xmlnsDeclsToRemove.append(name) + attrList = doc.documentElement.attributes + xmlnsDeclsToRemove = [] + redundantPrefixes = [] + for i in range(attrList.length): + attr = attrList.item(i) + name = attr.nodeName + val = attr.nodeValue + if name[0:6] == 'xmlns:': + if val == 'http://www.w3.org/2000/svg': + redundantPrefixes.append(name[6:]) + xmlnsDeclsToRemove.append(name) + elif xmlnsUnused(name[6:], val): + xmlnsDeclsToRemove.append(name) - for attrName in xmlnsDeclsToRemove: - doc.documentElement.removeAttribute(attrName) - numAttrsRemoved += 1 + for attrName in xmlnsDeclsToRemove: + doc.documentElement.removeAttribute(attrName) + numAttrsRemoved += 1 - for prefix in redundantPrefixes: - remapNamespacePrefix(doc.documentElement, prefix, '') + for prefix in redundantPrefixes: + remapNamespacePrefix(doc.documentElement, prefix, '') - if options.strip_comments: - numCommentsRemoved = removeComments(doc) + if options.strip_comments: + numCommentsRemoved = removeComments(doc) - if options.strip_xml_space_attribute and doc.documentElement.hasAttribute('xml:space'): - doc.documentElement.removeAttribute('xml:space') - numAttrsRemoved += 1 + if options.strip_xml_space_attribute and doc.documentElement.hasAttribute('xml:space'): + doc.documentElement.removeAttribute('xml:space') + numAttrsRemoved += 1 - # repair style (remove unnecessary style properties and change them into XML attributes) - numStylePropsFixed = repairStyle(doc.documentElement, options) + # repair style (remove unnecessary style properties and change them into XML attributes) + numStylePropsFixed = repairStyle(doc.documentElement, options) - # convert colors to #RRGGBB format - if options.simple_colors: - numBytesSavedInColors = convertColors(doc.documentElement) + # convert colors to #RRGGBB format + if options.simple_colors: + numBytesSavedInColors = convertColors(doc.documentElement) - # remove unreferenced gradients/patterns outside of defs - # and most unreferenced elements inside of defs - while removeUnreferencedElements(doc, options.keep_defs) > 0: - pass + # remove unreferenced gradients/patterns outside of defs + # and most unreferenced elements inside of defs + while removeUnreferencedElements(doc, options.keep_defs) > 0: + pass - # remove empty defs, metadata, g - # NOTE: these elements will be removed if they just have whitespace-only text nodes - for tag in ['defs', 'title', 'desc', 'metadata', 'g']: - for elem in doc.documentElement.getElementsByTagName(tag): - removeElem = not elem.hasChildNodes() - if removeElem == False: - for child in elem.childNodes: - if child.nodeType in [1, 4, 8]: - break - elif child.nodeType == 3 and not child.nodeValue.isspace(): - break - else: - removeElem = True - if removeElem: + # remove empty defs, metadata, g + # NOTE: these elements will be removed if they just have whitespace-only text nodes + for tag in ['defs', 'title', 'desc', 'metadata', 'g']: + for elem in doc.documentElement.getElementsByTagName(tag): + removeElem = not elem.hasChildNodes() + if removeElem == False: + for child in elem.childNodes: + if child.nodeType in [1, 4, 8]: + break + elif child.nodeType == 3 and not child.nodeValue.isspace(): + break + else: + removeElem = True + if removeElem: + elem.parentNode.removeChild(elem) + numElemsRemoved += 1 + + if options.strip_ids: + bContinueLooping = True + while bContinueLooping: + identifiedElements = unprotected_ids(doc, options) + referencedIDs = findReferencedElements(doc.documentElement) + bContinueLooping = (removeUnreferencedIDs(referencedIDs, identifiedElements) > 0) + + while removeDuplicateGradientStops(doc) > 0: + pass + + # remove gradients that are only referenced by one other gradient + while collapseSinglyReferencedGradients(doc) > 0: + pass + + # remove duplicate gradients + while removeDuplicateGradients(doc) > 0: + pass + + # create elements if there are runs of elements with the same attributes. + # this MUST be before moveCommonAttributesToParentGroup. + if options.group_create: + createGroupsForCommonAttributes(doc.documentElement) + + # move common attributes to parent group + # NOTE: the if the element's immediate children + # all have the same value for an attribute, it must not + # get moved to the element. The element + # doesn't accept fill=, stroke= etc.! + referencedIds = findReferencedElements(doc.documentElement) + for child in doc.documentElement.childNodes: + numAttrsRemoved += moveCommonAttributesToParentGroup(child, referencedIds) + + # remove unused attributes from parent + numAttrsRemoved += removeUnusedAttributesOnParent(doc.documentElement) + + # Collapse groups LAST, because we've created groups. If done before + # moveAttributesToParentGroup, empty 's may remain. + if options.group_collapse: + while removeNestedGroups(doc.documentElement) > 0: + pass + + # remove unnecessary closing point of polygons and scour points + for polygon in doc.documentElement.getElementsByTagName('polygon'): + cleanPolygon(polygon, options) + + # scour points of polyline + for polyline in doc.documentElement.getElementsByTagName('polyline'): + cleanPolyline(polyline, options) + + # clean path data + for elem in doc.documentElement.getElementsByTagName('path'): + if elem.getAttribute('d') == '': elem.parentNode.removeChild(elem) - numElemsRemoved += 1 + else: + cleanPath(elem, options) - if options.strip_ids: - bContinueLooping = True - while bContinueLooping: - identifiedElements = unprotected_ids(doc, options) - referencedIDs = findReferencedElements(doc.documentElement) - bContinueLooping = (removeUnreferencedIDs(referencedIDs, identifiedElements) > 0) + # shorten ID names as much as possible + if options.shorten_ids: + numBytesSavedInIDs += shortenIDs(doc, options.shorten_ids_prefix, unprotected_ids(doc, options)) - while removeDuplicateGradientStops(doc) > 0: - pass + # scour lengths (including coordinates) + for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', 'linearGradient', 'radialGradient', 'stop', 'filter']: + for elem in doc.getElementsByTagName(type): + for attr in ['x', 'y', 'width', 'height', 'cx', 'cy', 'r', 'rx', 'ry', + 'x1', 'y1', 'x2', 'y2', 'fx', 'fy', 'offset']: + if elem.getAttribute(attr) != '': + elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) - # remove gradients that are only referenced by one other gradient - while collapseSinglyReferencedGradients(doc) > 0: - pass + # more length scouring in this function + numBytesSavedInLengths = reducePrecision(doc.documentElement) - # remove duplicate gradients - while removeDuplicateGradients(doc) > 0: - pass + # remove default values of attributes + numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options) - # create elements if there are runs of elements with the same attributes. - # this MUST be before moveCommonAttributesToParentGroup. - if options.group_create: - createGroupsForCommonAttributes(doc.documentElement) + # reduce the length of transformation attributes + numBytesSavedInTransforms = optimizeTransforms(doc.documentElement, options) - # move common attributes to parent group - # NOTE: the if the element's immediate children - # all have the same value for an attribute, it must not - # get moved to the element. The element - # doesn't accept fill=, stroke= etc.! - referencedIds = findReferencedElements(doc.documentElement) - for child in doc.documentElement.childNodes: - numAttrsRemoved += moveCommonAttributesToParentGroup(child, referencedIds) + # convert rasters references to base64-encoded strings + if options.embed_rasters: + for elem in doc.documentElement.getElementsByTagName('image'): + embedRasters(elem, options) - # remove unused attributes from parent - numAttrsRemoved += removeUnusedAttributesOnParent(doc.documentElement) + # properly size the SVG document (ideally width/height should be 100% with a viewBox) + if options.enable_viewboxing: + properlySizeDoc(doc.documentElement, options) - # Collapse groups LAST, because we've created groups. If done before - # moveAttributesToParentGroup, empty 's may remain. - if options.group_collapse: - while removeNestedGroups(doc.documentElement) > 0: - pass - - # remove unnecessary closing point of polygons and scour points - for polygon in doc.documentElement.getElementsByTagName('polygon'): - cleanPolygon(polygon, options) - - # scour points of polyline - for polyline in doc.documentElement.getElementsByTagName('polyline'): - cleanPolyline(polyline, options) - - # clean path data - for elem in doc.documentElement.getElementsByTagName('path'): - if elem.getAttribute('d') == '': - elem.parentNode.removeChild(elem) - else: - cleanPath(elem, options) - - # shorten ID names as much as possible - if options.shorten_ids: - numBytesSavedInIDs += shortenIDs(doc, options.shorten_ids_prefix, unprotected_ids(doc, options)) - - # scour lengths (including coordinates) - for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', 'linearGradient', 'radialGradient', 'stop', 'filter']: - for elem in doc.getElementsByTagName(type): - for attr in ['x', 'y', 'width', 'height', 'cx', 'cy', 'r', 'rx', 'ry', - 'x1', 'y1', 'x2', 'y2', 'fx', 'fy', 'offset']: - if elem.getAttribute(attr) != '': - elem.setAttribute(attr, scourLength(elem.getAttribute(attr))) - - # more length scouring in this function - numBytesSavedInLengths = reducePrecision(doc.documentElement) - - # remove default values of attributes - numAttrsRemoved += removeDefaultAttributeValues(doc.documentElement, options) - - # reduce the length of transformation attributes - numBytesSavedInTransforms = optimizeTransforms(doc.documentElement, options) - - # convert rasters references to base64-encoded strings - if options.embed_rasters: - for elem in doc.documentElement.getElementsByTagName('image'): - embedRasters(elem, options) - - # properly size the SVG document (ideally width/height should be 100% with a viewBox) - if options.enable_viewboxing: - properlySizeDoc(doc.documentElement, options) - - # output the document as a pretty string with a single space for indent - # NOTE: removed pretty printing because of this problem: - # http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/ - # rolled our own serialize function here to save on space, put id first, customize indentation, etc + # output the document as a pretty string with a single space for indent + # NOTE: removed pretty printing because of this problem: + # http://ronrothman.com/public/leftbraned/xml-dom-minidom-toprettyxml-and-silly-whitespace/ + # rolled our own serialize function here to save on space, put id first, customize indentation, etc # out_string = doc.documentElement.toprettyxml(' ') - out_string = serializeXML(doc.documentElement, options) + '\n' + out_string = serializeXML(doc.documentElement, options) + '\n' - # now strip out empty lines - lines = [] - # Get rid of empty lines - for line in out_string.splitlines(True): - if line.strip(): - lines.append(line) + # now strip out empty lines + lines = [] + # Get rid of empty lines + for line in out_string.splitlines(True): + if line.strip(): + lines.append(line) - # return the string with its XML prolog and surrounding comments - if options.strip_xml_prolog == False: - total_output = '\n' - else: - total_output = "" + # return the string with its XML prolog and surrounding comments + if options.strip_xml_prolog == False: + total_output = '\n' + else: + total_output = "" - for child in doc.childNodes: - if child.nodeType == 1: - total_output += "".join(lines) - else: # doctypes, entities, comments - total_output += child.toxml() + '\n' - - return total_output + for child in doc.childNodes: + if child.nodeType == 1: + total_output += "".join(lines) + else: # doctypes, entities, comments + total_output += child.toxml() + '\n' + return total_output # used mostly by unit tests # input is a filename # returns the minidom doc representation of the SVG def scourXmlFile(filename, options=None): - with open(filename, "rb") as f: - in_string = f.read() - out_string = scourString(in_string, options) + with open(filename, "rb") as f: + in_string = f.read() + out_string = scourString(in_string, options) - doc = xml.dom.minidom.parseString(out_string.encode('utf-8')) + doc = xml.dom.minidom.parseString(out_string.encode('utf-8')) - # since minidom does not seem to parse DTDs properly - # manually declare all attributes with name "id" to be of type ID - # (otherwise things like doc.getElementById() won't work) - all_nodes = doc.getElementsByTagName("*") - for node in all_nodes: - try: - node.setIdAttribute('id') - except: - pass - - return doc + # since minidom does not seem to parse DTDs properly + # manually declare all attributes with name "id" to be of type ID + # (otherwise things like doc.getElementById() won't work) + all_nodes = doc.getElementsByTagName("*") + for node in all_nodes: + try: + node.setIdAttribute('id') + except: + pass + return doc # GZ: Seems most other commandline tools don't do this, is it really wanted? class HeaderedFormatter(optparse.IndentedHelpFormatter): - """ - Show application name, version number, and copyright statement - above usage information. - """ - def format_usage(self, usage): - return "%s %s\n%s\n%s" % (APP, VER, COPYRIGHT, - optparse.IndentedHelpFormatter.format_usage(self, usage)) + """ + Show application name, version number, and copyright statement + above usage information. + """ + def format_usage(self, usage): + return "%s %s\n%s\n%s" % (APP, VER, COPYRIGHT, + optparse.IndentedHelpFormatter.format_usage(self, usage)) # GZ: would prefer this to be in a function or class scope, but tests etc need # access to the defaults anyway _options_parser = optparse.OptionParser( - usage="%prog [INPUT.SVG [OUTPUT.SVG]] [OPTIONS]", - description=("If the input/output files are not specified, stdin/stdout are used. " - "If the input/output files are specified with a svgz extension, " - "then compressed SVG is assumed."), - formatter=HeaderedFormatter(max_help_position=33), - version=VER) + usage="%prog [INPUT.SVG [OUTPUT.SVG]] [OPTIONS]", + description=("If the input/output files are not specified, stdin/stdout are used. " + "If the input/output files are specified with a svgz extension, " + "then compressed SVG is assumed."), + formatter=HeaderedFormatter(max_help_position=33), + version=VER) _options_parser.add_option("-q", "--quiet", - action="store_true", dest="quiet", default=False, - help="suppress non-error output") + action="store_true", dest="quiet", default=False, + help="suppress non-error output") _options_parser.add_option("-v", "--verbose", - action="store_true", dest="verbose", default=False, - help="verbose output (optimization statistics, etc.)") + action="store_true", dest="verbose", default=False, + help="verbose output (optimization statistics, etc.)") _options_parser.add_option("-i", - action="store", dest="infilename", metavar="INPUT.SVG", - help="alternative way to specify input filename") + action="store", dest="infilename", metavar="INPUT.SVG", + help="alternative way to specify input filename") _options_parser.add_option("-o", - action="store", dest="outfilename", metavar="OUTPUT.SVG", - help="alternative way to specify output filename") + action="store", dest="outfilename", metavar="OUTPUT.SVG", + help="alternative way to specify output filename") _option_group_optimization = optparse.OptionGroup(_options_parser, "Optimization") _option_group_optimization.add_option("-p", "--set-precision", - action="store", type=int, dest="digits", default=5, metavar="NUM", - help="set number of significant digits (default: %default)") + action="store", type=int, dest="digits", default=5, metavar="NUM", + help="set number of significant digits (default: %default)") _option_group_optimization.add_option("--disable-simplify-colors", - action="store_false", dest="simple_colors", default=True, - help="won't convert all colors to #RRGGBB format") + action="store_false", dest="simple_colors", default=True, + help="won't convert all colors to #RRGGBB format") _option_group_optimization.add_option("--disable-style-to-xml", - action="store_false", dest="style_to_xml", default=True, - help="won't convert styles into XML attributes") + action="store_false", dest="style_to_xml", default=True, + help="won't convert styles into XML attributes") _option_group_optimization.add_option("--disable-group-collapsing", - action="store_false", dest="group_collapse", default=True, - help="won't collapse elements") + action="store_false", dest="group_collapse", default=True, + help="won't collapse elements") _option_group_optimization.add_option("--create-groups", - action="store_true", dest="group_create", default=False, - help="create elements for runs of elements with identical attributes") + action="store_true", dest="group_create", default=False, + help="create elements for runs of elements with identical attributes") _option_group_optimization.add_option("--keep-editor-data", - action="store_true", dest="keep_editor_data", default=False, - help="won't remove Inkscape, Sodipodi, Adobe Illustrator or Sketch elements and attributes") + action="store_true", dest="keep_editor_data", default=False, + help="won't remove Inkscape, Sodipodi, Adobe Illustrator or Sketch elements and attributes") _option_group_optimization.add_option("--keep-unreferenced-defs", - action="store_true", dest="keep_defs", default=False, - help="won't remove elements within the defs container that are unreferenced") + action="store_true", dest="keep_defs", default=False, + help="won't remove elements within the defs container that are unreferenced") _option_group_optimization.add_option("--renderer-workaround", - action="store_true", dest="renderer_workaround", default=True, - help="work around various renderer bugs (currently only librsvg) (default)") + action="store_true", dest="renderer_workaround", default=True, + help="work around various renderer bugs (currently only librsvg) (default)") _option_group_optimization.add_option("--no-renderer-workaround", - action="store_false", dest="renderer_workaround", default=True, - help="do not work around various renderer bugs (currently only librsvg)") + action="store_false", dest="renderer_workaround", default=True, + help="do not work around various renderer bugs (currently only librsvg)") _options_parser.add_option_group(_option_group_optimization) _option_group_document = optparse.OptionGroup(_options_parser, "SVG document") _option_group_document.add_option("--strip-xml-prolog", - action="store_true", dest="strip_xml_prolog", default=False, - help="won't output the XML prolog ()") + action="store_true", dest="strip_xml_prolog", default=False, + help="won't output the XML prolog ()") _option_group_document.add_option("--remove-titles", - action="store_true", dest="remove_titles", default=False, - help="remove elements") + action="store_true", dest="remove_titles", default=False, + help="remove elements") _option_group_document.add_option("--remove-descriptions", - action="store_true", dest="remove_descriptions", default=False, - help="remove <desc> elements") + action="store_true", dest="remove_descriptions", default=False, + help="remove <desc> elements") _option_group_document.add_option("--remove-metadata", - action="store_true", dest="remove_metadata", default=False, - help="remove <metadata> elements (which may contain license/author information etc.)") + action="store_true", dest="remove_metadata", default=False, + help="remove <metadata> elements (which may contain license/author information etc.)") _option_group_document.add_option("--remove-descriptive-elements", - action="store_true", dest="remove_descriptive_elements", default=False, - help="remove <title>, <desc> and <metadata> elements") + action="store_true", dest="remove_descriptive_elements", default=False, + help="remove <title>, <desc> and <metadata> elements") _option_group_document.add_option("--enable-comment-stripping", - action="store_true", dest="strip_comments", default=False, - help="remove all comments (<!-- -->)") + action="store_true", dest="strip_comments", default=False, + help="remove all comments (<!-- -->)") _option_group_document.add_option("--disable-embed-rasters", - action="store_false", dest="embed_rasters", default=True, - help="won't embed rasters as base64-encoded data") + action="store_false", dest="embed_rasters", default=True, + help="won't embed rasters as base64-encoded data") _option_group_document.add_option("--enable-viewboxing", - action="store_true", dest="enable_viewboxing", default=False, - help="changes document width/height to 100%/100% and creates viewbox coordinates") + action="store_true", dest="enable_viewboxing", default=False, + help="changes document width/height to 100%/100% and creates viewbox coordinates") _options_parser.add_option_group(_option_group_document) _option_group_formatting = optparse.OptionGroup(_options_parser, "Output formatting") _option_group_formatting.add_option("--indent", - action="store", type="string", dest="indent_type", default="space", metavar="TYPE", - help="indentation of the output: none, space, tab (default: %default)") + action="store", type="string", dest="indent_type", default="space", metavar="TYPE", + help="indentation of the output: none, space, tab (default: %default)") _option_group_formatting.add_option("--nindent", - action="store", type=int, dest="indent_depth", default=1, metavar="NUM", - help="depth of the indentation, i.e. number of spaces/tabs: (default: %default)") + action="store", type=int, dest="indent_depth", default=1, metavar="NUM", + help="depth of the indentation, i.e. number of spaces/tabs: (default: %default)") _option_group_formatting.add_option("--no-line-breaks", - action="store_false", dest="newlines", default=True, - help="do not create line breaks in output" - "(also disables indentation; might be overriden by xml:space=\"preserve\")") + action="store_false", dest="newlines", default=True, + help="do not create line breaks in output" + "(also disables indentation; might be overriden by xml:space=\"preserve\")") _option_group_formatting.add_option("--strip-xml-space", - action="store_true", dest="strip_xml_space_attribute", default=False, - help="strip the xml:space=\"preserve\" attribute from the root SVG element") + action="store_true", dest="strip_xml_space_attribute", default=False, + help="strip the xml:space=\"preserve\" attribute from the root SVG element") _options_parser.add_option_group(_option_group_formatting) _option_group_ids = optparse.OptionGroup(_options_parser, "ID attributes") _option_group_ids.add_option("--enable-id-stripping", - action="store_true", dest="strip_ids", default=False, - help="remove all unreferenced IDs") + action="store_true", dest="strip_ids", default=False, + help="remove all unreferenced IDs") _option_group_ids.add_option("--shorten-ids", - action="store_true", dest="shorten_ids", default=False, - help="shorten all IDs to the least number of letters possible") + action="store_true", dest="shorten_ids", default=False, + help="shorten all IDs to the least number of letters possible") _option_group_ids.add_option("--shorten-ids-prefix", - action="store", type="string", dest="shorten_ids_prefix", default="", metavar="PREFIX", - help="add custom prefix to shortened IDs") + action="store", type="string", dest="shorten_ids_prefix", default="", metavar="PREFIX", + help="add custom prefix to shortened IDs") _option_group_ids.add_option("--protect-ids-noninkscape", - action="store_true", dest="protect_ids_noninkscape", default=False, - help="don't remove IDs not ending with a digit") + action="store_true", dest="protect_ids_noninkscape", default=False, + help="don't remove IDs not ending with a digit") _option_group_ids.add_option("--protect-ids-list", - action="store", type="string", dest="protect_ids_list", metavar="LIST", - help="don't remove IDs given in this comma-separated list") + action="store", type="string", dest="protect_ids_list", metavar="LIST", + help="don't remove IDs given in this comma-separated list") _option_group_ids.add_option("--protect-ids-prefix", - action="store", type="string", dest="protect_ids_prefix", metavar="PREFIX", - help="don't remove IDs starting with the given prefix") + action="store", type="string", dest="protect_ids_prefix", metavar="PREFIX", + help="don't remove IDs starting with the given prefix") _options_parser.add_option_group(_option_group_ids) _option_group_compatibility = optparse.OptionGroup(_options_parser, "SVG compatibility checks") _option_group_compatibility.add_option("--error-on-flowtext", - action="store_true", dest="error_on_flowtext", default=False, - help="In case the input SVG uses flow text, bail out with error. Otherwise only warn. (default: False)") + action="store_true", dest="error_on_flowtext", default=False, + help="In case the input SVG uses flow text, bail out with error. Otherwise only warn. (default: False)") _options_parser.add_option_group(_option_group_compatibility) - def parse_args(args=None, ignore_additional_args=False): - options, rargs = _options_parser.parse_args(args) + options, rargs = _options_parser.parse_args(args) - if rargs: - if not options.infilename: - options.infilename = rargs.pop(0) - if not options.outfilename and rargs: - options.outfilename = rargs.pop(0) - if not ignore_additional_args and rargs: - _options_parser.error("Additional arguments not handled: %r, see --help" % rargs) - if options.digits < 0: - _options_parser.error("Can't have negative significant digits, see --help") - if not options.indent_type in ["tab", "space", "none"]: - _options_parser.error("Invalid value for --indent, see --help") - if options.indent_depth < 0: - _options_parser.error("Value for --nindent should be positive (or zero), see --help") - if options.infilename and options.outfilename and options.infilename == options.outfilename: - _options_parser.error("Input filename is the same as output filename") - - return options + if rargs: + if not options.infilename: + options.infilename = rargs.pop(0) + if not options.outfilename and rargs: + options.outfilename = rargs.pop(0) + if not ignore_additional_args and rargs: + _options_parser.error("Additional arguments not handled: %r, see --help" % rargs) + if options.digits < 0: + _options_parser.error("Can't have negative significant digits, see --help") + if not options.indent_type in ["tab", "space", "none"]: + _options_parser.error("Invalid value for --indent, see --help") + if options.indent_depth < 0: + _options_parser.error("Value for --nindent should be positive (or zero), see --help") + if options.infilename and options.outfilename and options.infilename == options.outfilename: + _options_parser.error("Input filename is the same as output filename") + return options def generateDefaultOptions(): - ## FIXME: clean up this mess/hack and refactor arg parsing to argparse - class Struct: - def __init__(self, **entries): - self.__dict__.update(entries) + # FIXME: clean up this mess/hack and refactor arg parsing to argparse + class Struct: - d = parse_args(args = [], ignore_additional_args = True).__dict__.copy() + def __init__(self, **entries): + self.__dict__.update(entries) - return Struct(**d) + d = parse_args(args=[], ignore_additional_args=True).__dict__.copy() + return Struct(**d) # sanitizes options by updating attributes in a set of defaults options while discarding unknown attributes def sanitizeOptions(options): - optionsDict = dict((key, getattr(options, key)) for key in dir(options) if not key.startswith('__')) + optionsDict = dict((key, getattr(options, key)) for key in dir(options) if not key.startswith('__')) - sanitizedOptions = _options_parser.get_default_values() - sanitizedOptions._update_careful(optionsDict) - - return sanitizedOptions + sanitizedOptions = _options_parser.get_default_values() + sanitizedOptions._update_careful(optionsDict) + return sanitizedOptions def maybe_gziped_file(filename, mode="r"): - if os.path.splitext(filename)[1].lower() in (".svgz", ".gz"): - import gzip - return gzip.GzipFile(filename, mode) - return open(filename, mode) - + if os.path.splitext(filename)[1].lower() in (".svgz", ".gz"): + import gzip + return gzip.GzipFile(filename, mode) + return open(filename, mode) def getInOut(options): - if options.infilename: - infile = maybe_gziped_file(options.infilename, "rb") - # GZ: could catch a raised IOError here and report - else: - # GZ: could sniff for gzip compression here - # - # open the binary buffer of stdin and let XML parser handle decoding - try: - infile = sys.stdin.buffer - except AttributeError: - infile = sys.stdin - # the user probably does not want to manually enter SVG code into the terminal... - if sys.stdin.isatty(): - _options_parser.error("No input file specified, see --help for detailed usage information") + if options.infilename: + infile = maybe_gziped_file(options.infilename, "rb") + # GZ: could catch a raised IOError here and report + else: + # GZ: could sniff for gzip compression here + # + # open the binary buffer of stdin and let XML parser handle decoding + try: + infile = sys.stdin.buffer + except AttributeError: + infile = sys.stdin + # the user probably does not want to manually enter SVG code into the terminal... + if sys.stdin.isatty(): + _options_parser.error("No input file specified, see --help for detailed usage information") - if options.outfilename: - outfile = maybe_gziped_file(options.outfilename, "wb") - else: - # open the binary buffer of stdout as the output is already encoded - try: - outfile = sys.stdout.buffer - except AttributeError: - outfile = sys.stdout - # redirect informational output to stderr when SVG is output to stdout - options.stdout = sys.stderr - - return [infile, outfile] + if options.outfilename: + outfile = maybe_gziped_file(options.outfilename, "wb") + else: + # open the binary buffer of stdout as the output is already encoded + try: + outfile = sys.stdout.buffer + except AttributeError: + outfile = sys.stdout + # redirect informational output to stderr when SVG is output to stdout + options.stdout = sys.stderr + return [infile, outfile] def getReport(): - return ' Number of elements removed: ' + str(numElemsRemoved) + os.linesep + \ - ' Number of attributes removed: ' + str(numAttrsRemoved) + os.linesep + \ - ' Number of unreferenced id attributes removed: ' + str(numIDsRemoved) + os.linesep + \ - ' Number of style properties fixed: ' + str(numStylePropsFixed) + os.linesep + \ - ' Number of raster images embedded inline: ' + str(numRastersEmbedded) + os.linesep + \ - ' Number of path segments reduced/removed: ' + str(numPathSegmentsReduced) + os.linesep + \ - ' Number of bytes saved in path data: ' + str(numBytesSavedInPathData) + os.linesep + \ - ' Number of bytes saved in colors: ' + str(numBytesSavedInColors) + os.linesep + \ - ' Number of points removed from polygons: ' + str(numPointsRemovedFromPolygon) + os.linesep + \ - ' Number of bytes saved in comments: ' + str(numCommentBytes) + os.linesep + \ - ' Number of bytes saved in id attributes: ' + str(numBytesSavedInIDs) + os.linesep + \ - ' Number of bytes saved in lengths: ' + str(numBytesSavedInLengths) + os.linesep + \ - ' Number of bytes saved in transformations: ' + str(numBytesSavedInTransforms) - + return ' Number of elements removed: ' + str(numElemsRemoved) + os.linesep + \ + ' Number of attributes removed: ' + str(numAttrsRemoved) + os.linesep + \ + ' Number of unreferenced id attributes removed: ' + str(numIDsRemoved) + os.linesep + \ + ' Number of style properties fixed: ' + str(numStylePropsFixed) + os.linesep + \ + ' Number of raster images embedded inline: ' + str(numRastersEmbedded) + os.linesep + \ + ' Number of path segments reduced/removed: ' + str(numPathSegmentsReduced) + os.linesep + \ + ' Number of bytes saved in path data: ' + str(numBytesSavedInPathData) + os.linesep + \ + ' Number of bytes saved in colors: ' + str(numBytesSavedInColors) + os.linesep + \ + ' Number of points removed from polygons: ' + str(numPointsRemovedFromPolygon) + os.linesep + \ + ' Number of bytes saved in comments: ' + str(numCommentBytes) + os.linesep + \ + ' Number of bytes saved in id attributes: ' + str(numBytesSavedInIDs) + os.linesep + \ + ' Number of bytes saved in lengths: ' + str(numBytesSavedInLengths) + os.linesep + \ + ' Number of bytes saved in transformations: ' + str(numBytesSavedInTransforms) def start(options, input, output): - start = walltime() + start = walltime() - # do the work - in_string = input.read() - out_string = scourString(in_string, options).encode("UTF-8") - output.write(out_string) + # do the work + in_string = input.read() + out_string = scourString(in_string, options).encode("UTF-8") + output.write(out_string) - # Close input and output files - input.close() - output.close() + # Close input and output files + input.close() + output.close() - end = walltime() + end = walltime() - # run-time in ms - duration = int(round((end - start) * 1000.)) + # run-time in ms + duration = int(round((end - start) * 1000.)) - oldsize = len(in_string) - newsize = len(out_string) - sizediff = (newsize / oldsize) * 100. + oldsize = len(in_string) + newsize = len(out_string) + sizediff = (newsize / oldsize) * 100. - if not options.quiet: - print('Scour processed file "{}" in {} ms: {}/{} bytes new/orig -> {:.1f}%'.format( - input.name, - duration, - newsize, - oldsize, - sizediff), file = options.ensure_value("stdout", sys.stdout)) - if options.verbose: - print(getReport(), file = options.ensure_value("stdout", sys.stdout)) + if not options.quiet: + print('Scour processed file "{}" in {} ms: {}/{} bytes new/orig -> {:.1f}%'.format( + input.name, + duration, + newsize, + oldsize, + sizediff), file=options.ensure_value("stdout", sys.stdout)) + if options.verbose: + print(getReport(), file=options.ensure_value("stdout", sys.stdout)) def run(): - options = parse_args() - (input, output) = getInOut(options) - start(options, input, output) + options = parse_args() + (input, output) = getInOut(options) + start(options, input, output) if __name__ == '__main__': - run() + run() diff --git a/scour/svg_regex.py b/scour/svg_regex.py index e6659e5..220dffb 100644 --- a/scour/svg_regex.py +++ b/scour/svg_regex.py @@ -48,7 +48,10 @@ from decimal import * from functools import partial # Sentinel. + + class _EOF(object): + def __repr__(self): return 'EOF' EOF = _EOF() @@ -70,6 +73,7 @@ class Lexer(object): http://www.gooli.org/blog/a-simple-lexer-in-python/ """ + def __init__(self, lexicon): self.lexicon = lexicon parts = [] @@ -270,7 +274,6 @@ class SVGPathParser(object): token = next_val_fn() return x, token - def rule_coordinate_pair(self, next_val_fn, token): # Inline these since this rule is so common. if token[0] not in self.number_tokens: diff --git a/scour/svg_transform.py b/scour/svg_transform.py index 85507ca..6ae3701 100644 --- a/scour/svg_transform.py +++ b/scour/svg_transform.py @@ -66,6 +66,7 @@ from functools import partial # Sentinel. class _EOF(object): + def __repr__(self): return 'EOF' EOF = _EOF() @@ -89,6 +90,7 @@ class Lexer(object): http://www.gooli.org/blog/a-simple-lexer-in-python/ """ + def __init__(self, lexicon): self.lexicon = lexicon parts = [] @@ -154,8 +156,8 @@ class SVGTransformationParser(object): commands = [] token = next_val_fn() while token[0] is not EOF: - command, token = self.rule_svg_transform(next_val_fn, token) - commands.append(command) + command, token = self.rule_svg_transform(next_val_fn, token) + commands.append(command) return commands def rule_svg_transform(self, next_val_fn, token): diff --git a/scour/yocto_css.py b/scour/yocto_css.py index 3efeeda..0aaac5a 100644 --- a/scour/yocto_css.py +++ b/scour/yocto_css.py @@ -48,25 +48,29 @@ # | DASHMATCH | FUNCTION S* any* ')' # | '(' S* any* ')' | '[' S* any* ']' ] S*; + def parseCssString(str): - rules = [] - # first, split on } to get the rule chunks - chunks = str.split('}') - for chunk in chunks: - # second, split on { to get the selector and the list of properties - bits = chunk.split('{') - if len(bits) != 2: continue - rule = {} - rule['selector'] = bits[0].strip() - # third, split on ; to get the property declarations - bites = bits[1].strip().split(';') - if len(bites) < 1: continue - props = {} - for bite in bites: - # fourth, split on : to get the property name and value - nibbles = bite.strip().split(':') - if len(nibbles) != 2: continue - props[nibbles[0].strip()] = nibbles[1].strip() - rule['properties'] = props - rules.append(rule) - return rules + rules = [] + # first, split on } to get the rule chunks + chunks = str.split('}') + for chunk in chunks: + # second, split on { to get the selector and the list of properties + bits = chunk.split('{') + if len(bits) != 2: + continue + rule = {} + rule['selector'] = bits[0].strip() + # third, split on ; to get the property declarations + bites = bits[1].strip().split(';') + if len(bites) < 1: + continue + props = {} + for bite in bites: + # fourth, split on : to get the property name and value + nibbles = bite.strip().split(':') + if len(nibbles) != 2: + continue + props[nibbles[0].strip()] = nibbles[1].strip() + rule['properties'] = props + rules.append(rule) + return rules diff --git a/setup.py b/setup.py index c14779a..3fe0831 100644 --- a/setup.py +++ b/setup.py @@ -1,19 +1,19 @@ ############################################################################### -## -## Copyright (C) 2013-2014 Tavendo GmbH -## -## 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. -## +# +# Copyright (C) 2013-2014 Tavendo GmbH +# +# 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. +# ############################################################################### import os @@ -42,35 +42,35 @@ else: raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) -setup ( - name = 'scour', - version = verstr, - description = 'Scour SVG Optimizer', -# long_description = open("README.md").read(), - long_description = LONGDESC, - license = 'Apache License 2.0', - author = 'Jeff Schiller', - author_email = 'codedread@gmail.com', - url = 'https://github.com/codedread/scour', - platforms = ('Any'), - install_requires = ['six>=1.9.0'], - packages = find_packages(), - zip_safe = True, - entry_points = { - 'console_scripts': [ - 'scour = scour.scour:run' - ]}, - classifiers = ["License :: OSI Approved :: Apache Software License", - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: System Administrators", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Topic :: Internet", - "Topic :: Software Development :: Build Tools", - "Topic :: Software Development :: Pre-processors", - "Topic :: Multimedia :: Graphics :: Graphics Conversion", - "Topic :: Utilities"], - keywords = 'svg optimizer' +setup( + name='scour', + version=verstr, + description='Scour SVG Optimizer', + # long_description = open("README.md").read(), + long_description=LONGDESC, + license='Apache License 2.0', + author='Jeff Schiller', + author_email='codedread@gmail.com', + url='https://github.com/codedread/scour', + platforms=('Any'), + install_requires=['six>=1.9.0'], + packages=find_packages(), + zip_safe=True, + entry_points={ + 'console_scripts': [ + 'scour = scour.scour:run' + ]}, + classifiers=["License :: OSI Approved :: Apache Software License", + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: System Administrators", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Topic :: Internet", + "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Pre-processors", + "Topic :: Multimedia :: Graphics :: Graphics Conversion", + "Topic :: Utilities"], + keywords='svg optimizer' ) diff --git a/testcss.py b/testcss.py index 88d5195..a342b5e 100755 --- a/testcss.py +++ b/testcss.py @@ -27,25 +27,30 @@ from scour.yocto_css import parseCssString class Blank(unittest.TestCase): - def runTest(self): - r = parseCssString('') - self.assertEqual( len(r), 0, 'Blank string returned non-empty list') - self.assertEqual( type(r), type([]), 'Blank string returned non list') + + def runTest(self): + r = parseCssString('') + self.assertEqual(len(r), 0, 'Blank string returned non-empty list') + self.assertEqual(type(r), type([]), 'Blank string returned non list') + class ElementSelector(unittest.TestCase): - def runTest(self): - r = parseCssString('foo {}') - self.assertEqual( len(r), 1, 'Element selector not returned') - self.assertEqual( r[0]['selector'], 'foo', 'Selector for foo not returned') - self.assertEqual( len(r[0]['properties']), 0, 'Property list for foo not empty') + + def runTest(self): + r = parseCssString('foo {}') + self.assertEqual(len(r), 1, 'Element selector not returned') + self.assertEqual(r[0]['selector'], 'foo', 'Selector for foo not returned') + self.assertEqual(len(r[0]['properties']), 0, 'Property list for foo not empty') + class ElementSelectorWithProperty(unittest.TestCase): - def runTest(self): - r = parseCssString('foo { bar: baz}') - self.assertEqual( len(r), 1, 'Element selector not returned') - self.assertEqual( r[0]['selector'], 'foo', 'Selector for foo not returned') - self.assertEqual( len(r[0]['properties']), 1, 'Property list for foo did not have 1') - self.assertEqual( r[0]['properties']['bar'], 'baz', 'Property bar did not have baz value') + + def runTest(self): + r = parseCssString('foo { bar: baz}') + self.assertEqual(len(r), 1, 'Element selector not returned') + self.assertEqual(r[0]['selector'], 'foo', 'Selector for foo not returned') + self.assertEqual(len(r[0]['properties']), 1, 'Property list for foo did not have 1') + self.assertEqual(r[0]['properties']['bar'], 'baz', 'Property bar did not have baz value') if __name__ == '__main__': unittest.main() diff --git a/testscour.py b/testscour.py index 28b3148..adc2021 100755 --- a/testscour.py +++ b/testscour.py @@ -39,1549 +39,2006 @@ SVGNS = 'http://www.w3.org/2000/svg' # so I decided to use minidom and this helper function that performs a test on a given node # and all its children # func must return either True (if pass) or False (if fail) + + def walkTree(elem, func): - if func(elem) == False: return False - for child in elem.childNodes: - if walkTree(child, func) == False: return False - return True + if func(elem) == False: + return False + for child in elem.childNodes: + if walkTree(child, func) == False: + return False + return True class ScourOptions: - pass + pass class EmptyOptions(unittest.TestCase): - def runTest(self): - options = ScourOptions - try: - scour.scourXmlFile('unittests/ids-to-strip.svg', options) - fail = False - except: - fail = True - self.assertEqual(fail, False, 'Exception when calling Scour with empty options object') + + def runTest(self): + options = ScourOptions + try: + scour.scourXmlFile('unittests/ids-to-strip.svg', options) + fail = False + except: + fail = True + self.assertEqual(fail, False, 'Exception when calling Scour with empty options object') + class InvalidOptions(unittest.TestCase): - def runTest(self): - options = ScourOptions - options.invalidOption = "invalid value" - try: - scour.scourXmlFile('unittests/ids-to-strip.svg', options) - fail = False - except: - fail = True - self.assertEqual(fail, False, 'Exception when calling Scour with invalid options') + + def runTest(self): + options = ScourOptions + options.invalidOption = "invalid value" + try: + scour.scourXmlFile('unittests/ids-to-strip.svg', options) + fail = False + except: + fail = True + self.assertEqual(fail, False, 'Exception when calling Scour with invalid options') + class GetElementById(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/ids.svg') - self.assertIsNotNone(doc.getElementById('svg1'), 'Root SVG element not found by ID') - self.assertIsNotNone(doc.getElementById('linearGradient1'), 'linearGradient not found by ID') - self.assertIsNotNone(doc.getElementById('layer1'), 'g not found by ID') - self.assertIsNotNone(doc.getElementById('rect1'), 'rect not found by ID') - self.assertIsNone(doc.getElementById('rect2'), 'Non-existing element found by ID') + + def runTest(self): + doc = scour.scourXmlFile('unittests/ids.svg') + self.assertIsNotNone(doc.getElementById('svg1'), 'Root SVG element not found by ID') + self.assertIsNotNone(doc.getElementById('linearGradient1'), 'linearGradient not found by ID') + self.assertIsNotNone(doc.getElementById('layer1'), 'g not found by ID') + self.assertIsNotNone(doc.getElementById('rect1'), 'rect not found by ID') + self.assertIsNone(doc.getElementById('rect2'), 'Non-existing element found by ID') + class NoInkscapeElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, - lambda e: e.namespaceURI != 'http://www.inkscape.org/namespaces/inkscape'), False, - 'Found Inkscape elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, + lambda e: e.namespaceURI != 'http://www.inkscape.org/namespaces/inkscape'), False, + 'Found Inkscape elements') + + class NoSodipodiElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, - lambda e: e.namespaceURI != 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'), False, - 'Found Sodipodi elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, + lambda e: e.namespaceURI != 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'), False, + 'Found Sodipodi elements') + + class NoAdobeIllustratorElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeIllustrator/10.0/'), False, - 'Found Adobe Illustrator elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeIllustrator/10.0/'), False, + 'Found Adobe Illustrator elements') + + class NoAdobeGraphsElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Graphs/1.0/'), False, - 'Found Adobe Graphs elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Graphs/1.0/'), False, + 'Found Adobe Graphs elements') + + class NoAdobeSVGViewerElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/'), False, - 'Found Adobe SVG Viewer elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/'), False, + 'Found Adobe SVG Viewer elements') + + class NoAdobeVariablesElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Variables/1.0/'), False, - 'Found Adobe Variables elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Variables/1.0/'), False, + 'Found Adobe Variables elements') + + class NoAdobeSaveForWebElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/SaveForWeb/1.0/'), False, - 'Found Adobe Save For Web elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/SaveForWeb/1.0/'), False, + 'Found Adobe Save For Web elements') + + class NoAdobeExtensibilityElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Extensibility/1.0/'), False, - 'Found Adobe Extensibility elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Extensibility/1.0/'), False, + 'Found Adobe Extensibility elements') + + class NoAdobeFlowsElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Flows/1.0/'), False, - 'Found Adobe Flows elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Flows/1.0/'), False, + 'Found Adobe Flows elements') + + class NoAdobeImageReplacementElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/ImageReplacement/1.0/'), False, - 'Found Adobe Image Replacement elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/ImageReplacement/1.0/'), False, + 'Found Adobe Image Replacement elements') + + class NoAdobeCustomElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/GenericCustomNamespace/1.0/'), False, - 'Found Adobe Custom elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/GenericCustomNamespace/1.0/'), False, + 'Found Adobe Custom elements') + + class NoAdobeXPathElements(unittest.TestCase): - def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), False, - 'Found Adobe XPath elements' ) + + def runTest(self): + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), False, + 'Found Adobe XPath elements') + class DoNotRemoveTitleWithOnlyText(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Removed title element with only text child' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, + 'Removed title element with only text child') + class RemoveEmptyTitleElement(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, - 'Did not remove empty title element' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, + 'Did not remove empty title element') + class DoNotRemoveDescriptionWithOnlyText(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, - 'Removed description element with only text child' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, + 'Removed description element with only text child') + class RemoveEmptyDescriptionElement(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, - 'Did not remove empty description element' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, + 'Did not remove empty description element') + class DoNotRemoveMetadataWithOnlyText(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, - 'Removed metadata element with only text child' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, + 'Removed metadata element with only text child') + class RemoveEmptyMetadataElement(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, - 'Did not remove empty metadata element' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, + 'Did not remove empty metadata element') + class DoNotRemoveDescriptiveElementsWithOnlyText(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Removed title element with only text child' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, - 'Removed description element with only text child') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, - 'Removed metadata element with only text child' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, + 'Removed title element with only text child') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, + 'Removed description element with only text child') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, + 'Removed metadata element with only text child') + class RemoveEmptyDescriptiveElements(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, - 'Did not remove empty title element' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, - 'Did not remove empty description element' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, - 'Did not remove empty metadata element' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, + 'Did not remove empty title element') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, + 'Did not remove empty description element') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, + 'Did not remove empty metadata element') + class RemoveEmptyGElements(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/empty-g.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, - 'Did not remove empty g element' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/empty-g.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, + 'Did not remove empty g element') + class RemoveUnreferencedPattern(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-pattern.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, - 'Unreferenced pattern not removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/unreferenced-pattern.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, + 'Unreferenced pattern not removed') + class RemoveUnreferencedLinearGradient(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-linearGradient.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Unreferenced linearGradient not removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/unreferenced-linearGradient.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, + 'Unreferenced linearGradient not removed') + class RemoveUnreferencedRadialGradient(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-radialGradient.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialradient')), 0, - 'Unreferenced radialGradient not removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/unreferenced-radialGradient.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialradient')), 0, + 'Unreferenced radialGradient not removed') + class RemoveUnreferencedElementInDefs(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/referenced-elements-1.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Unreferenced rect left in defs' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/referenced-elements-1.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, + 'Unreferenced rect left in defs') + class RemoveUnreferencedDefs(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-defs.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Referenced linearGradient removed from defs' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 0, - 'Unreferenced radialGradient left in defs' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, - 'Unreferenced pattern left in defs' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Referenced rect removed from defs' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 0, - 'Unreferenced circle left in defs' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/unreferenced-defs.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, + 'Referenced linearGradient removed from defs') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 0, + 'Unreferenced radialGradient left in defs') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, + 'Unreferenced pattern left in defs') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, + 'Referenced rect removed from defs') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 0, + 'Unreferenced circle left in defs') + class KeepUnreferencedDefs(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-defs.svg', - scour.parse_args(['--keep-unreferenced-defs'])) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Referenced linearGradient removed from defs with `--keep-unreferenced-defs`' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 1, - 'Unreferenced radialGradient removed from defs with `--keep-unreferenced-defs`' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 1, - 'Unreferenced pattern removed from defs with `--keep-unreferenced-defs`' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, - 'Referenced rect removed from defs with `--keep-unreferenced-defs`' ) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 1, - 'Unreferenced circle removed from defs with `--keep-unreferenced-defs`' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/unreferenced-defs.svg', + scour.parse_args(['--keep-unreferenced-defs'])) + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, + 'Referenced linearGradient removed from defs with `--keep-unreferenced-defs`') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 1, + 'Unreferenced radialGradient removed from defs with `--keep-unreferenced-defs`') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 1, + 'Unreferenced pattern removed from defs with `--keep-unreferenced-defs`') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, + 'Referenced rect removed from defs with `--keep-unreferenced-defs`') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'circle')), 1, + 'Unreferenced circle removed from defs with `--keep-unreferenced-defs`') + class DoNotRemoveChainedRefsInDefs(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/refs-in-defs.svg') - g = doc.getElementsByTagNameNS(SVGNS, 'g')[0] - self.assertEqual( g.childNodes.length >= 2, True, - 'Chained references not honored in defs' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/refs-in-defs.svg') + g = doc.getElementsByTagNameNS(SVGNS, 'g')[0] + self.assertEqual(g.childNodes.length >= 2, True, + 'Chained references not honored in defs') + class KeepTitleInDefs(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/referenced-elements-1.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, - 'Title removed from in defs' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/referenced-elements-1.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, + 'Title removed from in defs') + class RemoveNestedDefs(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/nested-defs.svg') - allDefs = doc.getElementsByTagNameNS(SVGNS, 'defs') - self.assertEqual(len(allDefs), 1, 'More than one defs left in doc') + + def runTest(self): + doc = scour.scourXmlFile('unittests/nested-defs.svg') + allDefs = doc.getElementsByTagNameNS(SVGNS, 'defs') + self.assertEqual(len(allDefs), 1, 'More than one defs left in doc') + class KeepUnreferencedIDsWhenEnabled(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/ids-to-strip.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), 'boo', - '<svg> ID stripped when it should be disabled' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/ids-to-strip.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), 'boo', + '<svg> ID stripped when it should be disabled') + class RemoveUnreferencedIDsWhenEnabled(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/ids-to-strip.svg', - scour.parse_args(['--enable-id-stripping'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), '', - '<svg> ID not stripped' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/ids-to-strip.svg', + scour.parse_args(['--enable-id-stripping'])) + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), '', + '<svg> ID not stripped') + class RemoveUselessNestedGroups(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/nested-useless-groups.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, - 'Useless nested groups not removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/nested-useless-groups.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, + 'Useless nested groups not removed') + class DoNotRemoveUselessNestedGroups(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/nested-useless-groups.svg', - scour.parse_args(['--disable-group-collapsing'])) - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Useless nested groups were removed despite --disable-group-collapsing' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/nested-useless-groups.svg', + scour.parse_args(['--disable-group-collapsing'])) + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, + 'Useless nested groups were removed despite --disable-group-collapsing') + class DoNotRemoveNestedGroupsWithTitle(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/groups-with-title-desc.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Nested groups with title was removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/groups-with-title-desc.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, + 'Nested groups with title was removed') + class DoNotRemoveNestedGroupsWithDesc(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/groups-with-title-desc.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, - 'Nested groups with desc was removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/groups-with-title-desc.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, + 'Nested groups with desc was removed') + class RemoveDuplicateLinearGradientStops(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/duplicate-gradient-stops.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate linear gradient stops not removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/duplicate-gradient-stops.svg') + grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') + self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, + 'Duplicate linear gradient stops not removed') + class RemoveDuplicateLinearGradientStopsPct(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/duplicate-gradient-stops-pct.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate linear gradient stops with percentages not removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/duplicate-gradient-stops-pct.svg') + grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') + self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, + 'Duplicate linear gradient stops with percentages not removed') + class RemoveDuplicateRadialGradientStops(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/duplicate-gradient-stops.svg') - grad = doc.getElementsByTagNameNS(SVGNS, 'radialGradient') - self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, - 'Duplicate radial gradient stops not removed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/duplicate-gradient-stops.svg') + grad = doc.getElementsByTagNameNS(SVGNS, 'radialGradient') + self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, + 'Duplicate radial gradient stops not removed') + class NoSodipodiNamespaceDecl(unittest.TestCase): - def runTest(self): - attrs = scour.scourXmlFile('unittests/sodipodi.svg').documentElement.attributes - for i in range(len(attrs)): - self.assertNotEqual(attrs.item(i).nodeValue, - 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', - 'Sodipodi namespace declaration found' ) + + def runTest(self): + attrs = scour.scourXmlFile('unittests/sodipodi.svg').documentElement.attributes + for i in range(len(attrs)): + self.assertNotEqual(attrs.item(i).nodeValue, + 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + 'Sodipodi namespace declaration found') + class NoInkscapeNamespaceDecl(unittest.TestCase): - def runTest(self): - attrs = scour.scourXmlFile('unittests/inkscape.svg').documentElement.attributes - for i in range(len(attrs)): - self.assertNotEqual(attrs.item(i).nodeValue, - 'http://www.inkscape.org/namespaces/inkscape', - 'Inkscape namespace declaration found' ) + + def runTest(self): + attrs = scour.scourXmlFile('unittests/inkscape.svg').documentElement.attributes + for i in range(len(attrs)): + self.assertNotEqual(attrs.item(i).nodeValue, + 'http://www.inkscape.org/namespaces/inkscape', + 'Inkscape namespace declaration found') + class NoSodipodiAttributes(unittest.TestCase): - def runTest(self): - def findSodipodiAttr(elem): - attrs = elem.attributes - if attrs == None: return True - for i in range(len(attrs)): - if attrs.item(i).namespaceURI == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': - return False - return True - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, - findSodipodiAttr), False, - 'Found Sodipodi attributes' ) + + def runTest(self): + def findSodipodiAttr(elem): + attrs = elem.attributes + if attrs == None: + return True + for i in range(len(attrs)): + if attrs.item(i).namespaceURI == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': + return False + return True + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, + findSodipodiAttr), False, + 'Found Sodipodi attributes') + class NoInkscapeAttributes(unittest.TestCase): - def runTest(self): - def findInkscapeAttr(elem): - attrs = elem.attributes - if attrs == None: return True - for i in range(len(attrs)): - if attrs.item(i).namespaceURI == 'http://www.inkscape.org/namespaces/inkscape': - return False - return True - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement, - findInkscapeAttr), False, - 'Found Inkscape attributes' ) + + def runTest(self): + def findInkscapeAttr(elem): + attrs = elem.attributes + if attrs == None: + return True + for i in range(len(attrs)): + if attrs.item(i).namespaceURI == 'http://www.inkscape.org/namespaces/inkscape': + return False + return True + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement, + findInkscapeAttr), False, + 'Found Inkscape attributes') + class KeepInkscapeNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): - def runTest(self): - options = ScourOptions - options.keep_editor_data = True - attrs = scour.scourXmlFile('unittests/inkscape.svg', options).documentElement.attributes - FoundNamespace = False - for i in range(len(attrs)): - if attrs.item(i).nodeValue == 'http://www.inkscape.org/namespaces/inkscape': - FoundNamespace = True - break - self.assertEqual(True, FoundNamespace, - "Did not find Inkscape namespace declaration when using --keep-editor-data") - return False + + def runTest(self): + options = ScourOptions + options.keep_editor_data = True + attrs = scour.scourXmlFile('unittests/inkscape.svg', options).documentElement.attributes + FoundNamespace = False + for i in range(len(attrs)): + if attrs.item(i).nodeValue == 'http://www.inkscape.org/namespaces/inkscape': + FoundNamespace = True + break + self.assertEqual(True, FoundNamespace, + "Did not find Inkscape namespace declaration when using --keep-editor-data") + return False + class KeepSodipodiNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): - def runTest(self): - options = ScourOptions - options.keep_editor_data = True - attrs = scour.scourXmlFile('unittests/sodipodi.svg', options).documentElement.attributes - FoundNamespace = False - for i in range(len(attrs)): - if attrs.item(i).nodeValue == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': - FoundNamespace = True - break - self.assertEqual(True, FoundNamespace, - "Did not find Sodipodi namespace declaration when using --keep-editor-data") - return False + + def runTest(self): + options = ScourOptions + options.keep_editor_data = True + attrs = scour.scourXmlFile('unittests/sodipodi.svg', options).documentElement.attributes + FoundNamespace = False + for i in range(len(attrs)): + if attrs.item(i).nodeValue == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': + FoundNamespace = True + break + self.assertEqual(True, FoundNamespace, + "Did not find Sodipodi namespace declaration when using --keep-editor-data") + return False + class KeepReferencedFonts(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/referenced-font.svg') - fonts = doc.documentElement.getElementsByTagNameNS(SVGNS,'font') - self.assertEqual(len(fonts), 1, - 'Font wrongly removed from <defs>' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/referenced-font.svg') + fonts = doc.documentElement.getElementsByTagNameNS(SVGNS, 'font') + self.assertEqual(len(fonts), 1, + 'Font wrongly removed from <defs>') + class ConvertStyleToAttrs(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('style'), '', - 'style attribute not emptied' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('style'), '', + 'style attribute not emptied') + class RemoveStrokeWhenStrokeTransparent(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when stroke opacity zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', + 'stroke attribute not emptied when stroke opacity zero') + class RemoveStrokeWidthWhenStrokeTransparent(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', - 'stroke-width attribute not emptied when stroke opacity zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', + 'stroke-width attribute not emptied when stroke opacity zero') + class RemoveStrokeLinecapWhenStrokeTransparent(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when stroke opacity zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', + 'stroke-linecap attribute not emptied when stroke opacity zero') + class RemoveStrokeLinejoinWhenStrokeTransparent(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when stroke opacity zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', + 'stroke-linejoin attribute not emptied when stroke opacity zero') + class RemoveStrokeDasharrayWhenStrokeTransparent(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when stroke opacity zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', + 'stroke-dasharray attribute not emptied when stroke opacity zero') + class RemoveStrokeDashoffsetWhenStrokeTransparent(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when stroke opacity zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', + 'stroke-dashoffset attribute not emptied when stroke opacity zero') + class RemoveStrokeWhenStrokeWidthZero(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when width zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', + 'stroke attribute not emptied when width zero') + class RemoveStrokeOpacityWhenStrokeWidthZero(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', - 'stroke-opacity attribute not emptied when width zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', + 'stroke-opacity attribute not emptied when width zero') + class RemoveStrokeLinecapWhenStrokeWidthZero(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when width zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', + 'stroke-linecap attribute not emptied when width zero') + class RemoveStrokeLinejoinWhenStrokeWidthZero(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when width zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', + 'stroke-linejoin attribute not emptied when width zero') + class RemoveStrokeDasharrayWhenStrokeWidthZero(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when width zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', + 'stroke-dasharray attribute not emptied when width zero') + class RemoveStrokeDashoffsetWhenStrokeWidthZero(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when width zero' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', + 'stroke-dashoffset attribute not emptied when width zero') + class RemoveStrokeWhenStrokeNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', - 'stroke attribute not emptied when no stroke' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', + 'stroke attribute not emptied when no stroke') + class KeepStrokeWhenInheritedFromParent(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('p1').getAttribute('stroke'), 'none', - 'stroke attribute removed despite a different value being inherited from a parent' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementById('p1').getAttribute('stroke'), 'none', + 'stroke attribute removed despite a different value being inherited from a parent') + class KeepStrokeWhenInheritedByChild(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('g2').getAttribute('stroke'), 'none', - 'stroke attribute removed despite it being inherited by a child' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementById('g2').getAttribute('stroke'), 'none', + 'stroke attribute removed despite it being inherited by a child') + class RemoveStrokeWidthWhenStrokeNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', - 'stroke-width attribute not emptied when no stroke' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', + 'stroke-width attribute not emptied when no stroke') + class KeepStrokeWidthWhenInheritedByChild(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementById('g3').getAttribute('stroke-width'), '1px', - 'stroke-width attribute removed despite it being inherited by a child' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementById('g3').getAttribute('stroke-width'), '1px', + 'stroke-width attribute removed despite it being inherited by a child') + class RemoveStrokeOpacityWhenStrokeNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', - 'stroke-opacity attribute not emptied when no stroke' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', + 'stroke-opacity attribute not emptied when no stroke') + class RemoveStrokeLinecapWhenStrokeNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', - 'stroke-linecap attribute not emptied when no stroke' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', + 'stroke-linecap attribute not emptied when no stroke') + class RemoveStrokeLinejoinWhenStrokeNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', - 'stroke-linejoin attribute not emptied when no stroke' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', + 'stroke-linejoin attribute not emptied when no stroke') + class RemoveStrokeDasharrayWhenStrokeNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', - 'stroke-dasharray attribute not emptied when no stroke' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', + 'stroke-dasharray attribute not emptied when no stroke') + class RemoveStrokeDashoffsetWhenStrokeNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', - 'stroke-dashoffset attribute not emptied when no stroke' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/stroke-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', + 'stroke-dashoffset attribute not emptied when no stroke') + class RemoveFillRuleWhenFillNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-rule'), '', - 'fill-rule attribute not emptied when no fill' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/fill-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-rule'), '', + 'fill-rule attribute not emptied when no fill') + class RemoveFillOpacityWhenFillNone(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-opacity'), '', - 'fill-opacity attribute not emptied when no fill' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/fill-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-opacity'), '', + 'fill-opacity attribute not emptied when no fill') + class ConvertFillPropertyToAttr(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg', - scour.parse_args(['--disable-simplify-colors'])) - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill'), 'black', - 'fill property not converted to XML attribute' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/fill-none.svg', + scour.parse_args(['--disable-simplify-colors'])) + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill'), 'black', + 'fill property not converted to XML attribute') + class ConvertFillOpacityPropertyToAttr(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-opacity'), '.5', - 'fill-opacity property not converted to XML attribute' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/fill-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-opacity'), '.5', + 'fill-opacity property not converted to XML attribute') + class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'evenodd', - 'fill-rule property not converted to XML attribute' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/fill-none.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'evenodd', + 'fill-rule property not converted to XML attribute') + class CollapseSinglyReferencedGradients(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/collapse-gradients.svg') - self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Singly-referenced linear gradient not collapsed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/collapse-gradients.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, + 'Singly-referenced linear gradient not collapsed') + class InheritGradientUnitsUponCollapsing(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/collapse-gradients.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), - 'userSpaceOnUse', - 'gradientUnits not properly inherited when collapsing gradients' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/collapse-gradients.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), + 'userSpaceOnUse', + 'gradientUnits not properly inherited when collapsing gradients') + class OverrideGradientUnitsUponCollapsing(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/collapse-gradients-gradientUnits.svg') - self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), '', - 'gradientUnits not properly overrode when collapsing gradients' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/collapse-gradients-gradientUnits.svg') + self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), '', + 'gradientUnits not properly overrode when collapsing gradients') + class DoNotCollapseMultiplyReferencedGradients(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/dont-collapse-gradients.svg') - self.assertNotEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, - 'Multiply-referenced linear gradient collapsed' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/dont-collapse-gradients.svg') + self.assertNotEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, + 'Multiply-referenced linear gradient collapsed') + class RemoveTrailingZerosFromPath(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path[:4] == 'm300' and path[4] != '.', True, - 'Trailing zeros not removed from path data' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(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.assertEqual(path, 'm5.81 0h0.1', - 'Trailing zeros not removed from path data after calculation' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-truncate-zeros-calc.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path, 'm5.81 0h0.1', + 'Trailing zeros not removed from path data after calculation') + class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg') - path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') - self.assertEqual(path[4], '-', - 'Delimiters not removed before negative coordinates in path data' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path[4], '-', + 'Delimiters not removed before negative coordinates in path data') + 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.assertEqual(path, 'm1e4 0', - 'Not using scientific notation for path coord when representation is shorter') + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-use-scientific-notation.svg') + path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') + self.assertEqual(path, 'm1e4 0', + 'Not using scientific notation for path coord when representation is shorter') + class ConvertAbsoluteToRelativePathCommands(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/path-abs-to-rel.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(path[1][0], 'v', - 'Absolute V command not converted to relative v command') - self.assertEqual(float(path[1][1][0]), -20.0, - 'Absolute V value not converted to relative v value') + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-abs-to-rel.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(path[1][0], 'v', + 'Absolute V command not converted to relative v command') + self.assertEqual(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.assertEqual(float(path[0][1][0]), 100.0, - 'Not rounding down' ) - self.assertEqual(float(path[0][1][1]), 100.0, - 'Not rounding up' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-precision.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(float(path[0][1][0]), 100.0, + 'Not rounding down') + self.assertEqual(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.assertEqual(float(path[1][1][0]), 100.01, - 'Not correctly limiting precision on path data' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-precision.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(float(path[1][1][0]), 100.01, + '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.assertEqual(path[4][0], 'z', - 'Did not remove an empty line segment from path' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(path[4][0], 'z', + 'Did not remove an empty line segment from path') # Do not remove empty segments if round linecaps. + + class DoNotRemoveEmptySegmentsFromPathWithRoundLineCaps(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/path-with-caps.svg') - path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) - self.assertEqual(len(path), 2, - 'Did not preserve empty segments when path had round linecaps' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-with-caps.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(len(path), 2, + 'Did not preserve empty segments when path had round linecaps') + 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.assertEqual(path[1][0], 'h', - 'Did not change line to horizontal line segment in path' ) - self.assertEqual(float(path[1][1][0]), 200.0, - 'Did not calculate horizontal line segment in path correctly' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(path[1][0], 'h', + 'Did not change line to horizontal line segment in path') + self.assertEqual(float(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.assertEqual(path[2][0], 'v', - 'Did not change line to vertical line segment in path' ) - self.assertEqual(float(path[2][1][0]), 100.0, - 'Did not calculate vertical line segment in path correctly' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) + self.assertEqual(path[2][0], 'v', + 'Did not change line to vertical line segment in path') + self.assertEqual(float(path[2][1][0]), 100.0, + 'Did not calculate vertical line segment in path correctly') + class ChangeBezierToShorthandInPath(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/path-bez-optimize.svg') - self.assertEqual(doc.getElementById('path1').getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', - 'Did not change bezier curves into shorthand curve segments in path') - self.assertEqual(doc.getElementById('path2a').getAttribute('d'), 'm200 200s200 100 200 0', - 'Did not change bezier curve into shorthand curve segment when first control point is the current point and previous command was not a bezier curve') - self.assertEqual(doc.getElementById('path2b').getAttribute('d'), 'm0 300s200-100 200 0c0 0 200 100 200 0', - 'Did change bezier curve into shorthand curve segment when first control point is the current point but previous command was a bezier curve with a different control point') + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-bez-optimize.svg') + self.assertEqual(doc.getElementById('path1').getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', + 'Did not change bezier curves into shorthand curve segments in path') + self.assertEqual(doc.getElementById('path2a').getAttribute('d'), 'm200 200s200 100 200 0', + 'Did not change bezier curve into shorthand curve segment when first control point is the current point and previous command was not a bezier curve') + self.assertEqual(doc.getElementById('path2b').getAttribute('d'), 'm0 300s200-100 200 0c0 0 200 100 200 0', + 'Did change bezier curve into shorthand curve segment when first control point is the current point but previous command was a bezier curve with a different control point') + class ChangeQuadToShorthandInPath(unittest.TestCase): - def runTest(self): - path = scour.scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', - 'Did not change quadratic curves into shorthand curve segments in path') + + def runTest(self): + path = scour.scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', + 'Did not change quadratic curves into shorthand curve segments in path') + class DoNotOptimzePathIfLarger(unittest.TestCase): - def runTest(self): - p = scour.scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0]; - self.assertTrue(len(p.getAttribute('d')) <= len("M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"), - 'Made path data longer during optimization') - # this was the scoured path data as of 2016-08-31 without the length check in cleanPath(): - # d="m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234-0.00345-0.00345z" + + def runTest(self): + p = scour.scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertTrue(len(p.getAttribute('d')) <= len("M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"), + 'Made path data longer during optimization') + # this was the scoured path data as of 2016-08-31 without the length check in cleanPath(): + # d="m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234-0.00345-0.00345z" + class HandleEncodingUTF8(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/encoding-utf8.svg') - text = u'Hello in many languages:\nar: أهلا\nbn: হ্যালো\nel: Χαίρετε\nen: Hello\nhi: नमस्ते\niw: שלום\nja: こんにちは\nkm: ជំរាបសួរ\nml: ഹലോ\nru: Здравствуйте\nur: ہیلو\nzh: 您好' - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() - self.assertEqual( desc, text, 'Did not handle international UTF8 characters' ) - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[1].firstChild.wholeText).strip() - self.assertEqual( desc, u'“”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿�', 'Did not handle common UTF8 characters' ) - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[2].firstChild.wholeText).strip() - self.assertEqual( desc, u':-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔', 'Did not handle mathematical UTF8 characters' ) - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[3].firstChild.wholeText).strip() - self.assertEqual( desc, u'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎', 'Did not handle superscript/subscript UTF8 characters' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/encoding-utf8.svg') + text = u'Hello in many languages:\nar: أهلا\nbn: হ্যালো\nel: Χαίρετε\nen: Hello\nhi: नमस्ते\niw: שלום\nja: こんにちは\nkm: ជំរាបសួរ\nml: ഹലോ\nru: Здравствуйте\nur: ہیلو\nzh: 您好' + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() + self.assertEqual(desc, text, 'Did not handle international UTF8 characters') + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[1].firstChild.wholeText).strip() + self.assertEqual(desc, u'“”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿�', 'Did not handle common UTF8 characters') + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[2].firstChild.wholeText).strip() + self.assertEqual(desc, u':-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔', 'Did not handle mathematical UTF8 characters') + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[3].firstChild.wholeText).strip() + self.assertEqual(desc, u'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎', 'Did not handle superscript/subscript UTF8 characters') + class HandleEncodingISO_8859_15(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/encoding-iso-8859-15.svg') - desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() - self.assertEqual( desc, u'áèîäöüß€ŠšŽžŒœŸ', 'Did not handle ISO 8859-15 encoded characters' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/encoding-iso-8859-15.svg') + desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() + self.assertEqual(desc, u'áèîäöüß€ŠšŽžŒœŸ', 'Did not handle ISO 8859-15 encoded characters') + class HandleSciNoInPathData(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/path-sn.svg') - self.assertEqual( len(doc.getElementsByTagNameNS(SVGNS, 'path')), 1, - 'Did not handle scientific notation in path data' ) + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-sn.svg') + self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'path')), 1, + 'Did not handle scientific notation in path data') + class TranslateRGBIntoHex(unittest.TestCase): - def runTest(self): - elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual( elem.getAttribute('fill'), '#0f1011', - 'Not converting rgb into hex') + + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEqual(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.assertEqual( elem.getAttribute('stop-color'), '#7f0000', - 'Not converting rgb pct into hex') + + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0] + self.assertEqual(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.assertEqual( elem.getAttribute('stroke'), '#a9a9a9', - 'Not converting standard color names into hex') + + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEqual(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.assertEqual( elem.getAttribute('solid-color'), '#fafad2', - 'Not converting extended color names into hex') + + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0] + self.assertEqual(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.assertEqual( elem.getAttribute('fill'), '#fff', - 'Not converting long hex color into short hex') + + def runTest(self): + elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0] + self.assertEqual(elem.getAttribute('fill'), '#fff', + 'Not converting long hex color into short hex') + class DoNotConvertShortColorNames(unittest.TestCase): - def runTest(self): - elem = scour.scourXmlFile('unittests/dont-convert-short-color-names.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual( 'red', elem.getAttribute('fill'), - 'Converted short color name to longer hex string') + + def runTest(self): + elem = scour.scourXmlFile('unittests/dont-convert-short-color-names.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEqual('red', elem.getAttribute('fill'), + 'Converted short color name to longer hex string') + class AllowQuotEntitiesInUrl(unittest.TestCase): - def runTest(self): - grads = scour.scourXmlFile('unittests/quot-in-url.svg').getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual( len(grads), 1, - 'Removed referenced gradient when " was in the url') + + def runTest(self): + grads = scour.scourXmlFile('unittests/quot-in-url.svg').getElementsByTagNameNS(SVGNS, 'linearGradient') + self.assertEqual(len(grads), 1, + 'Removed referenced gradient when " was in the url') + class RemoveFontStylesFromNonTextShapes(unittest.TestCase): - def runTest(self): - r = scour.scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] - self.assertEqual( r.getAttribute('font-size'), '', - 'font-size not removed from rect' ) + + def runTest(self): + r = scour.scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + self.assertEqual(r.getAttribute('font-size'), '', + 'font-size not removed from rect') + class CollapseConsecutiveHLinesSegments(unittest.TestCase): - def runTest(self): - p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual( p.getAttribute('d'), 'm100 100h200v100h-200z', - 'Did not collapse consecutive hlines segments') + + def runTest(self): + p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(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.assertEqual( p.getAttribute('d'), 'm100 300h200v100h-200z', - 'Did not collapse consecutive hlines coordinates') + + def runTest(self): + p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[1] + self.assertEqual(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.assertEqual( p.getAttribute('d'), 'm100 500h300-100v100h-200z', - 'Collapsed consecutive hlines segments with differing signs') + + def runTest(self): + p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[2] + self.assertEqual(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.assertEqual(p.getAttribute('d'), 'm10 10l40 40 40-40z', - 'Did not convert straight curves into lines') + + def runTest(self): + p = scour.scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(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.assertEqual(p.getAttribute('points'), '50 50 150 50 150 150 50 150', - 'Unnecessary polygon end point not removed' ) + + def runTest(self): + p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + self.assertEqual(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.assertEqual(p.getAttribute('points'), '200 50 300 50 300 150 200 150', - 'Last point of polygon removed' ) + + def runTest(self): + p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1] + self.assertEqual(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.assertEqual(p.getAttribute('points'), '1e4 50', - 'Polygon coordinates not scoured') + + def runTest(self): + p = scour.scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + self.assertEqual(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.assertEqual(p.getAttribute('points'), '1e4 50', - 'Polyline coordinates not scoured') + + def runTest(self): + p = scour.scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + self.assertEqual(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.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polygon coordinates not properly parsed') + + 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.assertEqual(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.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polyline coordinates not properly parsed') + + def runTest(self): + p = scour.scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', + 'Negative polyline coordinates not properly parsed') + class ScourPolygonNegativeCoordFirst(unittest.TestCase): - def runTest(self): - p = scour.scourXmlFile('unittests/polygon-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] - # points="-100,-100,100-100,100-100-100,-100-100,200" /> - self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polygon coordinates not properly parsed') + + def runTest(self): + p = scour.scourXmlFile('unittests/polygon-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + # points="-100,-100,100-100,100-100-100,-100-100,200" /> + self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', + 'Negative polygon coordinates not properly parsed') + class ScourPolylineNegativeCoordFirst(unittest.TestCase): - def runTest(self): - p = scour.scourXmlFile('unittests/polyline-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] - self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', - 'Negative polyline coordinates not properly parsed') + + def runTest(self): + p = scour.scourXmlFile('unittests/polyline-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', + 'Negative polyline coordinates not properly parsed') + class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase): - def runTest(self): - f = scour.scourXmlFile('unittests/important-groups-in-defs.svg') - self.assertEqual(len(f.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, - 'Group in defs with id\'ed element removed') + + def runTest(self): + f = scour.scourXmlFile('unittests/important-groups-in-defs.svg') + self.assertEqual(len(f.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, + 'Group in defs with id\'ed element removed') + class AlwaysKeepClosePathSegments(unittest.TestCase): - def runTest(self): - p = scour.scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual(p.getAttribute('d'), 'm10 10h100v100h-100z', - 'Path with closepath not preserved') + + def runTest(self): + p = scour.scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(p.getAttribute('d'), 'm10 10h100v100h-100z', + 'Path with closepath not preserved') + class RemoveDuplicateLinearGradients(unittest.TestCase): - def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') - lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') - self.assertEqual(1, lingrads.length, - 'Duplicate linear gradient not removed') + + def runTest(self): + svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') + self.assertEqual(1, lingrads.length, + 'Duplicate linear gradient not removed') + class RereferenceForLinearGradient(unittest.TestCase): - def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') - rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') - self.assertEqual(rects[0].getAttribute('fill'), rects[1].getAttribute('stroke'), - 'Reference not updated after removing duplicate linear gradient') - self.assertEqual(rects[0].getAttribute('fill'), rects[4].getAttribute('fill'), - 'Reference not updated after removing duplicate linear gradient') + + def runTest(self): + svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') + self.assertEqual(rects[0].getAttribute('fill'), rects[1].getAttribute('stroke'), + 'Reference not updated after removing duplicate linear gradient') + self.assertEqual(rects[0].getAttribute('fill'), rects[4].getAttribute('fill'), + 'Reference not updated after removing duplicate linear gradient') + class RemoveDuplicateRadialGradients(unittest.TestCase): - def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') - radgrads = svgdoc.getElementsByTagNameNS(SVGNS, 'radialGradient') - self.assertEqual(1, radgrads.length, - 'Duplicate radial gradient not removed') + + def runTest(self): + svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + radgrads = svgdoc.getElementsByTagNameNS(SVGNS, 'radialGradient') + self.assertEqual(1, radgrads.length, + 'Duplicate radial gradient not removed') + class RereferenceForRadialGradient(unittest.TestCase): - def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') - rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') - self.assertEqual(rects[2].getAttribute('stroke'), rects[3].getAttribute('fill'), - 'Reference not updated after removing duplicate radial gradient') + + def runTest(self): + svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') + self.assertEqual(rects[2].getAttribute('stroke'), rects[3].getAttribute('fill'), + 'Reference not updated after removing duplicate radial gradient') + class RereferenceForGradientWithFallback(unittest.TestCase): - def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') - rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') - self.assertEqual(rects[0].getAttribute('fill') + ' #fff', rects[5].getAttribute('fill'), - 'Reference (with fallback) not updated after removing duplicate linear gradient') + + def runTest(self): + svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') + self.assertEqual(rects[0].getAttribute('fill') + ' #fff', rects[5].getAttribute('fill'), + 'Reference (with fallback) not updated after removing duplicate linear gradient') class CollapseSamePathPoints(unittest.TestCase): - def runTest(self): - p = scour.scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0]; - self.assertEqual(p.getAttribute('d'), "m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234 0 0z", - 'Did not collapse same path points') + + def runTest(self): + p = scour.scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(p.getAttribute('d'), "m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234 0 0z", + 'Did not collapse same path points') + class ScourUnitlessLengths(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/scour-lengths.svg') - r = doc.getElementsByTagNameNS(SVGNS, 'rect')[0]; - svg = doc.documentElement - self.assertEqual(svg.getAttribute('x'), '1', - 'Did not scour x attribute of svg element with unitless number') - self.assertEqual(r.getAttribute('x'), '123.46', - 'Did not scour x attribute of rect with unitless number') - self.assertEqual(r.getAttribute('y'), '123', - 'Did not scour y attribute of rect unitless number') - self.assertEqual(r.getAttribute('width'), '300', - 'Did not scour width attribute of rect with unitless number') - self.assertEqual(r.getAttribute('height'), '100', - 'Did not scour height attribute of rect with unitless number') + + def runTest(self): + doc = scour.scourXmlFile('unittests/scour-lengths.svg') + r = doc.getElementsByTagNameNS(SVGNS, 'rect')[0] + svg = doc.documentElement + self.assertEqual(svg.getAttribute('x'), '1', + 'Did not scour x attribute of svg element with unitless number') + self.assertEqual(r.getAttribute('x'), '123.46', + 'Did not scour x attribute of rect with unitless number') + self.assertEqual(r.getAttribute('y'), '123', + 'Did not scour y attribute of rect unitless number') + self.assertEqual(r.getAttribute('width'), '300', + 'Did not scour width attribute of rect with unitless number') + self.assertEqual(r.getAttribute('height'), '100', + 'Did not scour height attribute of rect with unitless number') + class ScourLengthsWithUnits(unittest.TestCase): - def runTest(self): - r = scour.scourXmlFile('unittests/scour-lengths.svg').getElementsByTagNameNS(SVGNS, 'rect')[1]; - self.assertEqual(r.getAttribute('x'), '123.46px', - 'Did not scour x attribute with unit') - self.assertEqual(r.getAttribute('y'), '35ex', - 'Did not scour y attribute with unit') - self.assertEqual(r.getAttribute('width'), '300pt', - 'Did not scour width attribute with unit') - self.assertEqual(r.getAttribute('height'), '50%', - 'Did not scour height attribute with unit') + + def runTest(self): + r = scour.scourXmlFile('unittests/scour-lengths.svg').getElementsByTagNameNS(SVGNS, 'rect')[1] + self.assertEqual(r.getAttribute('x'), '123.46px', + 'Did not scour x attribute with unit') + self.assertEqual(r.getAttribute('y'), '35ex', + 'Did not scour y attribute with unit') + self.assertEqual(r.getAttribute('width'), '300pt', + 'Did not scour width attribute with unit') + self.assertEqual(r.getAttribute('height'), '50%', + 'Did not scour height attribute with unit') + class RemoveRedundantSvgNamespaceDeclaration(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement - self.assertNotEqual( doc.getAttribute('xmlns:svg'), 'http://www.w3.org/2000/svg', - 'Redundant svg namespace declaration not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement + self.assertNotEqual(doc.getAttribute('xmlns:svg'), 'http://www.w3.org/2000/svg', + 'Redundant svg namespace declaration not removed') + class RemoveRedundantSvgNamespacePrefix(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement - r = doc.getElementsByTagNameNS(SVGNS, 'rect')[1] - self.assertEqual( r.tagName, 'rect', - 'Redundant svg: prefix not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement + r = doc.getElementsByTagNameNS(SVGNS, 'rect')[1] + self.assertEqual(r.tagName, 'rect', + 'Redundant svg: prefix not removed') class RemoveDefaultGradX1Value(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual( g.getAttribute('x1'), '', - 'x1="0" not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('x1'), '', + 'x1="0" not removed') + class RemoveDefaultGradY1Value(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual( g.getAttribute('y1'), '', - 'y1="0" not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('y1'), '', + 'y1="0" not removed') + class RemoveDefaultGradX2Value(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/gradient-default-attrs.svg') - self.assertEqual( doc.getElementById('grad1').getAttribute('x2'), '', - 'x2="100%" not removed') - self.assertEqual( doc.getElementById('grad1b').getAttribute('x2'), '', - 'x2="1" not removed, which is equal to the default x2="100%" when gradientUnits="objectBoundingBox"') - self.assertNotEqual( doc.getElementById('grad1c').getAttribute('x2'), '', - 'x2="1" removed, which is NOT equal to the default x2="100%" when gradientUnits="userSpaceOnUse"') + + def runTest(self): + doc = scour.scourXmlFile('unittests/gradient-default-attrs.svg') + self.assertEqual(doc.getElementById('grad1').getAttribute('x2'), '', + 'x2="100%" not removed') + self.assertEqual(doc.getElementById('grad1b').getAttribute('x2'), '', + 'x2="1" not removed, which is equal to the default x2="100%" when gradientUnits="objectBoundingBox"') + self.assertNotEqual(doc.getElementById('grad1c').getAttribute('x2'), '', + 'x2="1" removed, which is NOT equal to the default x2="100%" when gradientUnits="userSpaceOnUse"') + class RemoveDefaultGradY2Value(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual( g.getAttribute('y2'), '', - 'y2="0" not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('y2'), '', + 'y2="0" not removed') + class RemoveDefaultGradGradientUnitsValue(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual( g.getAttribute('gradientUnits'), '', - 'gradientUnits="objectBoundingBox" not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('gradientUnits'), '', + 'gradientUnits="objectBoundingBox" not removed') + class RemoveDefaultGradSpreadMethodValue(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') - self.assertEqual( g.getAttribute('spreadMethod'), '', - 'spreadMethod="pad" not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + self.assertEqual(g.getAttribute('spreadMethod'), '', + 'spreadMethod="pad" not removed') + class RemoveDefaultGradCXValue(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual( g.getAttribute('cx'), '', - 'cx="50%" not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('cx'), '', + 'cx="50%" not removed') + class RemoveDefaultGradCYValue(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual( g.getAttribute('cy'), '', - 'cy="50%" not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('cy'), '', + 'cy="50%" not removed') + class RemoveDefaultGradRValue(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual( g.getAttribute('r'), '', - 'r="50%" not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('r'), '', + 'r="50%" not removed') + class RemoveDefaultGradFXValue(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual( g.getAttribute('fx'), '', - 'fx matching cx not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('fx'), '', + 'fx matching cx not removed') + class RemoveDefaultGradFYValue(unittest.TestCase): - def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') - self.assertEqual( g.getAttribute('fy'), '', - 'fy matching cy not removed') + + def runTest(self): + g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + self.assertEqual(g.getAttribute('fy'), '', + 'fy matching cy not removed') + class CDATAInXml(unittest.TestCase): - def runTest(self): - with open('unittests/cdata.svg') as f: - lines = scour.scourString(f.read()).splitlines() - self.assertEqual( lines[3], - " alert('pb&j');", - 'CDATA did not come out correctly') + + def runTest(self): + with open('unittests/cdata.svg') as f: + lines = scour.scourString(f.read()).splitlines() + self.assertEqual(lines[3], + " alert('pb&j');", + 'CDATA did not come out correctly') + class WellFormedXMLLesserThanInAttrValue(unittest.TestCase): - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) - self.assertTrue( wellformed.find('unicode="<"') != -1, - "Improperly serialized < in attribute value") + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scour.scourString(f.read()) + self.assertTrue(wellformed.find('unicode="<"') != -1, + "Improperly serialized < in attribute value") + class WellFormedXMLAmpersandInAttrValue(unittest.TestCase): - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) - self.assertTrue( wellformed.find('unicode="&"') != -1, - 'Improperly serialized & in attribute value' ) + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scour.scourString(f.read()) + self.assertTrue(wellformed.find('unicode="&"') != -1, + 'Improperly serialized & in attribute value') + class WellFormedXMLLesserThanInTextContent(unittest.TestCase): - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) - self.assertTrue( wellformed.find('<title>2 < 5') != -1, - 'Improperly serialized < in text content') + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scour.scourString(f.read()) + self.assertTrue(wellformed.find('2 < 5') != -1, + 'Improperly serialized < in text content') + class WellFormedXMLAmpersandInTextContent(unittest.TestCase): - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) - self.assertTrue( wellformed.find('Peanut Butter & Jelly') != -1, - 'Improperly serialized & in text content') + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scour.scourString(f.read()) + self.assertTrue(wellformed.find('Peanut Butter & Jelly') != -1, + 'Improperly serialized & in text content') + class WellFormedXMLNamespacePrefixRemoveUnused(unittest.TestCase): - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) - self.assertTrue( wellformed.find('xmlns:foo=') == -1, - 'Improperly serialized namespace prefix declarations: Unused namespace decaration not removed') + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scour.scourString(f.read()) + self.assertTrue(wellformed.find('xmlns:foo=') == -1, + 'Improperly serialized namespace prefix declarations: Unused namespace decaration not removed') + class WellFormedXMLNamespacePrefixKeepUsedElementPrefix(unittest.TestCase): - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) - self.assertTrue( wellformed.find('xmlns:bar=') != -1, - 'Improperly serialized namespace prefix declarations: Used element prefix removed') + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scour.scourString(f.read()) + self.assertTrue(wellformed.find('xmlns:bar=') != -1, + 'Improperly serialized namespace prefix declarations: Used element prefix removed') + class WellFormedXMLNamespacePrefixKeepUsedAttributePrefix(unittest.TestCase): - def runTest(self): - with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) - self.assertTrue( wellformed.find('xmlns:baz=') != -1, - 'Improperly serialized namespace prefix declarations: Used attribute prefix removed') + + def runTest(self): + with open('unittests/xml-well-formed.svg') as f: + wellformed = scour.scourString(f.read()) + self.assertTrue(wellformed.find('xmlns:baz=') != -1, + 'Improperly serialized namespace prefix declarations: Used attribute prefix removed') + class NamespaceDeclPrefixesInXMLWhenNotInDefaultNamespace(unittest.TestCase): - def runTest(self): - with open('unittests/xml-ns-decl.svg') as f: - xmlstring = scour.scourString(f.read()) - self.assertTrue( xmlstring.find('xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"') != -1, - 'Improperly serialized namespace prefix declarations when not in default namespace') + + def runTest(self): + with open('unittests/xml-ns-decl.svg') as f: + xmlstring = scour.scourString(f.read()) + self.assertTrue(xmlstring.find('xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"') != -1, + 'Improperly serialized namespace prefix declarations when not in default namespace') + class MoveSVGElementsToDefaultNamespace(unittest.TestCase): - def runTest(self): - with open('unittests/xml-ns-decl.svg') as f: - xmlstring = scour.scourString(f.read()) - self.assertTrue( xmlstring.find(' + + def runTest(self): + with open('unittests/whitespace-important.svg') as f: + s = scour.scourString(f.read()).splitlines() + c = ''' This is some messed-up markup '''.splitlines() - for i in range(4): - self.assertEqual( s[i], c[i], - 'Whitespace not preserved for line ' + str(i)) + for i in range(4): + self.assertEqual(s[i], c[i], + 'Whitespace not preserved for line ' + str(i)) + class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): - def runTest(self): - with open('unittests/whitespace-nested.svg') as f: - s = scour.scourString(f.read()).splitlines() - c = ''' + + def runTest(self): + with open('unittests/whitespace-nested.svg') as f: + s = scour.scourString(f.read()).splitlines() + c = ''' Use bold text '''.splitlines() - for i in range(4): - self.assertEqual( s[i], c[i], - 'Whitespace not preserved when nested for line ' + str(i)) + for i in range(4): + self.assertEqual(s[i], c[i], + 'Whitespace not preserved when nested for line ' + str(i)) + class GetAttrPrefixRight(unittest.TestCase): - def runTest(self): - grad = scour.scourXmlFile('unittests/xml-namespace-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[1] - self.assertEqual( grad.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#linearGradient841', - 'Did not get xlink:href prefix right') + + def runTest(self): + grad = scour.scourXmlFile('unittests/xml-namespace-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[1] + self.assertEqual(grad.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#linearGradient841', + 'Did not get xlink:href prefix right') + class EnsurePreserveWhitespaceOnNonTextElements(unittest.TestCase): - def runTest(self): - with open('unittests/no-collapse-lines.svg') as f: - s = scour.scourString(f.read()) - self.assertEqual( len(s.splitlines()), 6, - 'Did not properly preserve whitespace on elements even if they were not textual') + + def runTest(self): + with open('unittests/no-collapse-lines.svg') as f: + s = scour.scourString(f.read()) + self.assertEqual(len(s.splitlines()), 6, + 'Did not properly preserve whitespace on elements even if they were not textual') + class HandleEmptyStyleElement(unittest.TestCase): - def runTest(self): - try: - styles = scour.scourXmlFile('unittests/empty-style.svg').getElementsByTagNameNS(SVGNS, 'style') - fail = len(styles) != 1 - except AttributeError: - fail = True - self.assertEqual( fail, False, - 'Could not handle an empty style element') + + def runTest(self): + try: + styles = scour.scourXmlFile('unittests/empty-style.svg').getElementsByTagNameNS(SVGNS, 'style') + fail = len(styles) != 1 + except AttributeError: + fail = True + self.assertEqual(fail, False, + 'Could not handle an empty style element') + class EnsureLineEndings(unittest.TestCase): - def runTest(self): - with open('unittests/whitespace-important.svg') as f: - s = scour.scourString(f.read()) - self.assertEqual( len(s.splitlines()), 4, - 'Did not output line ending character correctly') + + def runTest(self): + with open('unittests/whitespace-important.svg') as f: + s = scour.scourString(f.read()) + self.assertEqual(len(s.splitlines()), 4, + 'Did not output line ending character correctly') + class XmlEntities(unittest.TestCase): - def runTest(self): - self.assertEqual( scour.makeWellFormed('<>&'), '<>&', - 'Incorrectly translated XML entities') + + def runTest(self): + self.assertEqual(scour.makeWellFormed('<>&'), '<>&', + 'Incorrectly translated XML entities') + class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/comments.svg') - self.assertEqual( doc.childNodes.length, 4, - 'Did not include all comment children outside of root') - self.assertEqual( doc.childNodes[0].nodeType, 8, 'First node not a comment') - self.assertEqual( doc.childNodes[1].nodeType, 8, 'Second node not a comment') - self.assertEqual( doc.childNodes[3].nodeType, 8, 'Fourth node not a comment') + + def runTest(self): + doc = scour.scourXmlFile('unittests/comments.svg') + self.assertEqual(doc.childNodes.length, 4, + 'Did not include all comment children outside of root') + self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') + self.assertEqual(doc.childNodes[1].nodeType, 8, 'Second node not a comment') + self.assertEqual(doc.childNodes[3].nodeType, 8, 'Fourth node not a comment') + class DoNotStripDoctype(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/doctype.svg') - self.assertEqual( doc.childNodes.length, 3, - 'Did not include the DOCROOT') - self.assertEqual( doc.childNodes[0].nodeType, 8, 'First node not a comment') - self.assertEqual( doc.childNodes[1].nodeType, 10, 'Second node not a doctype') - self.assertEqual( doc.childNodes[2].nodeType, 1, 'Third node not the root node') + + def runTest(self): + doc = scour.scourXmlFile('unittests/doctype.svg') + self.assertEqual(doc.childNodes.length, 3, + 'Did not include the DOCROOT') + self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') + self.assertEqual(doc.childNodes[1].nodeType, 10, 'Second node not a doctype') + self.assertEqual(doc.childNodes[2].nodeType, 1, 'Third node not the root node') + class PathImplicitLineWithMoveCommands(unittest.TestCase): - def runTest(self): - path = scour.scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertEqual( path.getAttribute('d'), "m100 100v100m200-100h-200m200 100v-100", - "Implicit line segments after move not preserved") + + def runTest(self): + path = scour.scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + self.assertEqual(path.getAttribute('d'), "m100 100v100m200-100h-200m200 100v-100", + "Implicit line segments after move not preserved") + class RemoveTitlesOption(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', - scour.parse_args(['--remove-titles'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove tag with --remove-titles') + + def runTest(self): + doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', + scour.parse_args(['--remove-titles'])) + self.assertEqual(doc.childNodes.length, 1, + 'Did not remove <title> tag with --remove-titles') + class RemoveDescriptionsOption(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', - scour.parse_args(['--remove-descriptions'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <desc> tag with --remove-descriptions') + + def runTest(self): + doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', + scour.parse_args(['--remove-descriptions'])) + self.assertEqual(doc.childNodes.length, 1, + 'Did not remove <desc> tag with --remove-descriptions') + class RemoveMetadataOption(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', - scour.parse_args(['--remove-metadata'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <metadata> tag with --remove-metadata') + + def runTest(self): + doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', + scour.parse_args(['--remove-metadata'])) + self.assertEqual(doc.childNodes.length, 1, + 'Did not remove <metadata> tag with --remove-metadata') + class RemoveDescriptiveElementsOption(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', - scour.parse_args(['--remove-descriptive-elements'])) - self.assertEqual(doc.childNodes.length, 1, - 'Did not remove <title>, <desc> and <metadata> tags with --remove-descriptive-elements') + + def runTest(self): + doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', + scour.parse_args(['--remove-descriptive-elements'])) + self.assertEqual(doc.childNodes.length, 1, + 'Did not remove <title>, <desc> and <metadata> tags with --remove-descriptive-elements') + class EnableCommentStrippingOption(unittest.TestCase): - def runTest(self): - with open('unittests/comment-beside-xml-decl.svg') as f: - docStr = f.read() - docStr = scour.scourString(docStr, - scour.parse_args(['--enable-comment-stripping'])) - self.assertEqual(docStr.find('<!--'), -1, - 'Did not remove document-level comment with --enable-comment-stripping') + + def runTest(self): + with open('unittests/comment-beside-xml-decl.svg') as f: + docStr = f.read() + docStr = scour.scourString(docStr, + scour.parse_args(['--enable-comment-stripping'])) + self.assertEqual(docStr.find('<!--'), -1, + 'Did not remove document-level comment with --enable-comment-stripping') + class StripXmlPrologOption(unittest.TestCase): - def runTest(self): - with open('unittests/comment-beside-xml-decl.svg') as f: - docStr = f.read() - docStr = scour.scourString(docStr, - scour.parse_args(['--strip-xml-prolog'])) - self.assertEqual(docStr.find('<?xml'), -1, - 'Did not remove <?xml?> with --strip-xml-prolog') + + def runTest(self): + with open('unittests/comment-beside-xml-decl.svg') as f: + docStr = f.read() + docStr = scour.scourString(docStr, + scour.parse_args(['--strip-xml-prolog'])) + self.assertEqual(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'])) - gradientTag = doc.getElementsByTagName('linearGradient')[0] - self.assertEqual(gradientTag.getAttribute('id'), 'a', - "Did not shorten a linear gradient's ID with --shorten-ids") - rectTag = doc.getElementsByTagName('rect')[0] - self.assertEqual(rectTag.getAttribute('fill'), 'url(#a)', - 'Did not update reference to shortened ID') + + def runTest(self): + doc = scour.scourXmlFile('unittests/shorten-ids.svg', + scour.parse_args(['--shorten-ids'])) + gradientTag = doc.getElementsByTagName('linearGradient')[0] + self.assertEqual(gradientTag.getAttribute('id'), 'a', + "Did not shorten a linear gradient's ID with --shorten-ids") + rectTag = doc.getElementsByTagName('rect')[0] + self.assertEqual(rectTag.getAttribute('fill'), 'url(#a)', + 'Did not update reference to shortened ID') + class MustKeepGInSwitch(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/groups-in-switch.svg') - self.assertEqual(doc.getElementsByTagName('g').length, 1, - 'Erroneously removed a <g> in a <switch>') + + def runTest(self): + doc = scour.scourXmlFile('unittests/groups-in-switch.svg') + self.assertEqual(doc.getElementsByTagName('g').length, 1, + 'Erroneously removed a <g> in a <switch>') + class MustKeepGInSwitch2(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/groups-in-switch-with-id.svg', - scour.parse_args(['--enable-id-stripping'])) - self.assertEqual(doc.getElementsByTagName('g').length, 1, - 'Erroneously removed a <g> in a <switch>') + + def runTest(self): + doc = scour.scourXmlFile('unittests/groups-in-switch-with-id.svg', + scour.parse_args(['--enable-id-stripping'])) + self.assertEqual(doc.getElementsByTagName('g').length, 1, + 'Erroneously removed a <g> in a <switch>') + class GroupCreation(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/group-creation.svg', - scour.parse_args(['--create-groups'])) - self.assertEqual(doc.getElementsByTagName('g').length, 1, - 'Did not create a <g> for a run of elements having similar attributes') + + def runTest(self): + doc = scour.scourXmlFile('unittests/group-creation.svg', + scour.parse_args(['--create-groups'])) + self.assertEqual(doc.getElementsByTagName('g').length, 1, + 'Did not create a <g> for a run of elements having similar attributes') + class GroupCreationForInheritableAttributesOnly(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/group-creation.svg', - scour.parse_args(['--create-groups'])) - self.assertEqual(doc.getElementsByTagName('g').item(0).getAttribute('y'), '', - 'Promoted the uninheritable attribute y to a <g>') + + def runTest(self): + doc = scour.scourXmlFile('unittests/group-creation.svg', + scour.parse_args(['--create-groups'])) + self.assertEqual(doc.getElementsByTagName('g').item(0).getAttribute('y'), '', + 'Promoted the uninheritable attribute y to a <g>') + class GroupNoCreation(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/group-no-creation.svg', - scour.parse_args(['--create-groups'])) - self.assertEqual(doc.getElementsByTagName('g').length, 0, - 'Created a <g> for a run of elements having dissimilar attributes') + + def runTest(self): + doc = scour.scourXmlFile('unittests/group-no-creation.svg', + scour.parse_args(['--create-groups'])) + self.assertEqual(doc.getElementsByTagName('g').length, 0, + 'Created a <g> for a run of elements having dissimilar attributes') + class GroupNoCreationForTspan(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/group-no-creation-tspan.svg', - scour.parse_args(['--create-groups'])) - self.assertEqual(doc.getElementsByTagName('g').length, 0, - 'Created a <g> for a run of <tspan>s that are not allowed as children according to content model') + + def runTest(self): + doc = scour.scourXmlFile('unittests/group-no-creation-tspan.svg', + scour.parse_args(['--create-groups'])) + self.assertEqual(doc.getElementsByTagName('g').length, 0, + 'Created a <g> for a run of <tspan>s that are not allowed as children according to content model') + class DoNotCommonizeAttributesOnReferencedElements(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/commonized-referenced-elements.svg') - self.assertEqual(doc.getElementsByTagName('circle')[0].getAttribute('fill'), '#0f0', - 'Grouped an element referenced elsewhere into a <g>') + + def runTest(self): + doc = scour.scourXmlFile('unittests/commonized-referenced-elements.svg') + self.assertEqual(doc.getElementsByTagName('circle')[0].getAttribute('fill'), '#0f0', + 'Grouped an element referenced elsewhere into a <g>') + class DoNotRemoveOverflowVisibleOnMarker(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/overflow-marker.svg') - self.assertEqual(doc.getElementById('m1').getAttribute('overflow'), 'visible', - 'Removed the overflow attribute when it was not using the default value') - self.assertEqual(doc.getElementById('m2').getAttribute('overflow'), '', - 'Did not remove the overflow attribute when it was using the default value') + + def runTest(self): + doc = scour.scourXmlFile('unittests/overflow-marker.svg') + self.assertEqual(doc.getElementById('m1').getAttribute('overflow'), 'visible', + 'Removed the overflow attribute when it was not using the default value') + self.assertEqual(doc.getElementById('m2').getAttribute('overflow'), '', + 'Did not remove the overflow attribute when it was using the default value') + class DoNotRemoveOrientAutoOnMarker(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/orient-marker.svg') - self.assertEqual(doc.getElementById('m1').getAttribute('orient'), 'auto', - 'Removed the orient attribute when it was not using the default value') - self.assertEqual(doc.getElementById('m2').getAttribute('orient'), '', - 'Did not remove the orient attribute when it was using the default value') + + def runTest(self): + doc = scour.scourXmlFile('unittests/orient-marker.svg') + self.assertEqual(doc.getElementById('m1').getAttribute('orient'), 'auto', + 'Removed the orient attribute when it was not using the default value') + self.assertEqual(doc.getElementById('m2').getAttribute('orient'), '', + 'Did not remove the orient attribute when it was using the default value') + class MarkerOnSvgElements(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/overflow-svg.svg') - self.assertEqual(doc.getElementsByTagName('svg')[0].getAttribute('overflow'), '', - 'Did not remove the overflow attribute when it was using the default value') - self.assertEqual(doc.getElementsByTagName('svg')[1].getAttribute('overflow'), '', - 'Did not remove the overflow attribute when it was using the default value') - self.assertEqual(doc.getElementsByTagName('svg')[2].getAttribute('overflow'), 'visible', - 'Removed the overflow attribute when it was not using the default value') + + def runTest(self): + doc = scour.scourXmlFile('unittests/overflow-svg.svg') + self.assertEqual(doc.getElementsByTagName('svg')[0].getAttribute('overflow'), '', + 'Did not remove the overflow attribute when it was using the default value') + self.assertEqual(doc.getElementsByTagName('svg')[1].getAttribute('overflow'), '', + 'Did not remove the overflow attribute when it was using the default value') + self.assertEqual(doc.getElementsByTagName('svg')[2].getAttribute('overflow'), 'visible', + 'Removed the overflow attribute when it was not using the default value') + class GradientReferencedByStyleCDATA(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/style-cdata.svg') - self.assertEqual(len(doc.getElementsByTagName('linearGradient')), 1, - 'Removed a gradient referenced by an internal stylesheet') + + def runTest(self): + doc = scour.scourXmlFile('unittests/style-cdata.svg') + self.assertEqual(len(doc.getElementsByTagName('linearGradient')), 1, + 'Removed a gradient referenced by an internal stylesheet') + class ShortenIDsInStyleCDATA(unittest.TestCase): - def runTest(self): - with open('unittests/style-cdata.svg') as f: - docStr = f.read() - docStr = scour.scourString(docStr, - scour.parse_args(['--shorten-ids'])) - self.assertEqual(docStr.find('somethingreallylong'), -1, - 'Did not shorten IDs in the internal stylesheet') + + def runTest(self): + with open('unittests/style-cdata.svg') as f: + docStr = f.read() + docStr = scour.scourString(docStr, + scour.parse_args(['--shorten-ids'])) + self.assertEqual(docStr.find('somethingreallylong'), -1, + 'Did not shorten IDs in the internal stylesheet') + class StyleToAttr(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/style-to-attr.svg') - line = doc.getElementsByTagName('line')[0] - self.assertEqual(line.getAttribute('stroke'), '#000') - self.assertEqual(line.getAttribute('marker-start'), 'url(#m)') - self.assertEqual(line.getAttribute('marker-mid'), 'url(#m)') - self.assertEqual(line.getAttribute('marker-end'), 'url(#m)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/style-to-attr.svg') + line = doc.getElementsByTagName('line')[0] + self.assertEqual(line.getAttribute('stroke'), '#000') + self.assertEqual(line.getAttribute('marker-start'), 'url(#m)') + self.assertEqual(line.getAttribute('marker-mid'), 'url(#m)') + self.assertEqual(line.getAttribute('marker-end'), 'url(#m)') + class PathEmptyMove(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/path-empty-move.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100l200 100z') - self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200l100 100z') + + def runTest(self): + doc = scour.scourXmlFile('unittests/path-empty-move.svg') + self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100l200 100z') + self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200l100 100z') + class DefaultsRemovalToplevel(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('fill-rule'), '', - 'Default attribute fill-rule:nonzero not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('fill-rule'), '', + 'Default attribute fill-rule:nonzero not removed') + class DefaultsRemovalToplevelInverse(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('fill-rule'), 'evenodd', - 'Non-Default attribute fill-rule:evenodd removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('fill-rule'), 'evenodd', + 'Non-Default attribute fill-rule:evenodd removed') + class DefaultsRemovalToplevelFormat(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('stroke-width'), '', - 'Default attribute stroke-width:1.00 not removed'); + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('stroke-width'), '', + 'Default attribute stroke-width:1.00 not removed') + class DefaultsRemovalInherited(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[3].getAttribute('fill-rule'), '', - 'Default attribute fill-rule:nonzero not removed in child') + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[3].getAttribute('fill-rule'), '', + 'Default attribute fill-rule:nonzero not removed in child') + class DefaultsRemovalInheritedInverse(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('fill-rule'), 'evenodd', - 'Non-Default attribute fill-rule:evenodd removed in child') + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('fill-rule'), 'evenodd', + 'Non-Default attribute fill-rule:evenodd removed in child') + class DefaultsRemovalInheritedFormat(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('stroke-width'), '', - 'Default attribute stroke-width:1.00 not removed in child') + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('stroke-width'), '', + 'Default attribute stroke-width:1.00 not removed in child') + class DefaultsRemovalOverwrite(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[5].getAttribute('fill-rule'), 'nonzero', - 'Default attribute removed, although it overwrites parent element') + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[5].getAttribute('fill-rule'), 'nonzero', + 'Default attribute removed, although it overwrites parent element') + class DefaultsRemovalOverwriteMarker(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[4].getAttribute('marker-start'), 'none', - 'Default marker attribute removed, although it overwrites parent element') + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[4].getAttribute('marker-start'), 'none', + 'Default marker attribute removed, although it overwrites parent element') + class DefaultsRemovalNonOverwrite(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') - self.assertEqual(doc.getElementsByTagName('path')[10].getAttribute('fill-rule'), '', - 'Default attribute not removed, although its parent used default') + + def runTest(self): + doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + self.assertEqual(doc.getElementsByTagName('path')[10].getAttribute('fill-rule'), '', + 'Default attribute not removed, although its parent used default') + class RemoveDefsWithUnreferencedElements(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/useless-defs.svg') - self.assertEqual(doc.getElementsByTagName('defs').length, 0, - 'Kept defs, although it contains only unreferenced elements') + + def runTest(self): + doc = scour.scourXmlFile('unittests/useless-defs.svg') + self.assertEqual(doc.getElementsByTagName('defs').length, 0, + 'Kept defs, although it contains only unreferenced elements') + class RemoveDefsWithWhitespace(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/whitespace-defs.svg') - self.assertEqual(doc.getElementsByTagName('defs').length, 0, - 'Kept defs, although it contains only whitespace or is <defs/>') + + def runTest(self): + doc = scour.scourXmlFile('unittests/whitespace-defs.svg') + self.assertEqual(doc.getElementsByTagName('defs').length, 0, + 'Kept defs, although it contains only whitespace or is <defs/>') + class TransformIdentityMatrix(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity matrix not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity matrix not removed') + class TransformRotate135(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-135.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(135)', - 'Rotation matrix not converted to rotate(135)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-135.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(135)', + 'Rotation matrix not converted to rotate(135)') + class TransformRotate45(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-45.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(45)', - 'Rotation matrix not converted to rotate(45)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-45.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(45)', + 'Rotation matrix not converted to rotate(45)') + class TransformRotate90(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-90.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', - 'Rotation matrix not converted to rotate(90)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-90.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', + 'Rotation matrix not converted to rotate(90)') + class TransformRotateCCW135(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-225.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(225)', - 'Counter-clockwise rotation matrix not converted to rotate(225)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-225.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(225)', + 'Counter-clockwise rotation matrix not converted to rotate(225)') + class TransformRotateCCW45(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-neg-45.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-45)', - 'Counter-clockwise rotation matrix not converted to rotate(-45)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-neg-45.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-45)', + 'Counter-clockwise rotation matrix not converted to rotate(-45)') + class TransformRotateCCW90(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-neg-90.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-90)', - 'Counter-clockwise rotation matrix not converted to rotate(-90)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-neg-90.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-90)', + 'Counter-clockwise rotation matrix not converted to rotate(-90)') + class TransformScale2by3(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-scale-2-3.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(2 3)', - 'Scaling matrix not converted to scale(2 3)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-scale-2-3.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(2 3)', + 'Scaling matrix not converted to scale(2 3)') + class TransformScaleMinus1(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-scale-neg-1.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(-1)', - 'Scaling matrix not converted to scale(-1)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-scale-neg-1.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(-1)', + 'Scaling matrix not converted to scale(-1)') + class TransformTranslate(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-translate.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'translate(2 3)', - 'Translation matrix not converted to translate(2 3)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-matrix-is-translate.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'translate(2 3)', + 'Translation matrix not converted to translate(2 3)') + class TransformRotationRange719_5(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-rotate-trim-range-719.5.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-.5)', - 'Transform containing rotate(719.5) not shortened to rotate(-.5)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-rotate-trim-range-719.5.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-.5)', + 'Transform containing rotate(719.5) not shortened to rotate(-.5)') + class TransformRotationRangeCCW540_0(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-rotate-trim-range-neg-540.0.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(180)', - 'Transform containing rotate(-540.0) not shortened to rotate(180)') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-rotate-trim-range-neg-540.0.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(180)', + 'Transform containing rotate(-540.0) not shortened to rotate(180)') + class TransformRotation3Args(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-rotate-fold-3args.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', - 'Optional zeroes in rotate(angle 0 0) not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-rotate-fold-3args.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', + 'Optional zeroes in rotate(angle 0 0) not removed') + class TransformIdentityRotation(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-rotate-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity rotation not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-rotate-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity rotation not removed') + class TransformIdentitySkewX(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-skewX-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity X-axis skew not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-skewX-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity X-axis skew not removed') + class TransformIdentitySkewY(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-skewY-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity Y-axis skew not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-skewY-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity Y-axis skew not removed') + class TransformIdentityTranslate(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/transform-translate-is-identity.svg') - self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', - 'Transform containing identity translation not removed') + + def runTest(self): + doc = scour.scourXmlFile('unittests/transform-translate-is-identity.svg') + self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', + 'Transform containing identity translation not removed') + class DuplicateGradientsUpdateStyle(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/duplicate-gradients-update-style.svg', - scour.parse_args(['--disable-style-to-xml'])) - gradient = doc.getElementsByTagName('linearGradient')[0] - rects = doc.getElementsByTagName('rect') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[0].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" was not updated to reflect this') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[1].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" was not updated to reflect this') - self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ') #fff', rects[2].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" (with fallback) was not updated to reflect this') + + def runTest(self): + doc = scour.scourXmlFile('unittests/duplicate-gradients-update-style.svg', + scour.parse_args(['--disable-style-to-xml'])) + gradient = doc.getElementsByTagName('linearGradient')[0] + rects = doc.getElementsByTagName('rect') + self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[0].getAttribute('style'), + 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" was not updated to reflect this') + self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[1].getAttribute('style'), + 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" was not updated to reflect this') + self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ') #fff', rects[2].getAttribute('style'), + 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" (with fallback) was not updated to reflect this') + class DocWithFlowtext(unittest.TestCase): + def runTest(self): with self.assertRaises(Exception): scour.scourXmlFile('unittests/flowtext.svg', scour.parse_args(['--error-on-flowtext'])) + class DocWithNoFlowtext(unittest.TestCase): + def runTest(self): try: scour.scourXmlFile('unittests/flowtext-less.svg', @@ -1591,10 +2048,11 @@ class DocWithNoFlowtext(unittest.TestCase): class ParseStyleAttribute(unittest.TestCase): - def runTest(self): - doc = scour.scourXmlFile('unittests/style.svg') - self.assertEqual(doc.documentElement.getAttribute('style'), 'property1:value1;property2:value2;property3:value3', - 'Style attribute not properly parsed and/or serialized') + + def runTest(self): + doc = scour.scourXmlFile('unittests/style.svg') + self.assertEqual(doc.documentElement.getAttribute('style'), 'property1:value1;property2:value2;property3:value3', + 'Style attribute not properly parsed and/or serialized') # TODO: write tests for --enable-viewboxing # TODO; write a test for embedding rasters @@ -1602,7 +2060,7 @@ class ParseStyleAttribute(unittest.TestCase): # TODO: write tests for --keep-editor-data if __name__ == '__main__': - testcss = __import__('testcss') - scour = __import__('__main__') - suite = unittest.TestSuite( list(map(unittest.defaultTestLoader.loadTestsFromModule, [testcss, scour])) ) - unittest.main(defaultTest="suite") + testcss = __import__('testcss') + scour = __import__('__main__') + suite = unittest.TestSuite(list(map(unittest.defaultTestLoader.loadTestsFromModule, [testcss, scour]))) + unittest.main(defaultTest="suite") From fc356815a275d558bb29f63dfd262c5885ae9b7f Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Thu, 15 Sep 2016 01:54:19 +0200 Subject: [PATCH 2/6] Some reformatting and manually break all long lines at column 119 --- scour/scour.py | 143 ++++++++++++++++++++++++++++++------------------- testscour.py | 140 +++++++++++++++++++++++++++++++---------------- 2 files changed, 182 insertions(+), 101 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 78bb66b..6b5da5d 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -32,8 +32,8 @@ # * Collapse all group based transformations # Even more ideas here: http://esw.w3.org/topic/SvgTidy -# * analysis of path elements to see if rect can be used instead? (must also need to look -# at rounded corners) +# * analysis of path elements to see if rect can be used instead? +# (must also need to look at rounded corners) # Next Up: # - why are marker-start, -end not removed from the style attribute? @@ -104,9 +104,9 @@ unwanted_ns = [NS['SODIPODI'], NS['INKSCAPE'], NS['ADOBE_ILLUSTRATOR'], # A list of all SVG presentation properties # # Sources for this list: -# https://www.w3.org/TR/SVG/propidx.html (implemented) -# https://www.w3.org/TR/SVGTiny12/attributeTable.html (implemented) -# https://www.w3.org/TR/SVG2/propidx.html (not yet implemented) +# https://www.w3.org/TR/SVG/propidx.html (implemented) +# https://www.w3.org/TR/SVGTiny12/attributeTable.html (implemented) +# https://www.w3.org/TR/SVG2/propidx.html (not yet implemented) # svgAttributes = [ # SVG 1.1 @@ -337,9 +337,9 @@ colors = { # A list of default poperties that are safe to remove # # Sources for this list: -# https://www.w3.org/TR/SVG/propidx.html (implemented) -# https://www.w3.org/TR/SVGTiny12/attributeTable.html (implemented) -# https://www.w3.org/TR/SVG2/propidx.html (not yet implemented) +# https://www.w3.org/TR/SVG/propidx.html (implemented) +# https://www.w3.org/TR/SVGTiny12/attributeTable.html (implemented) +# https://www.w3.org/TR/SVG2/propidx.html (not yet implemented) # default_properties = { # excluded all properties with 'auto' as default # SVG 1.1 presentation attributes @@ -1370,7 +1370,8 @@ def removeDuplicateGradients(doc): # compare grad to ograd (all properties, then all stops) # if attributes do not match, go to next gradient someGradAttrsDoNotMatch = False - for attr in ['gradientUnits', 'spreadMethod', 'gradientTransform', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy', 'r']: + for attr in ['gradientUnits', 'spreadMethod', 'gradientTransform', + 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'fx', 'fy', 'r']: if grad.getAttribute(attr) != ograd.getAttribute(attr): someGradAttrsDoNotMatch = True break @@ -1484,7 +1485,9 @@ def repairStyle(node, options): for prop in ['fill', 'stroke']: if prop in styleMap: chunk = styleMap[prop].split(') ') - if len(chunk) == 2 and (chunk[0][:5] == 'url(#' or chunk[0][:6] == 'url("#' or chunk[0][:6] == "url('#") and chunk[1] == 'rgb(0, 0, 0)': + if (len(chunk) == 2 + and (chunk[0][:5] == 'url(#' or chunk[0][:6] == 'url("#' or chunk[0][:6] == "url('#") + and chunk[1] == 'rgb(0, 0, 0)'): styleMap[prop] = chunk[0] + ')' num += 1 @@ -1676,7 +1679,8 @@ def styleInheritedByChild(node, style, nodeIsChild=False): # If the current element is a container element the inherited style is meaningless # (since we made sure it's not inherited by any of its children) - if node.nodeName in ['a', 'defs', 'glyph', 'g', 'marker', 'mask', 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol']: + if node.nodeName in ['a', 'defs', 'glyph', 'g', 'marker', 'mask', + 'missing-glyph', 'pattern', 'svg', 'switch', 'symbol']: return False # in all other cases we have to assume the inherited value of 'style' is meaningfull and has to be kept @@ -1730,18 +1734,19 @@ def mayContainTextNodes(node): # A list of default attributes that are safe to remove if all conditions are fulfilled # # Each default attribute is an object of type 'DefaultAttribute' with the following fields: -# name - name of the attribute to be matched -# value - default value of the attribute -# units - the unit(s) for which 'value' is valid (see 'Unit' class for possible specifications) -# elements - name(s) of SVG element(s) for which the attribute specification is valid -# conditions - additional conditions that have to be fulfilled for removal of the specified default attribute -# implemented as lambda functions with one argument (a xml.dom.minidom node) evaluating to True or False +# name - name of the attribute to be matched +# value - default value of the attribute +# units - the unit(s) for which 'value' is valid (see 'Unit' class for possible specifications) +# elements - name(s) of SVG element(s) for which the attribute specification is valid +# conditions - additional conditions that have to be fulfilled for removal of the specified default attribute +# implemented as lambda functions with one argument (an xml.dom.minidom node) +# evaluating to either True or False # When not specifying a field value, it will be ignored (i.e. always matches) # # Sources for this list: -# https://www.w3.org/TR/SVG/attindex.html (mostly implemented) -# https://www.w3.org/TR/SVGTiny12/attributeTable.html (not yet implemented) -# https://www.w3.org/TR/SVG2/attindex.html (not yet implemented) +# https://www.w3.org/TR/SVG/attindex.html (mostly implemented) +# https://www.w3.org/TR/SVGTiny12/attributeTable.html (not yet implemented) +# https://www.w3.org/TR/SVG2/attindex.html (not yet implemented) # DefaultAttribute = namedtuple('DefaultAttribute', ['name', 'value', 'units', 'elements', 'conditions']) DefaultAttribute.__new__.__defaults__ = (None,) * len(DefaultAttribute._fields) @@ -1756,21 +1761,26 @@ default_attributes = [ DefaultAttribute('patternContentUnits', 'userSpaceOnUse', elements='pattern'), DefaultAttribute('primitiveUnits', 'userSpaceOnUse', elements='filter'), - DefaultAttribute('externalResourcesRequired', 'false', elements=['a', 'altGlyph', 'animate', 'animateColor', - 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'cursor', 'defs', 'ellipse', 'feImage', 'filter', - 'font', 'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker', 'mask', 'mpath', 'path', 'pattern', - 'polygon', 'polyline', 'radialGradient', 'rect', 'script', 'set', 'svg', 'switch', 'symbol', 'text', 'textPath', - 'tref', 'tspan', 'use', 'view']), + DefaultAttribute('externalResourcesRequired', 'false', + elements=['a', 'altGlyph', 'animate', 'animateColor', + 'animateMotion', 'animateTransform', 'circle', 'clipPath', 'cursor', 'defs', 'ellipse', + 'feImage', 'filter', 'font', 'foreignObject', 'g', 'image', 'line', 'linearGradient', + 'marker', 'mask', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', + 'rect', 'script', 'set', 'svg', 'switch', 'symbol', 'text', 'textPath', 'tref', 'tspan', + 'use', 'view']), # svg elements DefaultAttribute('width', 100, Unit.PCT, elements='svg'), DefaultAttribute('height', 100, Unit.PCT, elements='svg'), DefaultAttribute('baseProfile', 'none', elements='svg'), - DefaultAttribute('preserveAspectRatio', 'xMidYMid meet', elements=['feImage', 'image', 'marker', 'pattern', 'svg', 'symbol', 'view']), + DefaultAttribute('preserveAspectRatio', 'xMidYMid meet', + elements=['feImage', 'image', 'marker', 'pattern', 'svg', 'symbol', 'view']), # common attributes / basic types - DefaultAttribute('x', 0, elements=['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', 'image', 'pattern', 'rect', 'svg', 'text', 'use']), - DefaultAttribute('y', 0, elements=['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', 'image', 'pattern', 'rect', 'svg', 'text', 'use']), + DefaultAttribute('x', 0, elements=['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', + 'image', 'pattern', 'rect', 'svg', 'text', 'use']), + DefaultAttribute('y', 0, elements=['cursor', 'fePointLight', 'feSpotLight', 'foreignObject', + 'image', 'pattern', 'rect', 'svg', 'text', 'use']), DefaultAttribute('z', 0, elements=['fePointLight', 'feSpotLight']), DefaultAttribute('x1', 0, elements='line'), DefaultAttribute('y1', 0, elements='line'), @@ -1795,29 +1805,39 @@ default_attributes = [ # filters and masks DefaultAttribute('x', -10, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('x', -0.1, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('x', -0.1, Unit.NONE, ['filter', 'mask'], + conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('y', -10, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('y', -0.1, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('y', -0.1, Unit.NONE, ['filter', 'mask'], + conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('width', 120, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('width', 1.2, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('width', 1.2, Unit.NONE, ['filter', 'mask'], + conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('height', 120, Unit.PCT, ['filter', 'mask']), - DefaultAttribute('height', 1.2, Unit.NONE, ['filter', 'mask'], lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('height', 1.2, Unit.NONE, ['filter', 'mask'], + conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), # gradients DefaultAttribute('x1', 0, elements='linearGradient'), DefaultAttribute('y1', 0, elements='linearGradient'), DefaultAttribute('y2', 0, elements='linearGradient'), DefaultAttribute('x2', 100, Unit.PCT, 'linearGradient'), - DefaultAttribute('x2', 1, Unit.NONE, 'linearGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('x2', 1, Unit.NONE, 'linearGradient', + conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), # remove fx/fy before cx/cy to catch the case where fx = cx = 50% or fy = cy = 50% respectively - DefaultAttribute('fx', elements='radialGradient', conditions=lambda node: node.getAttribute('fx') == node.getAttribute('cx')), - DefaultAttribute('fy', elements='radialGradient', conditions=lambda node: node.getAttribute('fy') == node.getAttribute('cy')), + DefaultAttribute('fx', elements='radialGradient', + conditions=lambda node: node.getAttribute('fx') == node.getAttribute('cx')), + DefaultAttribute('fy', elements='radialGradient', + conditions=lambda node: node.getAttribute('fy') == node.getAttribute('cy')), DefaultAttribute('r', 50, Unit.PCT, 'radialGradient'), - DefaultAttribute('r', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('r', 0.5, Unit.NONE, 'radialGradient', + conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('cx', 50, Unit.PCT, 'radialGradient'), - DefaultAttribute('cx', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('cx', 0.5, Unit.NONE, 'radialGradient', + conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('cy', 50, Unit.PCT, 'radialGradient'), - DefaultAttribute('cy', 0.5, Unit.NONE, 'radialGradient', lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), + DefaultAttribute('cy', 0.5, Unit.NONE, 'radialGradient', + conditions=lambda node: node.getAttribute('gradientUnits') != 'userSpaceOnUse'), DefaultAttribute('spreadMethod', 'pad'), # filter effects @@ -1888,8 +1908,11 @@ def removeDefaultAttributeValue(node, attribute): return 1 else: nodeValue = SVGLength(node.getAttribute(attribute.name)) - if (attribute.value is None) or ((nodeValue.value == attribute.value) and not (nodeValue.units == Unit.INVALID)): - if (attribute.units is None) or (nodeValue.units == attribute.units) or (isinstance(attribute.units, list) and nodeValue.units in attribute.units): + if ((attribute.value is None) + or ((nodeValue.value == attribute.value) and not (nodeValue.units == Unit.INVALID))): + if ((attribute.units is None) + or (nodeValue.units == attribute.units) + or (isinstance(attribute.units, list) and nodeValue.units in attribute.units)): if (attribute.conditions is None) or attribute.conditions(node): node.removeAttribute(attribute.name) return 1 @@ -2325,9 +2348,11 @@ def cleanPath(element, options): newPath.append((cmd, lineTuples)) # convert Bézier curve segments into s where possible elif cmd == 'c': - # set up the assumed bezier control point as the current point, i.e. (0,0) since we're using relative coords + # set up the assumed bezier control point as the current point, + # i.e. (0,0) since we're using relative coords bez_ctl_pt = (0, 0) - # however if the previous command was 's' the assumed control point is a reflection of the previous control point at the current point + # however if the previous command was 's' + # the assumed control point is a reflection of the previous control point at the current point if len(newPath): (prevCmd, prevData) = newPath[-1] if prevCmd == 's': @@ -2732,9 +2757,9 @@ def optimizeTransform(transform): """ # FIXME: reordering these would optimize even more cases: # first: Fold consecutive runs of the same transformation - # extra: Attempt to cast between types to create sameness: - # "matrix(0 1 -1 0 0 0) rotate(180) scale(-1)" all - # are rotations (90, 180, 180) -- thus "rotate(90)" + # extra: Attempt to cast between types to create sameness: + # "matrix(0 1 -1 0 0 0) rotate(180) scale(-1)" all + # are rotations (90, 180, 180) -- thus "rotate(90)" # second: Simplify transforms where numbers are optional. # third: Attempt to simplify any single remaining matrix() # @@ -3068,15 +3093,15 @@ def remapNamespacePrefix(node, oldprefix, newprefix): def makeWellFormed(str): # Don't escape quotation marks for now as they are fine in text nodes # as well as in attributes if used reciprocally - # xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} + # xml_ents = { '<':'<', '>':'>', '&':'&', "'":''', '"':'"'} xml_ents = {'<': '<', '>': '>', '&': '&'} # starr = [] # for c in str: -# if c in xml_ents: -# starr.append(xml_ents[c]) -# else: -# starr.append(c) +# if c in xml_ents: +# starr.append(xml_ents[c]) +# else: +# starr.append(c) # this list comprehension is short-form for the above for-loop: return ''.join([xml_ents[c] if c in xml_ents else c for c in str]) @@ -3222,7 +3247,8 @@ def scourString(in_string, options=None): options = sanitizeOptions(options) # create decimal context with reduced precision for scouring numbers - # calculations should be done in the default context (precision defaults to 28 significant digits) to minimize errors + # calculations should be done in the default context (precision defaults to 28 significant digits) + # to minimize errors global scouringContext scouringContext = Context(prec=options.digits) @@ -3240,7 +3266,8 @@ def scourString(in_string, options=None): # flowRoot elements don't render at all on current browsers (04/2016) cnt_flowText_el = len(doc.getElementsByTagName('flowRoot')) if cnt_flowText_el: - errmsg = "SVG input document uses {} flow text elements, which won't render on browsers!".format(cnt_flowText_el) + errmsg = "SVG input document uses {} flow text elements, " \ + "which won't render on browsers!".format(cnt_flowText_el) if options.error_on_flowtext: raise Exception(errmsg) else: @@ -3404,7 +3431,8 @@ def scourString(in_string, options=None): numBytesSavedInIDs += shortenIDs(doc, options.shorten_ids_prefix, unprotected_ids(doc, options)) # scour lengths (including coordinates) - for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', 'linearGradient', 'radialGradient', 'stop', 'filter']: + for type in ['svg', 'image', 'rect', 'circle', 'ellipse', 'line', + 'linearGradient', 'radialGradient', 'stop', 'filter']: for elem in doc.getElementsByTagName(type): for attr in ['x', 'y', 'width', 'height', 'cx', 'cy', 'r', 'rx', 'ry', 'x1', 'y1', 'x2', 'y2', 'fx', 'fy', 'offset']: @@ -3537,7 +3565,8 @@ _option_group_optimization.add_option("--create-groups", help="create <g> elements for runs of elements with identical attributes") _option_group_optimization.add_option("--keep-editor-data", action="store_true", dest="keep_editor_data", default=False, - help="won't remove Inkscape, Sodipodi, Adobe Illustrator or Sketch elements and attributes") + help="won't remove Inkscape, Sodipodi, Adobe Illustrator " + "or Sketch elements and attributes") _option_group_optimization.add_option("--keep-unreferenced-defs", action="store_true", dest="keep_defs", default=False, help="won't remove elements within the defs container that are unreferenced") @@ -3561,7 +3590,8 @@ _option_group_document.add_option("--remove-descriptions", help="remove <desc> elements") _option_group_document.add_option("--remove-metadata", action="store_true", dest="remove_metadata", default=False, - help="remove <metadata> elements (which may contain license/author information etc.)") + help="remove <metadata> elements " + "(which may contain license/author information etc.)") _option_group_document.add_option("--remove-descriptive-elements", action="store_true", dest="remove_descriptive_elements", default=False, help="remove <title>, <desc> and <metadata> elements") @@ -3616,7 +3646,8 @@ _options_parser.add_option_group(_option_group_ids) _option_group_compatibility = optparse.OptionGroup(_options_parser, "SVG compatibility checks") _option_group_compatibility.add_option("--error-on-flowtext", action="store_true", dest="error_on_flowtext", default=False, - help="In case the input SVG uses flow text, bail out with error. Otherwise only warn. (default: False)") + help="If the input SVG uses non-standard flowing text exit with error. " + "Otherwise only warn.") _options_parser.add_option_group(_option_group_compatibility) diff --git a/testscour.py b/testscour.py index adc2021..5f9515c 100755 --- a/testscour.py +++ b/testscour.py @@ -63,7 +63,8 @@ class EmptyOptions(unittest.TestCase): fail = False except: fail = True - self.assertEqual(fail, False, 'Exception when calling Scour with empty options object') + self.assertEqual(fail, False, + 'Exception when calling Scour with empty options object') class InvalidOptions(unittest.TestCase): @@ -76,7 +77,8 @@ class InvalidOptions(unittest.TestCase): fail = False except: fail = True - self.assertEqual(fail, False, 'Exception when calling Scour with invalid options') + self.assertEqual(fail, False, + 'Exception when calling Scour with invalid options') class GetElementById(unittest.TestCase): @@ -94,7 +96,8 @@ class NoInkscapeElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, - lambda e: e.namespaceURI != 'http://www.inkscape.org/namespaces/inkscape'), False, + lambda e: e.namespaceURI != 'http://www.inkscape.org/namespaces/inkscape'), + False, 'Found Inkscape elements') @@ -102,7 +105,8 @@ class NoSodipodiElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, - lambda e: e.namespaceURI != 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'), False, + lambda e: e.namespaceURI != 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'), + False, 'Found Sodipodi elements') @@ -110,7 +114,8 @@ class NoAdobeIllustratorElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeIllustrator/10.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeIllustrator/10.0/'), + False, 'Found Adobe Illustrator elements') @@ -118,7 +123,8 @@ class NoAdobeGraphsElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Graphs/1.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Graphs/1.0/'), + False, 'Found Adobe Graphs elements') @@ -126,7 +132,8 @@ class NoAdobeSVGViewerElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/'), + False, 'Found Adobe SVG Viewer elements') @@ -134,7 +141,8 @@ class NoAdobeVariablesElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Variables/1.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Variables/1.0/'), + False, 'Found Adobe Variables elements') @@ -142,7 +150,8 @@ class NoAdobeSaveForWebElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/SaveForWeb/1.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/SaveForWeb/1.0/'), + False, 'Found Adobe Save For Web elements') @@ -150,7 +159,8 @@ class NoAdobeExtensibilityElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Extensibility/1.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Extensibility/1.0/'), + False, 'Found Adobe Extensibility elements') @@ -158,7 +168,8 @@ class NoAdobeFlowsElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/Flows/1.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/Flows/1.0/'), + False, 'Found Adobe Flows elements') @@ -166,7 +177,8 @@ class NoAdobeImageReplacementElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/ImageReplacement/1.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/ImageReplacement/1.0/'), + False, 'Found Adobe Image Replacement elements') @@ -174,7 +186,8 @@ class NoAdobeCustomElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/GenericCustomNamespace/1.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/GenericCustomNamespace/1.0/'), + False, 'Found Adobe Custom elements') @@ -182,7 +195,8 @@ class NoAdobeXPathElements(unittest.TestCase): def runTest(self): self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, - lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), False, + lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), + False, 'Found Adobe XPath elements') @@ -464,8 +478,8 @@ class NoSodipodiAttributes(unittest.TestCase): if attrs.item(i).namespaceURI == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': return False return True - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, - findSodipodiAttr), False, + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, findSodipodiAttr), + False, 'Found Sodipodi attributes') @@ -480,8 +494,8 @@ class NoInkscapeAttributes(unittest.TestCase): if attrs.item(i).namespaceURI == 'http://www.inkscape.org/namespaces/inkscape': return False return True - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement, - findInkscapeAttr), False, + self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement, findInkscapeAttr), + False, 'Found Inkscape attributes') @@ -900,9 +914,11 @@ class ChangeBezierToShorthandInPath(unittest.TestCase): self.assertEqual(doc.getElementById('path1').getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', 'Did not change bezier curves into shorthand curve segments in path') self.assertEqual(doc.getElementById('path2a').getAttribute('d'), 'm200 200s200 100 200 0', - 'Did not change bezier curve into shorthand curve segment when first control point is the current point and previous command was not a bezier curve') + 'Did not change bezier curve into shorthand curve segment when first control point ' + 'is the current point and previous command was not a bezier curve') self.assertEqual(doc.getElementById('path2b').getAttribute('d'), 'm0 300s200-100 200 0c0 0 200 100 200 0', - 'Did change bezier curve into shorthand curve segment when first control point is the current point but previous command was a bezier curve with a different control point') + 'Did change bezier curve into shorthand curve segment when first control point ' + 'is the current point but previous command was a bezier curve with a different control point') class ChangeQuadToShorthandInPath(unittest.TestCase): @@ -917,25 +933,42 @@ class DoNotOptimzePathIfLarger(unittest.TestCase): def runTest(self): p = scour.scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] - self.assertTrue(len(p.getAttribute('d')) <= len("M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"), + self.assertTrue(len(p.getAttribute('d')) <= + # this was the scoured path data as of 2016-08-31 without the length check in cleanPath(): + # d="m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234-0.00345-0.00345z" + len("M100,100 L200.12345,200.12345 C215,205 185,195 200.12,200.12 Z"), 'Made path data longer during optimization') - # this was the scoured path data as of 2016-08-31 without the length check in cleanPath(): - # d="m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234-0.00345-0.00345z" class HandleEncodingUTF8(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/encoding-utf8.svg') - text = u'Hello in many languages:\nar: أهلا\nbn: হ্যালো\nel: Χαίρετε\nen: Hello\nhi: नमस्ते\niw: שלום\nja: こんにちは\nkm: ជំរាបសួរ\nml: ഹലോ\nru: Здравствуйте\nur: ہیلو\nzh: 您好' + text = u'Hello in many languages:\n' \ + u'ar: أهلا\n' \ + u'bn: হ্যালো\n' \ + u'el: Χαίρετε\n' \ + u'en: Hello\n' \ + u'hi: नमस्ते\n' \ + u'iw: שלום\n' \ + u'ja: こんにちは\n' \ + u'km: ជំរាបសួរ\n' \ + u'ml: ഹലോ\n' \ + u'ru: Здравствуйте\n' \ + u'ur: ہیلو\n' \ + u'zh: 您好' desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() - self.assertEqual(desc, text, 'Did not handle international UTF8 characters') + self.assertEqual(desc, text, + 'Did not handle international UTF8 characters') desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[1].firstChild.wholeText).strip() - self.assertEqual(desc, u'“”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿�', 'Did not handle common UTF8 characters') + self.assertEqual(desc, u'“”‘’–—…‐‒°©®™•½¼¾⅓⅔†‡µ¢£€«»♠♣♥♦¿�', + 'Did not handle common UTF8 characters') desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[2].firstChild.wholeText).strip() - self.assertEqual(desc, u':-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔', 'Did not handle mathematical UTF8 characters') + self.assertEqual(desc, u':-×÷±∞π∅≤≥≠≈∧∨∩∪∈∀∃∄∑∏←↑→↓↔↕↖↗↘↙↺↻⇒⇔', + 'Did not handle mathematical UTF8 characters') desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[3].firstChild.wholeText).strip() - self.assertEqual(desc, u'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎', 'Did not handle superscript/subscript UTF8 characters') + self.assertEqual(desc, u'⁰¹²³⁴⁵⁶⁷⁸⁹⁺⁻⁽⁾ⁿⁱ₀₁₂₃₄₅₆₇₈₉₊₋₌₍₎', + 'Did not handle superscript/subscript UTF8 characters') class HandleEncodingISO_8859_15(unittest.TestCase): @@ -997,7 +1030,8 @@ class TranslateLongHexColorIntoShortHex(unittest.TestCase): class DoNotConvertShortColorNames(unittest.TestCase): def runTest(self): - elem = scour.scourXmlFile('unittests/dont-convert-short-color-names.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + elem = scour.scourXmlFile('unittests/dont-convert-short-color-names.svg') \ + .getElementsByTagNameNS(SVGNS, 'rect')[0] self.assertEqual('red', elem.getAttribute('fill'), 'Converted short color name to longer hex string') @@ -1259,9 +1293,11 @@ class RemoveDefaultGradX2Value(unittest.TestCase): self.assertEqual(doc.getElementById('grad1').getAttribute('x2'), '', 'x2="100%" not removed') self.assertEqual(doc.getElementById('grad1b').getAttribute('x2'), '', - 'x2="1" not removed, which is equal to the default x2="100%" when gradientUnits="objectBoundingBox"') + 'x2="1" not removed, ' + 'which is equal to the default x2="100%" when gradientUnits="objectBoundingBox"') self.assertNotEqual(doc.getElementById('grad1c').getAttribute('x2'), '', - 'x2="1" removed, which is NOT equal to the default x2="100%" when gradientUnits="userSpaceOnUse"') + 'x2="1" removed, ' + 'which is NOT equal to the default x2="100%" when gradientUnits="userSpaceOnUse"') class RemoveDefaultGradY2Value(unittest.TestCase): @@ -1422,7 +1458,8 @@ class MoveSVGElementsToDefaultNamespace(unittest.TestCase): class MoveCommonAttributesToParent(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/move-common-attributes-to-parent.svg').getElementsByTagNameNS(SVGNS, 'g')[0] + g = scour.scourXmlFile('unittests/move-common-attributes-to-parent.svg') \ + .getElementsByTagNameNS(SVGNS, 'g')[0] self.assertEqual(g.getAttribute('fill'), '#0F0', 'Did not move common fill attribute to parent group') @@ -1430,7 +1467,8 @@ class MoveCommonAttributesToParent(unittest.TestCase): class RemoveCommonAttributesFromChild(unittest.TestCase): def runTest(self): - r = scour.scourXmlFile('unittests/move-common-attributes-to-parent.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + r = scour.scourXmlFile('unittests/move-common-attributes-to-parent.svg') \ + .getElementsByTagNameNS(SVGNS, 'rect')[0] self.assertNotEqual(r.getAttribute('fill'), '#0F0', 'Did not remove common fill attribute from child') @@ -1438,7 +1476,8 @@ class RemoveCommonAttributesFromChild(unittest.TestCase): class DontRemoveCommonAttributesIfParentHasTextNodes(unittest.TestCase): def runTest(self): - text = scour.scourXmlFile('unittests/move-common-attributes-to-parent.svg').getElementsByTagNameNS(SVGNS, 'text')[0] + text = scour.scourXmlFile('unittests/move-common-attributes-to-parent.svg') \ + .getElementsByTagNameNS(SVGNS, 'text')[0] self.assertNotEqual(text.getAttribute('font-style'), 'italic', 'Removed common attributes when parent contained text elements') @@ -1446,7 +1485,8 @@ class DontRemoveCommonAttributesIfParentHasTextNodes(unittest.TestCase): class PropagateCommonAttributesUp(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/move-common-attributes-to-grandparent.svg').getElementsByTagNameNS(SVGNS, 'g')[0] + g = scour.scourXmlFile('unittests/move-common-attributes-to-grandparent.svg') \ + .getElementsByTagNameNS(SVGNS, 'g')[0] self.assertEqual(g.getAttribute('fill'), '#0F0', 'Did not move common fill attribute to grandparent') @@ -1454,7 +1494,8 @@ class PropagateCommonAttributesUp(unittest.TestCase): class PathEllipticalArcParsingCommaWsp(unittest.TestCase): 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.assertEqual(p.getAttribute('d'), 'm100 100a100 100 0 1 1 -50 100z', 'Did not parse elliptical arc command properly') @@ -1462,7 +1503,8 @@ class PathEllipticalArcParsingCommaWsp(unittest.TestCase): class RemoveUnusedAttributesOnParent(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/remove-unused-attributes-on-parent.svg').getElementsByTagNameNS(SVGNS, 'g')[0] + g = scour.scourXmlFile('unittests/remove-unused-attributes-on-parent.svg') \ + .getElementsByTagNameNS(SVGNS, 'g')[0] self.assertNotEqual(g.getAttribute('stroke'), '#000', 'Unused attributes on group not removed') @@ -1470,7 +1512,8 @@ class RemoveUnusedAttributesOnParent(unittest.TestCase): class DoNotRemoveCommonAttributesOnParentIfAtLeastOneUsed(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/remove-unused-attributes-on-parent.svg').getElementsByTagNameNS(SVGNS, 'g')[0] + g = scour.scourXmlFile('unittests/remove-unused-attributes-on-parent.svg') \ + .getElementsByTagNameNS(SVGNS, 'g')[0] self.assertEqual(g.getAttribute('fill'), '#0F0', 'Used attributes on group were removed') @@ -1478,7 +1521,8 @@ class DoNotRemoveCommonAttributesOnParentIfAtLeastOneUsed(unittest.TestCase): class DoNotRemoveGradientsWhenReferencedInStyleCss(unittest.TestCase): def runTest(self): - grads = scour.scourXmlFile('unittests/css-reference.svg').getElementsByTagNameNS(SVGNS, 'linearGradient') + grads = scour.scourXmlFile('unittests/css-reference.svg') \ + .getElementsByTagNameNS(SVGNS, 'linearGradient') self.assertEqual(grads.length, 2, 'Gradients removed when referenced in CSS') @@ -1516,7 +1560,8 @@ class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): class GetAttrPrefixRight(unittest.TestCase): def runTest(self): - grad = scour.scourXmlFile('unittests/xml-namespace-attrs.svg').getElementsByTagNameNS(SVGNS, 'linearGradient')[1] + grad = scour.scourXmlFile('unittests/xml-namespace-attrs.svg') \ + .getElementsByTagNameNS(SVGNS, 'linearGradient')[1] self.assertEqual(grad.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#linearGradient841', 'Did not get xlink:href prefix right') @@ -1709,7 +1754,8 @@ class GroupNoCreationForTspan(unittest.TestCase): doc = scour.scourXmlFile('unittests/group-no-creation-tspan.svg', scour.parse_args(['--create-groups'])) self.assertEqual(doc.getElementsByTagName('g').length, 0, - 'Created a <g> for a run of <tspan>s that are not allowed as children according to content model') + 'Created a <g> for a run of <tspan>s ' + 'that are not allowed as children according to content model') class DoNotCommonizeAttributesOnReferencedElements(unittest.TestCase): @@ -2022,11 +2068,14 @@ class DuplicateGradientsUpdateStyle(unittest.TestCase): gradient = doc.getElementsByTagName('linearGradient')[0] rects = doc.getElementsByTagName('rect') self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[0].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" was not updated to reflect this') + 'Either of #duplicate-one or #duplicate-two was removed, ' + 'but style="fill:" was not updated to reflect this') self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[1].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" was not updated to reflect this') + 'Either of #duplicate-one or #duplicate-two was removed, ' + 'but style="fill:" was not updated to reflect this') self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ') #fff', rects[2].getAttribute('style'), - 'Either of #duplicate-one or #duplicate-two was removed, but style="fill:" (with fallback) was not updated to reflect this') + 'Either of #duplicate-one or #duplicate-two was removed, ' + 'but style="fill:" (with fallback) was not updated to reflect this') class DocWithFlowtext(unittest.TestCase): @@ -2051,7 +2100,8 @@ class ParseStyleAttribute(unittest.TestCase): def runTest(self): doc = scour.scourXmlFile('unittests/style.svg') - self.assertEqual(doc.documentElement.getAttribute('style'), 'property1:value1;property2:value2;property3:value3', + self.assertEqual(doc.documentElement.getAttribute('style'), + 'property1:value1;property2:value2;property3:value3', 'Style attribute not properly parsed and/or serialized') # TODO: write tests for --enable-viewboxing From 82df0d2327779016a95138daaefa1a696dc99a0f Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Thu, 15 Sep 2016 21:02:15 +0200 Subject: [PATCH 3/6] More PEP 8 cleanup (solves all issues reported by `pycodestyle`) --- scour/scour.py | 68 ++++++++++++++++++++++++++------------------------ testscour.py | 8 +++--- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index 6b5da5d..ec27954 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -66,11 +66,6 @@ import six from six.moves import range from decimal import Context, Decimal, InvalidOperation, getcontext -# select the most precise walltime measurement function available on the platform -if sys.platform.startswith('win'): - walltime = time.clock -else: - walltime = time.time from scour import __version__ @@ -78,6 +73,14 @@ APP = u'scour' VER = __version__ COPYRIGHT = u'Copyright Jeff Schiller, Louis Simard, 2010' + +# select the most precise walltime measurement function available on the platform +if sys.platform.startswith('win'): + walltime = time.clock +else: + walltime = time.time + + NS = {'SVG': 'http://www.w3.org/2000/svg', 'XLINK': 'http://www.w3.org/1999/xlink', 'SODIPODI': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', @@ -486,7 +489,7 @@ class SVGLength(object): self.value = 0 unitBegin = 0 scinum = scinumber.match(str) - if scinum != None: + if scinum is not None: # this will always match, no need to check it numMatch = number.match(str) expMatch = sciExponent.search(str, numMatch.start(0)) @@ -496,7 +499,7 @@ class SVGLength(object): else: # unit or invalid numMatch = number.match(str) - if numMatch != None: + if numMatch is not None: self.value = float(numMatch.group(0)) unitBegin = numMatch.end(0) @@ -505,7 +508,7 @@ class SVGLength(object): if unitBegin != 0: unitMatch = unit.search(str, unitBegin) - if unitMatch != None: + if unitMatch is not None: self.units = Unit.get(unitMatch.group(0)) # invalid @@ -616,7 +619,7 @@ def findReferencingProperty(node, prop, val, ids): # single-quote elif val[0:6] == "url('#": id = val[6:val.find("')")] - if id != None: + if id is not None: if id in ids: ids[id][0] += 1 ids[id][1].append(node) @@ -649,14 +652,13 @@ def removeUnusedDefs(doc, defElem, elemsToRemove=None): for elem in defElem.childNodes: # only look at it if an element and not referenced anywhere else if elem.nodeType == 1 and (elem.getAttribute('id') == '' or - (not elem.getAttribute('id') in referencedIDs)): - + elem.getAttribute('id') not in referencedIDs): # we only inspect the children of a group in a defs if the group # is not referenced anywhere else if elem.nodeName == 'g' and elem.namespaceURI == NS['SVG']: elemsToRemove = removeUnusedDefs(doc, elem, elemsToRemove) # we only remove if it is not one of our tags we always keep (see above) - elif not elem.nodeName in keepTags: + elif elem.nodeName not in keepTags: elemsToRemove.append(elem) return elemsToRemove @@ -677,10 +679,10 @@ def removeUnreferencedElements(doc, keepDefs): referencedIDs = findReferencedElements(doc.documentElement) for id in identifiedElements: - if not id in referencedIDs: + if id not in referencedIDs: goner = identifiedElements[id] - if (goner != None and goner.nodeName in removeTags - and goner.parentNode != None + if (goner is not None and goner.nodeName in removeTags + and goner.parentNode is not None and goner.parentNode.tagName != 'defs'): goner.parentNode.removeChild(goner) num += 1 @@ -723,7 +725,7 @@ def shortenIDs(doc, prefix, unprotectedElements=None): idList = [rid for count, rid in idList] # Add unreferenced IDs to end of idList in arbitrary order - idList.extend([rid for rid in unprotectedElements if not rid in idList]) + idList.extend([rid for rid in unprotectedElements if rid not in idList]) curIdNum = 1 @@ -790,7 +792,7 @@ def renameID(doc, idFrom, idTo, identifiedElements, referencedIDs): # if this node is a style element, parse its text into CSS if node.nodeName == 'style' and node.namespaceURI == NS['SVG']: # node.firstChild will be either a CDATA or a Text node now - if node.firstChild != None: + if node.firstChild is not None: # concatenate the value of all children, in case # there's a CDATASection node surrounded by whitespace # nodes @@ -873,7 +875,7 @@ def removeUnreferencedIDs(referencedIDs, identifiedElements): num = 0 for id in list(identifiedElements.keys()): node = identifiedElements[id] - if (id in referencedIDs) == False and not node.nodeName in keepTags: + if id not in referencedIDs and node.nodeName not in keepTags: node.removeAttribute('id') numIDsRemoved += 1 num += 1 @@ -889,7 +891,7 @@ def removeNamespacedAttributes(node, namespaces): attrsToRemove = [] for attrNum in range(attrList.length): attr = attrList.item(attrNum) - if attr != None and attr.namespaceURI in namespaces: + if attr is not None and attr.namespaceURI in namespaces: attrsToRemove.append(attr.nodeName) for attrName in attrsToRemove: num += 1 @@ -910,7 +912,7 @@ def removeNamespacedElements(node, namespaces): childList = node.childNodes childrenToRemove = [] for child in childList: - if child != None and child.namespaceURI in namespaces: + if child is not None and child.namespaceURI in namespaces: childrenToRemove.append(child) for child in childrenToRemove: num += 1 @@ -1239,7 +1241,7 @@ def removeUnusedAttributesOnParent(elem): inheritedAttrs = [] for name in list(unusedAttrs.keys()): val = child.getAttribute(name) - if val == '' or val == None or val == 'inherit': + if val == '' or val is None or val == 'inherit': inheritedAttrs.append(name) for a in inheritedAttrs: del unusedAttrs[a] @@ -1307,7 +1309,7 @@ def collapseSinglyReferencedGradients(doc): # (Cyn: I've seen documents with #id references but no element with that ID!) if count == 1 and rid in identifiedElements: elem = identifiedElements[rid] - if elem != None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \ + if elem is not None and elem.nodeType == 1 and elem.nodeName in ['linearGradient', 'radialGradient'] \ and elem.namespaceURI == NS['SVG']: # found a gradient that is referenced by only 1 other element refElem = nodes[0] @@ -1577,7 +1579,7 @@ def repairStyle(node, options): if 'overflow' in styleMap: # remove overflow from elements to which it does not apply, # see https://www.w3.org/TR/SVG/masking.html#OverflowProperty - if not node.nodeName in ['svg', 'symbol', 'image', 'foreignObject', 'marker', 'pattern']: + if node.nodeName not in ['svg', 'symbol', 'image', 'foreignObject', 'marker', 'pattern']: del styleMap['overflow'] num += 1 # if the node is not the root <svg> element the SVG's user agent style sheet @@ -1974,14 +1976,14 @@ def convertColor(value): s = colors[s] rgbpMatch = rgbp.match(s) - if rgbpMatch != None: + if rgbpMatch is not None: r = int(float(rgbpMatch.group(1)) * 255.0 / 100.0) g = int(float(rgbpMatch.group(2)) * 255.0 / 100.0) b = int(float(rgbpMatch.group(3)) * 255.0 / 100.0) s = '#%02x%02x%02x' % (r, g, b) else: rgbMatch = rgb.match(s) - if rgbMatch != None: + if rgbMatch is not None: r = int(rgbMatch.group(1)) g = int(rgbMatch.group(2)) b = int(rgbMatch.group(3)) @@ -2576,7 +2578,7 @@ def scourCoordinates(data, options, forceCommaWsp=False): - removes extraneous whitespace - adds spaces between values in a subcommand if required (or if forceCommaWsp is True) """ - if data != None: + if data is not None: newData = [] c = 0 previousCoord = '' @@ -3054,7 +3056,7 @@ def properlySizeDoc(docElement, options): def remapNamespacePrefix(node, oldprefix, newprefix): - if node == None or node.nodeType != 1: + if node is None or node.nodeType != 1: return if node.prefix == oldprefix: @@ -3179,9 +3181,9 @@ def serializeXML(element, options, ind=0, preserveWhitespace=False): outParts.append(' ') # preserve xmlns: if it is a namespace prefix declaration - if attr.prefix != None: + if attr.prefix is not None: outParts.extend([attr.prefix, ':']) - elif attr.namespaceURI != None: + elif attr.namespaceURI is not None: if attr.namespaceURI == 'http://www.w3.org/2000/xmlns/' and attr.nodeName.find('xmlns') == -1: outParts.append('xmlns:') elif attr.namespaceURI == 'http://www.w3.org/1999/xlink': @@ -3279,7 +3281,7 @@ def scourString(in_string, options=None): # for whatever reason this does not always remove all inkscape/sodipodi attributes/elements # on the first pass, so we do it multiple times # does it have to do with removal of children affecting the childlist? - if options.keep_editor_data == False: + if options.keep_editor_data is False: while removeNamespacedElements(doc.documentElement, unwanted_ns) > 0: pass while removeNamespacedAttributes(doc.documentElement, unwanted_ns) > 0: @@ -3358,7 +3360,7 @@ def scourString(in_string, options=None): for tag in ['defs', 'title', 'desc', 'metadata', 'g']: for elem in doc.documentElement.getElementsByTagName(tag): removeElem = not elem.hasChildNodes() - if removeElem == False: + if removeElem is False: for child in elem.childNodes: if child.nodeType in [1, 4, 8]: break @@ -3472,7 +3474,7 @@ def scourString(in_string, options=None): lines.append(line) # return the string with its XML prolog and surrounding comments - if options.strip_xml_prolog == False: + if options.strip_xml_prolog is False: total_output = '<?xml version="1.0" encoding="UTF-8"' if doc.standalone: total_output += ' standalone="yes"' @@ -3663,7 +3665,7 @@ def parse_args(args=None, ignore_additional_args=False): _options_parser.error("Additional arguments not handled: %r, see --help" % rargs) if options.digits < 0: _options_parser.error("Can't have negative significant digits, see --help") - if not options.indent_type in ["tab", "space", "none"]: + if options.indent_type not in ['tab', 'space', 'none']: _options_parser.error("Invalid value for --indent, see --help") if options.indent_depth < 0: _options_parser.error("Value for --nindent should be positive (or zero), see --help") diff --git a/testscour.py b/testscour.py index 5f9515c..0d77d35 100755 --- a/testscour.py +++ b/testscour.py @@ -42,10 +42,10 @@ SVGNS = 'http://www.w3.org/2000/svg' def walkTree(elem, func): - if func(elem) == False: + if func(elem) is False: return False for child in elem.childNodes: - if walkTree(child, func) == False: + if walkTree(child, func) is False: return False return True @@ -472,7 +472,7 @@ class NoSodipodiAttributes(unittest.TestCase): def runTest(self): def findSodipodiAttr(elem): attrs = elem.attributes - if attrs == None: + if attrs is None: return True for i in range(len(attrs)): if attrs.item(i).namespaceURI == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': @@ -488,7 +488,7 @@ class NoInkscapeAttributes(unittest.TestCase): def runTest(self): def findInkscapeAttr(elem): attrs = elem.attributes - if attrs == None: + if attrs is None: return True for i in range(len(attrs)): if attrs.item(i).namespaceURI == 'http://www.inkscape.org/namespaces/inkscape': From 99363c9089b280ae076992c60dbc638c2cc4c202 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Thu, 15 Sep 2016 21:31:34 +0200 Subject: [PATCH 4/6] Fix all issues detected by `pyflakes` --- scour/scour.py | 2 +- scour/svg_regex.py | 2 +- scour/svg_transform.py | 2 +- testscour.py | 485 ++++++++++++++++++++--------------------- 4 files changed, 245 insertions(+), 246 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index ec27954..a531795 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -645,7 +645,6 @@ def removeUnusedDefs(doc, defElem, elemsToRemove=None): if elemsToRemove is None: elemsToRemove = [] - identifiedElements = findElementsWithId(doc.documentElement) referencedIDs = findReferencedElements(doc.documentElement) keepTags = ['font', 'style', 'metadata', 'script', 'title', 'desc'] @@ -2072,6 +2071,7 @@ def cleanPath(element, options): # convert absolute coordinates into relative ones. # Reuse the data structure 'path', since we're not adding or removing subcommands. # Also reuse the coordinate lists since we're not adding or removing any. + x = y = 0 for pathIndex in range(0, len(path)): cmd, data = path[pathIndex] # Changes to cmd don't get through to the data structure i = 0 diff --git a/scour/svg_regex.py b/scour/svg_regex.py index 220dffb..4cba554 100644 --- a/scour/svg_regex.py +++ b/scour/svg_regex.py @@ -44,7 +44,7 @@ Out[5]: [('M', [(100.0, -200.0)])] from __future__ import absolute_import import re -from decimal import * +from decimal import Decimal, getcontext from functools import partial # Sentinel. diff --git a/scour/svg_transform.py b/scour/svg_transform.py index 6ae3701..85bea88 100644 --- a/scour/svg_transform.py +++ b/scour/svg_transform.py @@ -59,7 +59,7 @@ Out[12]: [('translate', [30.0, -30.0]), ('rotate', [36.0])] from __future__ import absolute_import import re -from decimal import * +from decimal import Decimal from six.moves import range from functools import partial diff --git a/testscour.py b/testscour.py index 0d77d35..1f5bbf9 100755 --- a/testscour.py +++ b/testscour.py @@ -26,7 +26,6 @@ import six from six.moves import map, range import unittest -import xml.dom.minidom from scour.svg_regex import svg_parser from scour.scour import scourXmlFile, scourString, parse_args, makeWellFormed @@ -59,7 +58,7 @@ class EmptyOptions(unittest.TestCase): def runTest(self): options = ScourOptions try: - scour.scourXmlFile('unittests/ids-to-strip.svg', options) + scourXmlFile('unittests/ids-to-strip.svg', options) fail = False except: fail = True @@ -73,7 +72,7 @@ class InvalidOptions(unittest.TestCase): options = ScourOptions options.invalidOption = "invalid value" try: - scour.scourXmlFile('unittests/ids-to-strip.svg', options) + scourXmlFile('unittests/ids-to-strip.svg', options) fail = False except: fail = True @@ -84,7 +83,7 @@ class InvalidOptions(unittest.TestCase): class GetElementById(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/ids.svg') + doc = scourXmlFile('unittests/ids.svg') self.assertIsNotNone(doc.getElementById('svg1'), 'Root SVG element not found by ID') self.assertIsNotNone(doc.getElementById('linearGradient1'), 'linearGradient not found by ID') self.assertIsNotNone(doc.getElementById('layer1'), 'g not found by ID') @@ -95,7 +94,7 @@ class GetElementById(unittest.TestCase): class NoInkscapeElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, lambda e: e.namespaceURI != 'http://www.inkscape.org/namespaces/inkscape'), False, 'Found Inkscape elements') @@ -104,7 +103,7 @@ class NoInkscapeElements(unittest.TestCase): class NoSodipodiElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, lambda e: e.namespaceURI != 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'), False, 'Found Sodipodi elements') @@ -113,7 +112,7 @@ class NoSodipodiElements(unittest.TestCase): class NoAdobeIllustratorElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeIllustrator/10.0/'), False, 'Found Adobe Illustrator elements') @@ -122,7 +121,7 @@ class NoAdobeIllustratorElements(unittest.TestCase): class NoAdobeGraphsElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/Graphs/1.0/'), False, 'Found Adobe Graphs elements') @@ -131,7 +130,7 @@ class NoAdobeGraphsElements(unittest.TestCase): class NoAdobeSVGViewerElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/'), False, 'Found Adobe SVG Viewer elements') @@ -140,7 +139,7 @@ class NoAdobeSVGViewerElements(unittest.TestCase): class NoAdobeVariablesElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/Variables/1.0/'), False, 'Found Adobe Variables elements') @@ -149,7 +148,7 @@ class NoAdobeVariablesElements(unittest.TestCase): class NoAdobeSaveForWebElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/SaveForWeb/1.0/'), False, 'Found Adobe Save For Web elements') @@ -158,7 +157,7 @@ class NoAdobeSaveForWebElements(unittest.TestCase): class NoAdobeExtensibilityElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/Extensibility/1.0/'), False, 'Found Adobe Extensibility elements') @@ -167,7 +166,7 @@ class NoAdobeExtensibilityElements(unittest.TestCase): class NoAdobeFlowsElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/Flows/1.0/'), False, 'Found Adobe Flows elements') @@ -176,7 +175,7 @@ class NoAdobeFlowsElements(unittest.TestCase): class NoAdobeImageReplacementElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/ImageReplacement/1.0/'), False, 'Found Adobe Image Replacement elements') @@ -185,7 +184,7 @@ class NoAdobeImageReplacementElements(unittest.TestCase): class NoAdobeCustomElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/GenericCustomNamespace/1.0/'), False, 'Found Adobe Custom elements') @@ -194,7 +193,7 @@ class NoAdobeCustomElements(unittest.TestCase): class NoAdobeXPathElements(unittest.TestCase): def runTest(self): - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/adobe.svg').documentElement, + self.assertNotEqual(walkTree(scourXmlFile('unittests/adobe.svg').documentElement, lambda e: e.namespaceURI != 'http://ns.adobe.com/XPath/1.0/'), False, 'Found Adobe XPath elements') @@ -203,7 +202,7 @@ class NoAdobeXPathElements(unittest.TestCase): class DoNotRemoveTitleWithOnlyText(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') + doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, 'Removed title element with only text child') @@ -211,7 +210,7 @@ class DoNotRemoveTitleWithOnlyText(unittest.TestCase): class RemoveEmptyTitleElement(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') + doc = scourXmlFile('unittests/empty-descriptive-elements.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, 'Did not remove empty title element') @@ -219,7 +218,7 @@ class RemoveEmptyTitleElement(unittest.TestCase): class DoNotRemoveDescriptionWithOnlyText(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') + doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, 'Removed description element with only text child') @@ -227,7 +226,7 @@ class DoNotRemoveDescriptionWithOnlyText(unittest.TestCase): class RemoveEmptyDescriptionElement(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') + doc = scourXmlFile('unittests/empty-descriptive-elements.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, 'Did not remove empty description element') @@ -235,7 +234,7 @@ class RemoveEmptyDescriptionElement(unittest.TestCase): class DoNotRemoveMetadataWithOnlyText(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') + doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 1, 'Removed metadata element with only text child') @@ -243,7 +242,7 @@ class DoNotRemoveMetadataWithOnlyText(unittest.TestCase): class RemoveEmptyMetadataElement(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') + doc = scourXmlFile('unittests/empty-descriptive-elements.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'metadata')), 0, 'Did not remove empty metadata element') @@ -251,7 +250,7 @@ class RemoveEmptyMetadataElement(unittest.TestCase): class DoNotRemoveDescriptiveElementsWithOnlyText(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/descriptive-elements-with-text.svg') + doc = scourXmlFile('unittests/descriptive-elements-with-text.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, 'Removed title element with only text child') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 1, @@ -263,7 +262,7 @@ class DoNotRemoveDescriptiveElementsWithOnlyText(unittest.TestCase): class RemoveEmptyDescriptiveElements(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/empty-descriptive-elements.svg') + doc = scourXmlFile('unittests/empty-descriptive-elements.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 0, 'Did not remove empty title element') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'desc')), 0, @@ -275,7 +274,7 @@ class RemoveEmptyDescriptiveElements(unittest.TestCase): class RemoveEmptyGElements(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/empty-g.svg') + doc = scourXmlFile('unittests/empty-g.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, 'Did not remove empty g element') @@ -283,7 +282,7 @@ class RemoveEmptyGElements(unittest.TestCase): class RemoveUnreferencedPattern(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-pattern.svg') + doc = scourXmlFile('unittests/unreferenced-pattern.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'pattern')), 0, 'Unreferenced pattern not removed') @@ -291,7 +290,7 @@ class RemoveUnreferencedPattern(unittest.TestCase): class RemoveUnreferencedLinearGradient(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-linearGradient.svg') + doc = scourXmlFile('unittests/unreferenced-linearGradient.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, 'Unreferenced linearGradient not removed') @@ -299,7 +298,7 @@ class RemoveUnreferencedLinearGradient(unittest.TestCase): class RemoveUnreferencedRadialGradient(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-radialGradient.svg') + doc = scourXmlFile('unittests/unreferenced-radialGradient.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialradient')), 0, 'Unreferenced radialGradient not removed') @@ -307,7 +306,7 @@ class RemoveUnreferencedRadialGradient(unittest.TestCase): class RemoveUnreferencedElementInDefs(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/referenced-elements-1.svg') + doc = scourXmlFile('unittests/referenced-elements-1.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'rect')), 1, 'Unreferenced rect left in defs') @@ -315,7 +314,7 @@ class RemoveUnreferencedElementInDefs(unittest.TestCase): class RemoveUnreferencedDefs(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-defs.svg') + doc = scourXmlFile('unittests/unreferenced-defs.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, 'Referenced linearGradient removed from defs') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 0, @@ -331,8 +330,8 @@ class RemoveUnreferencedDefs(unittest.TestCase): class KeepUnreferencedDefs(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/unreferenced-defs.svg', - scour.parse_args(['--keep-unreferenced-defs'])) + doc = scourXmlFile('unittests/unreferenced-defs.svg', + parse_args(['--keep-unreferenced-defs'])) self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, 'Referenced linearGradient removed from defs with `--keep-unreferenced-defs`') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')), 1, @@ -348,7 +347,7 @@ class KeepUnreferencedDefs(unittest.TestCase): class DoNotRemoveChainedRefsInDefs(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/refs-in-defs.svg') + doc = scourXmlFile('unittests/refs-in-defs.svg') g = doc.getElementsByTagNameNS(SVGNS, 'g')[0] self.assertEqual(g.childNodes.length >= 2, True, 'Chained references not honored in defs') @@ -357,7 +356,7 @@ class DoNotRemoveChainedRefsInDefs(unittest.TestCase): class KeepTitleInDefs(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/referenced-elements-1.svg') + doc = scourXmlFile('unittests/referenced-elements-1.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'title')), 1, 'Title removed from in defs') @@ -365,7 +364,7 @@ class KeepTitleInDefs(unittest.TestCase): class RemoveNestedDefs(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/nested-defs.svg') + doc = scourXmlFile('unittests/nested-defs.svg') allDefs = doc.getElementsByTagNameNS(SVGNS, 'defs') self.assertEqual(len(allDefs), 1, 'More than one defs left in doc') @@ -373,7 +372,7 @@ class RemoveNestedDefs(unittest.TestCase): class KeepUnreferencedIDsWhenEnabled(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/ids-to-strip.svg') + doc = scourXmlFile('unittests/ids-to-strip.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), 'boo', '<svg> ID stripped when it should be disabled') @@ -381,8 +380,8 @@ class KeepUnreferencedIDsWhenEnabled(unittest.TestCase): class RemoveUnreferencedIDsWhenEnabled(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/ids-to-strip.svg', - scour.parse_args(['--enable-id-stripping'])) + doc = scourXmlFile('unittests/ids-to-strip.svg', + parse_args(['--enable-id-stripping'])) self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'svg')[0].getAttribute('id'), '', '<svg> ID not stripped') @@ -390,7 +389,7 @@ class RemoveUnreferencedIDsWhenEnabled(unittest.TestCase): class RemoveUselessNestedGroups(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/nested-useless-groups.svg') + doc = scourXmlFile('unittests/nested-useless-groups.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 1, 'Useless nested groups not removed') @@ -398,8 +397,8 @@ class RemoveUselessNestedGroups(unittest.TestCase): class DoNotRemoveUselessNestedGroups(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/nested-useless-groups.svg', - scour.parse_args(['--disable-group-collapsing'])) + doc = scourXmlFile('unittests/nested-useless-groups.svg', + parse_args(['--disable-group-collapsing'])) self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, 'Useless nested groups were removed despite --disable-group-collapsing') @@ -407,7 +406,7 @@ class DoNotRemoveUselessNestedGroups(unittest.TestCase): class DoNotRemoveNestedGroupsWithTitle(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/groups-with-title-desc.svg') + doc = scourXmlFile('unittests/groups-with-title-desc.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, 'Nested groups with title was removed') @@ -415,7 +414,7 @@ class DoNotRemoveNestedGroupsWithTitle(unittest.TestCase): class DoNotRemoveNestedGroupsWithDesc(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/groups-with-title-desc.svg') + doc = scourXmlFile('unittests/groups-with-title-desc.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'g')), 2, 'Nested groups with desc was removed') @@ -423,7 +422,7 @@ class DoNotRemoveNestedGroupsWithDesc(unittest.TestCase): class RemoveDuplicateLinearGradientStops(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/duplicate-gradient-stops.svg') + doc = scourXmlFile('unittests/duplicate-gradient-stops.svg') grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, 'Duplicate linear gradient stops not removed') @@ -432,7 +431,7 @@ class RemoveDuplicateLinearGradientStops(unittest.TestCase): class RemoveDuplicateLinearGradientStopsPct(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/duplicate-gradient-stops-pct.svg') + doc = scourXmlFile('unittests/duplicate-gradient-stops-pct.svg') grad = doc.getElementsByTagNameNS(SVGNS, 'linearGradient') self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, 'Duplicate linear gradient stops with percentages not removed') @@ -441,7 +440,7 @@ class RemoveDuplicateLinearGradientStopsPct(unittest.TestCase): class RemoveDuplicateRadialGradientStops(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/duplicate-gradient-stops.svg') + doc = scourXmlFile('unittests/duplicate-gradient-stops.svg') grad = doc.getElementsByTagNameNS(SVGNS, 'radialGradient') self.assertEqual(len(grad[0].getElementsByTagNameNS(SVGNS, 'stop')), 3, 'Duplicate radial gradient stops not removed') @@ -450,7 +449,7 @@ class RemoveDuplicateRadialGradientStops(unittest.TestCase): class NoSodipodiNamespaceDecl(unittest.TestCase): def runTest(self): - attrs = scour.scourXmlFile('unittests/sodipodi.svg').documentElement.attributes + attrs = scourXmlFile('unittests/sodipodi.svg').documentElement.attributes for i in range(len(attrs)): self.assertNotEqual(attrs.item(i).nodeValue, 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', @@ -460,7 +459,7 @@ class NoSodipodiNamespaceDecl(unittest.TestCase): class NoInkscapeNamespaceDecl(unittest.TestCase): def runTest(self): - attrs = scour.scourXmlFile('unittests/inkscape.svg').documentElement.attributes + attrs = scourXmlFile('unittests/inkscape.svg').documentElement.attributes for i in range(len(attrs)): self.assertNotEqual(attrs.item(i).nodeValue, 'http://www.inkscape.org/namespaces/inkscape', @@ -478,7 +477,7 @@ class NoSodipodiAttributes(unittest.TestCase): if attrs.item(i).namespaceURI == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': return False return True - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/sodipodi.svg').documentElement, findSodipodiAttr), + self.assertNotEqual(walkTree(scourXmlFile('unittests/sodipodi.svg').documentElement, findSodipodiAttr), False, 'Found Sodipodi attributes') @@ -494,7 +493,7 @@ class NoInkscapeAttributes(unittest.TestCase): if attrs.item(i).namespaceURI == 'http://www.inkscape.org/namespaces/inkscape': return False return True - self.assertNotEqual(walkTree(scour.scourXmlFile('unittests/inkscape.svg').documentElement, findInkscapeAttr), + self.assertNotEqual(walkTree(scourXmlFile('unittests/inkscape.svg').documentElement, findInkscapeAttr), False, 'Found Inkscape attributes') @@ -504,7 +503,7 @@ class KeepInkscapeNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): def runTest(self): options = ScourOptions options.keep_editor_data = True - attrs = scour.scourXmlFile('unittests/inkscape.svg', options).documentElement.attributes + attrs = scourXmlFile('unittests/inkscape.svg', options).documentElement.attributes FoundNamespace = False for i in range(len(attrs)): if attrs.item(i).nodeValue == 'http://www.inkscape.org/namespaces/inkscape': @@ -520,7 +519,7 @@ class KeepSodipodiNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): def runTest(self): options = ScourOptions options.keep_editor_data = True - attrs = scour.scourXmlFile('unittests/sodipodi.svg', options).documentElement.attributes + attrs = scourXmlFile('unittests/sodipodi.svg', options).documentElement.attributes FoundNamespace = False for i in range(len(attrs)): if attrs.item(i).nodeValue == 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd': @@ -534,7 +533,7 @@ class KeepSodipodiNamespaceDeclarationsWhenKeepEditorData(unittest.TestCase): class KeepReferencedFonts(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/referenced-font.svg') + doc = scourXmlFile('unittests/referenced-font.svg') fonts = doc.documentElement.getElementsByTagNameNS(SVGNS, 'font') self.assertEqual(len(fonts), 1, 'Font wrongly removed from <defs>') @@ -543,7 +542,7 @@ class KeepReferencedFonts(unittest.TestCase): class ConvertStyleToAttrs(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + doc = scourXmlFile('unittests/stroke-transparent.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('style'), '', 'style attribute not emptied') @@ -551,7 +550,7 @@ class ConvertStyleToAttrs(unittest.TestCase): class RemoveStrokeWhenStrokeTransparent(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + doc = scourXmlFile('unittests/stroke-transparent.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', 'stroke attribute not emptied when stroke opacity zero') @@ -559,7 +558,7 @@ class RemoveStrokeWhenStrokeTransparent(unittest.TestCase): class RemoveStrokeWidthWhenStrokeTransparent(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + doc = scourXmlFile('unittests/stroke-transparent.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', 'stroke-width attribute not emptied when stroke opacity zero') @@ -567,7 +566,7 @@ class RemoveStrokeWidthWhenStrokeTransparent(unittest.TestCase): class RemoveStrokeLinecapWhenStrokeTransparent(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + doc = scourXmlFile('unittests/stroke-transparent.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', 'stroke-linecap attribute not emptied when stroke opacity zero') @@ -575,7 +574,7 @@ class RemoveStrokeLinecapWhenStrokeTransparent(unittest.TestCase): class RemoveStrokeLinejoinWhenStrokeTransparent(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + doc = scourXmlFile('unittests/stroke-transparent.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', 'stroke-linejoin attribute not emptied when stroke opacity zero') @@ -583,7 +582,7 @@ class RemoveStrokeLinejoinWhenStrokeTransparent(unittest.TestCase): class RemoveStrokeDasharrayWhenStrokeTransparent(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + doc = scourXmlFile('unittests/stroke-transparent.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', 'stroke-dasharray attribute not emptied when stroke opacity zero') @@ -591,7 +590,7 @@ class RemoveStrokeDasharrayWhenStrokeTransparent(unittest.TestCase): class RemoveStrokeDashoffsetWhenStrokeTransparent(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-transparent.svg') + doc = scourXmlFile('unittests/stroke-transparent.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', 'stroke-dashoffset attribute not emptied when stroke opacity zero') @@ -599,7 +598,7 @@ class RemoveStrokeDashoffsetWhenStrokeTransparent(unittest.TestCase): class RemoveStrokeWhenStrokeWidthZero(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + doc = scourXmlFile('unittests/stroke-nowidth.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', 'stroke attribute not emptied when width zero') @@ -607,7 +606,7 @@ class RemoveStrokeWhenStrokeWidthZero(unittest.TestCase): class RemoveStrokeOpacityWhenStrokeWidthZero(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + doc = scourXmlFile('unittests/stroke-nowidth.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', 'stroke-opacity attribute not emptied when width zero') @@ -615,7 +614,7 @@ class RemoveStrokeOpacityWhenStrokeWidthZero(unittest.TestCase): class RemoveStrokeLinecapWhenStrokeWidthZero(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + doc = scourXmlFile('unittests/stroke-nowidth.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', 'stroke-linecap attribute not emptied when width zero') @@ -623,7 +622,7 @@ class RemoveStrokeLinecapWhenStrokeWidthZero(unittest.TestCase): class RemoveStrokeLinejoinWhenStrokeWidthZero(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + doc = scourXmlFile('unittests/stroke-nowidth.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', 'stroke-linejoin attribute not emptied when width zero') @@ -631,7 +630,7 @@ class RemoveStrokeLinejoinWhenStrokeWidthZero(unittest.TestCase): class RemoveStrokeDasharrayWhenStrokeWidthZero(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + doc = scourXmlFile('unittests/stroke-nowidth.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', 'stroke-dasharray attribute not emptied when width zero') @@ -639,7 +638,7 @@ class RemoveStrokeDasharrayWhenStrokeWidthZero(unittest.TestCase): class RemoveStrokeDashoffsetWhenStrokeWidthZero(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-nowidth.svg') + doc = scourXmlFile('unittests/stroke-nowidth.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', 'stroke-dashoffset attribute not emptied when width zero') @@ -647,7 +646,7 @@ class RemoveStrokeDashoffsetWhenStrokeWidthZero(unittest.TestCase): class RemoveStrokeWhenStrokeNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke'), '', 'stroke attribute not emptied when no stroke') @@ -655,7 +654,7 @@ class RemoveStrokeWhenStrokeNone(unittest.TestCase): class KeepStrokeWhenInheritedFromParent(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementById('p1').getAttribute('stroke'), 'none', 'stroke attribute removed despite a different value being inherited from a parent') @@ -663,7 +662,7 @@ class KeepStrokeWhenInheritedFromParent(unittest.TestCase): class KeepStrokeWhenInheritedByChild(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementById('g2').getAttribute('stroke'), 'none', 'stroke attribute removed despite it being inherited by a child') @@ -671,7 +670,7 @@ class KeepStrokeWhenInheritedByChild(unittest.TestCase): class RemoveStrokeWidthWhenStrokeNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-width'), '', 'stroke-width attribute not emptied when no stroke') @@ -679,7 +678,7 @@ class RemoveStrokeWidthWhenStrokeNone(unittest.TestCase): class KeepStrokeWidthWhenInheritedByChild(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementById('g3').getAttribute('stroke-width'), '1px', 'stroke-width attribute removed despite it being inherited by a child') @@ -687,7 +686,7 @@ class KeepStrokeWidthWhenInheritedByChild(unittest.TestCase): class RemoveStrokeOpacityWhenStrokeNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-opacity'), '', 'stroke-opacity attribute not emptied when no stroke') @@ -695,7 +694,7 @@ class RemoveStrokeOpacityWhenStrokeNone(unittest.TestCase): class RemoveStrokeLinecapWhenStrokeNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linecap'), '', 'stroke-linecap attribute not emptied when no stroke') @@ -703,7 +702,7 @@ class RemoveStrokeLinecapWhenStrokeNone(unittest.TestCase): class RemoveStrokeLinejoinWhenStrokeNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-linejoin'), '', 'stroke-linejoin attribute not emptied when no stroke') @@ -711,7 +710,7 @@ class RemoveStrokeLinejoinWhenStrokeNone(unittest.TestCase): class RemoveStrokeDasharrayWhenStrokeNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dasharray'), '', 'stroke-dasharray attribute not emptied when no stroke') @@ -719,7 +718,7 @@ class RemoveStrokeDasharrayWhenStrokeNone(unittest.TestCase): class RemoveStrokeDashoffsetWhenStrokeNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/stroke-none.svg') + doc = scourXmlFile('unittests/stroke-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('stroke-dashoffset'), '', 'stroke-dashoffset attribute not emptied when no stroke') @@ -727,7 +726,7 @@ class RemoveStrokeDashoffsetWhenStrokeNone(unittest.TestCase): class RemoveFillRuleWhenFillNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg') + doc = scourXmlFile('unittests/fill-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-rule'), '', 'fill-rule attribute not emptied when no fill') @@ -735,7 +734,7 @@ class RemoveFillRuleWhenFillNone(unittest.TestCase): class RemoveFillOpacityWhenFillNone(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg') + doc = scourXmlFile('unittests/fill-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('fill-opacity'), '', 'fill-opacity attribute not emptied when no fill') @@ -743,8 +742,8 @@ class RemoveFillOpacityWhenFillNone(unittest.TestCase): class ConvertFillPropertyToAttr(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg', - scour.parse_args(['--disable-simplify-colors'])) + doc = scourXmlFile('unittests/fill-none.svg', + parse_args(['--disable-simplify-colors'])) self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill'), 'black', 'fill property not converted to XML attribute') @@ -752,7 +751,7 @@ class ConvertFillPropertyToAttr(unittest.TestCase): class ConvertFillOpacityPropertyToAttr(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg') + doc = scourXmlFile('unittests/fill-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-opacity'), '.5', 'fill-opacity property not converted to XML attribute') @@ -760,7 +759,7 @@ class ConvertFillOpacityPropertyToAttr(unittest.TestCase): class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/fill-none.svg') + doc = scourXmlFile('unittests/fill-none.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'path')[1].getAttribute('fill-rule'), 'evenodd', 'fill-rule property not converted to XML attribute') @@ -768,7 +767,7 @@ class ConvertFillRuleOpacityPropertyToAttr(unittest.TestCase): class CollapseSinglyReferencedGradients(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/collapse-gradients.svg') + doc = scourXmlFile('unittests/collapse-gradients.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, 'Singly-referenced linear gradient not collapsed') @@ -776,7 +775,7 @@ class CollapseSinglyReferencedGradients(unittest.TestCase): class InheritGradientUnitsUponCollapsing(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/collapse-gradients.svg') + doc = scourXmlFile('unittests/collapse-gradients.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), 'userSpaceOnUse', 'gradientUnits not properly inherited when collapsing gradients') @@ -785,7 +784,7 @@ class InheritGradientUnitsUponCollapsing(unittest.TestCase): class OverrideGradientUnitsUponCollapsing(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/collapse-gradients-gradientUnits.svg') + doc = scourXmlFile('unittests/collapse-gradients-gradientUnits.svg') self.assertEqual(doc.getElementsByTagNameNS(SVGNS, 'radialGradient')[0].getAttribute('gradientUnits'), '', 'gradientUnits not properly overrode when collapsing gradients') @@ -793,7 +792,7 @@ class OverrideGradientUnitsUponCollapsing(unittest.TestCase): class DoNotCollapseMultiplyReferencedGradients(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/dont-collapse-gradients.svg') + doc = scourXmlFile('unittests/dont-collapse-gradients.svg') self.assertNotEqual(len(doc.getElementsByTagNameNS(SVGNS, 'linearGradient')), 0, 'Multiply-referenced linear gradient collapsed') @@ -801,7 +800,7 @@ class DoNotCollapseMultiplyReferencedGradients(unittest.TestCase): class RemoveTrailingZerosFromPath(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg') + doc = scourXmlFile('unittests/path-truncate-zeros.svg') path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') self.assertEqual(path[:4] == 'm300' and path[4] != '.', True, 'Trailing zeros not removed from path data') @@ -810,7 +809,7 @@ class RemoveTrailingZerosFromPath(unittest.TestCase): class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-truncate-zeros-calc.svg') + doc = scourXmlFile('unittests/path-truncate-zeros-calc.svg') path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') self.assertEqual(path, 'm5.81 0h0.1', 'Trailing zeros not removed from path data after calculation') @@ -819,7 +818,7 @@ class RemoveTrailingZerosFromPathAfterCalculation(unittest.TestCase): class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-truncate-zeros.svg') + doc = scourXmlFile('unittests/path-truncate-zeros.svg') path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') self.assertEqual(path[4], '-', 'Delimiters not removed before negative coordinates in path data') @@ -828,7 +827,7 @@ class RemoveDelimiterBeforeNegativeCoordsInPath(unittest.TestCase): class UseScientificNotationToShortenCoordsInPath(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-use-scientific-notation.svg') + doc = scourXmlFile('unittests/path-use-scientific-notation.svg') path = doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d') self.assertEqual(path, 'm1e4 0', 'Not using scientific notation for path coord when representation is shorter') @@ -837,7 +836,7 @@ class UseScientificNotationToShortenCoordsInPath(unittest.TestCase): class ConvertAbsoluteToRelativePathCommands(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-abs-to-rel.svg') + doc = scourXmlFile('unittests/path-abs-to-rel.svg') path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) self.assertEqual(path[1][0], 'v', 'Absolute V command not converted to relative v command') @@ -848,7 +847,7 @@ class ConvertAbsoluteToRelativePathCommands(unittest.TestCase): class RoundPathData(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-precision.svg') + doc = scourXmlFile('unittests/path-precision.svg') path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) self.assertEqual(float(path[0][1][0]), 100.0, 'Not rounding down') @@ -859,7 +858,7 @@ class RoundPathData(unittest.TestCase): class LimitPrecisionInPathData(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-precision.svg') + doc = scourXmlFile('unittests/path-precision.svg') path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) self.assertEqual(float(path[1][1][0]), 100.01, 'Not correctly limiting precision on path data') @@ -868,7 +867,7 @@ class LimitPrecisionInPathData(unittest.TestCase): class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + doc = scourXmlFile('unittests/path-line-optimize.svg') path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) self.assertEqual(path[4][0], 'z', 'Did not remove an empty line segment from path') @@ -879,7 +878,7 @@ class RemoveEmptyLineSegmentsFromPath(unittest.TestCase): class DoNotRemoveEmptySegmentsFromPathWithRoundLineCaps(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-with-caps.svg') + doc = scourXmlFile('unittests/path-with-caps.svg') path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) self.assertEqual(len(path), 2, 'Did not preserve empty segments when path had round linecaps') @@ -888,7 +887,7 @@ class DoNotRemoveEmptySegmentsFromPathWithRoundLineCaps(unittest.TestCase): class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + doc = scourXmlFile('unittests/path-line-optimize.svg') path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) self.assertEqual(path[1][0], 'h', 'Did not change line to horizontal line segment in path') @@ -899,7 +898,7 @@ class ChangeLineToHorizontalLineSegmentInPath(unittest.TestCase): class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-line-optimize.svg') + doc = scourXmlFile('unittests/path-line-optimize.svg') path = svg_parser.parse(doc.getElementsByTagNameNS(SVGNS, 'path')[0].getAttribute('d')) self.assertEqual(path[2][0], 'v', 'Did not change line to vertical line segment in path') @@ -910,7 +909,7 @@ class ChangeLineToVerticalLineSegmentInPath(unittest.TestCase): class ChangeBezierToShorthandInPath(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-bez-optimize.svg') + doc = scourXmlFile('unittests/path-bez-optimize.svg') self.assertEqual(doc.getElementById('path1').getAttribute('d'), 'm10 100c50-50 50 50 100 0s50 50 100 0', 'Did not change bezier curves into shorthand curve segments in path') self.assertEqual(doc.getElementById('path2a').getAttribute('d'), 'm200 200s200 100 200 0', @@ -924,7 +923,7 @@ class ChangeBezierToShorthandInPath(unittest.TestCase): class ChangeQuadToShorthandInPath(unittest.TestCase): def runTest(self): - path = scour.scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + path = scourXmlFile('unittests/path-quad-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] self.assertEqual(path.getAttribute('d'), 'm10 100q50-50 100 0t100 0', 'Did not change quadratic curves into shorthand curve segments in path') @@ -932,7 +931,7 @@ class ChangeQuadToShorthandInPath(unittest.TestCase): class DoNotOptimzePathIfLarger(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + p = scourXmlFile('unittests/path-no-optimize.svg').getElementsByTagNameNS(SVGNS, 'path')[0] self.assertTrue(len(p.getAttribute('d')) <= # this was the scoured path data as of 2016-08-31 without the length check in cleanPath(): # d="m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234-0.00345-0.00345z" @@ -943,7 +942,7 @@ class DoNotOptimzePathIfLarger(unittest.TestCase): class HandleEncodingUTF8(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/encoding-utf8.svg') + doc = scourXmlFile('unittests/encoding-utf8.svg') text = u'Hello in many languages:\n' \ u'ar: أهلا\n' \ u'bn: হ্যালো\n' \ @@ -974,7 +973,7 @@ class HandleEncodingUTF8(unittest.TestCase): class HandleEncodingISO_8859_15(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/encoding-iso-8859-15.svg') + doc = scourXmlFile('unittests/encoding-iso-8859-15.svg') desc = six.text_type(doc.getElementsByTagNameNS(SVGNS, 'desc')[0].firstChild.wholeText).strip() self.assertEqual(desc, u'áèîäöüß€ŠšŽžŒœŸ', 'Did not handle ISO 8859-15 encoded characters') @@ -982,7 +981,7 @@ class HandleEncodingISO_8859_15(unittest.TestCase): class HandleSciNoInPathData(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-sn.svg') + doc = scourXmlFile('unittests/path-sn.svg') self.assertEqual(len(doc.getElementsByTagNameNS(SVGNS, 'path')), 1, 'Did not handle scientific notation in path data') @@ -990,7 +989,7 @@ class HandleSciNoInPathData(unittest.TestCase): class TranslateRGBIntoHex(unittest.TestCase): def runTest(self): - elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] self.assertEqual(elem.getAttribute('fill'), '#0f1011', 'Not converting rgb into hex') @@ -998,7 +997,7 @@ class TranslateRGBIntoHex(unittest.TestCase): class TranslateRGBPctIntoHex(unittest.TestCase): def runTest(self): - elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0] + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'stop')[0] self.assertEqual(elem.getAttribute('stop-color'), '#7f0000', 'Not converting rgb pct into hex') @@ -1006,7 +1005,7 @@ class TranslateRGBPctIntoHex(unittest.TestCase): class TranslateColorNamesIntoHex(unittest.TestCase): def runTest(self): - elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] self.assertEqual(elem.getAttribute('stroke'), '#a9a9a9', 'Not converting standard color names into hex') @@ -1014,7 +1013,7 @@ class TranslateColorNamesIntoHex(unittest.TestCase): class TranslateExtendedColorNamesIntoHex(unittest.TestCase): def runTest(self): - elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0] + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'solidColor')[0] self.assertEqual(elem.getAttribute('solid-color'), '#fafad2', 'Not converting extended color names into hex') @@ -1022,7 +1021,7 @@ class TranslateExtendedColorNamesIntoHex(unittest.TestCase): class TranslateLongHexColorIntoShortHex(unittest.TestCase): def runTest(self): - elem = scour.scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0] + elem = scourXmlFile('unittests/color-formats.svg').getElementsByTagNameNS(SVGNS, 'ellipse')[0] self.assertEqual(elem.getAttribute('fill'), '#fff', 'Not converting long hex color into short hex') @@ -1030,7 +1029,7 @@ class TranslateLongHexColorIntoShortHex(unittest.TestCase): class DoNotConvertShortColorNames(unittest.TestCase): def runTest(self): - elem = scour.scourXmlFile('unittests/dont-convert-short-color-names.svg') \ + elem = scourXmlFile('unittests/dont-convert-short-color-names.svg') \ .getElementsByTagNameNS(SVGNS, 'rect')[0] self.assertEqual('red', elem.getAttribute('fill'), 'Converted short color name to longer hex string') @@ -1039,7 +1038,7 @@ class DoNotConvertShortColorNames(unittest.TestCase): class AllowQuotEntitiesInUrl(unittest.TestCase): def runTest(self): - grads = scour.scourXmlFile('unittests/quot-in-url.svg').getElementsByTagNameNS(SVGNS, 'linearGradient') + grads = scourXmlFile('unittests/quot-in-url.svg').getElementsByTagNameNS(SVGNS, 'linearGradient') self.assertEqual(len(grads), 1, 'Removed referenced gradient when " was in the url') @@ -1047,7 +1046,7 @@ class AllowQuotEntitiesInUrl(unittest.TestCase): class RemoveFontStylesFromNonTextShapes(unittest.TestCase): def runTest(self): - r = scour.scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] + r = scourXmlFile('unittests/font-styles.svg').getElementsByTagNameNS(SVGNS, 'rect')[0] self.assertEqual(r.getAttribute('font-size'), '', 'font-size not removed from rect') @@ -1055,7 +1054,7 @@ class RemoveFontStylesFromNonTextShapes(unittest.TestCase): class CollapseConsecutiveHLinesSegments(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + p = scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[0] self.assertEqual(p.getAttribute('d'), 'm100 100h200v100h-200z', 'Did not collapse consecutive hlines segments') @@ -1063,7 +1062,7 @@ class CollapseConsecutiveHLinesSegments(unittest.TestCase): class CollapseConsecutiveHLinesCoords(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[1] + p = scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[1] self.assertEqual(p.getAttribute('d'), 'm100 300h200v100h-200z', 'Did not collapse consecutive hlines coordinates') @@ -1071,7 +1070,7 @@ class CollapseConsecutiveHLinesCoords(unittest.TestCase): class DoNotCollapseConsecutiveHLinesSegsWithDifferingSigns(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[2] + p = scourXmlFile('unittests/consecutive-hlines.svg').getElementsByTagNameNS(SVGNS, 'path')[2] self.assertEqual(p.getAttribute('d'), 'm100 500h300-100v100h-200z', 'Collapsed consecutive hlines segments with differing signs') @@ -1079,7 +1078,7 @@ class DoNotCollapseConsecutiveHLinesSegsWithDifferingSigns(unittest.TestCase): class ConvertStraightCurvesToLines(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + p = scourXmlFile('unittests/straight-curve.svg').getElementsByTagNameNS(SVGNS, 'path')[0] self.assertEqual(p.getAttribute('d'), 'm10 10l40 40 40-40z', 'Did not convert straight curves into lines') @@ -1087,7 +1086,7 @@ class ConvertStraightCurvesToLines(unittest.TestCase): class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + p = scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] self.assertEqual(p.getAttribute('points'), '50 50 150 50 150 150 50 150', 'Unnecessary polygon end point not removed') @@ -1095,7 +1094,7 @@ class RemoveUnnecessaryPolygonEndPoint(unittest.TestCase): class DoNotRemovePolgonLastPoint(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1] + p = scourXmlFile('unittests/polygon.svg').getElementsByTagNameNS(SVGNS, 'polygon')[1] self.assertEqual(p.getAttribute('points'), '200 50 300 50 300 150 200 150', 'Last point of polygon removed') @@ -1103,7 +1102,7 @@ class DoNotRemovePolgonLastPoint(unittest.TestCase): class ScourPolygonCoordsSciNo(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + p = scourXmlFile('unittests/polygon-coord.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] self.assertEqual(p.getAttribute('points'), '1e4 50', 'Polygon coordinates not scoured') @@ -1111,7 +1110,7 @@ class ScourPolygonCoordsSciNo(unittest.TestCase): class ScourPolylineCoordsSciNo(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + p = scourXmlFile('unittests/polyline-coord.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] self.assertEqual(p.getAttribute('points'), '1e4 50', 'Polyline coordinates not scoured') @@ -1119,7 +1118,7 @@ class ScourPolylineCoordsSciNo(unittest.TestCase): class ScourPolygonNegativeCoords(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + p = scourXmlFile('unittests/polygon-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] # points="100,-100,100-100,100-100-100,-100-100,200" /> self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', 'Negative polygon coordinates not properly parsed') @@ -1128,7 +1127,7 @@ class ScourPolygonNegativeCoords(unittest.TestCase): class ScourPolylineNegativeCoords(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + p = scourXmlFile('unittests/polyline-coord-neg.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] self.assertEqual(p.getAttribute('points'), '100 -100 100 -100 100 -100 -100 -100 -100 200', 'Negative polyline coordinates not properly parsed') @@ -1136,7 +1135,7 @@ class ScourPolylineNegativeCoords(unittest.TestCase): class ScourPolygonNegativeCoordFirst(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/polygon-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] + p = scourXmlFile('unittests/polygon-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polygon')[0] # points="-100,-100,100-100,100-100-100,-100-100,200" /> self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', 'Negative polygon coordinates not properly parsed') @@ -1145,7 +1144,7 @@ class ScourPolygonNegativeCoordFirst(unittest.TestCase): class ScourPolylineNegativeCoordFirst(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/polyline-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] + p = scourXmlFile('unittests/polyline-coord-neg-first.svg').getElementsByTagNameNS(SVGNS, 'polyline')[0] self.assertEqual(p.getAttribute('points'), '-100 -100 100 -100 100 -100 -100 -100 -100 200', 'Negative polyline coordinates not properly parsed') @@ -1153,7 +1152,7 @@ class ScourPolylineNegativeCoordFirst(unittest.TestCase): class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase): def runTest(self): - f = scour.scourXmlFile('unittests/important-groups-in-defs.svg') + f = scourXmlFile('unittests/important-groups-in-defs.svg') self.assertEqual(len(f.getElementsByTagNameNS(SVGNS, 'linearGradient')), 1, 'Group in defs with id\'ed element removed') @@ -1161,7 +1160,7 @@ class DoNotRemoveGroupsWithIDsInDefs(unittest.TestCase): class AlwaysKeepClosePathSegments(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + p = scourXmlFile('unittests/path-with-closepath.svg').getElementsByTagNameNS(SVGNS, 'path')[0] self.assertEqual(p.getAttribute('d'), 'm10 10h100v100h-100z', 'Path with closepath not preserved') @@ -1169,7 +1168,7 @@ class AlwaysKeepClosePathSegments(unittest.TestCase): class RemoveDuplicateLinearGradients(unittest.TestCase): def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') lingrads = svgdoc.getElementsByTagNameNS(SVGNS, 'linearGradient') self.assertEqual(1, lingrads.length, 'Duplicate linear gradient not removed') @@ -1178,7 +1177,7 @@ class RemoveDuplicateLinearGradients(unittest.TestCase): class RereferenceForLinearGradient(unittest.TestCase): def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') self.assertEqual(rects[0].getAttribute('fill'), rects[1].getAttribute('stroke'), 'Reference not updated after removing duplicate linear gradient') @@ -1189,7 +1188,7 @@ class RereferenceForLinearGradient(unittest.TestCase): class RemoveDuplicateRadialGradients(unittest.TestCase): def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') radgrads = svgdoc.getElementsByTagNameNS(SVGNS, 'radialGradient') self.assertEqual(1, radgrads.length, 'Duplicate radial gradient not removed') @@ -1198,7 +1197,7 @@ class RemoveDuplicateRadialGradients(unittest.TestCase): class RereferenceForRadialGradient(unittest.TestCase): def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') self.assertEqual(rects[2].getAttribute('stroke'), rects[3].getAttribute('fill'), 'Reference not updated after removing duplicate radial gradient') @@ -1207,7 +1206,7 @@ class RereferenceForRadialGradient(unittest.TestCase): class RereferenceForGradientWithFallback(unittest.TestCase): def runTest(self): - svgdoc = scour.scourXmlFile('unittests/remove-duplicate-gradients.svg') + svgdoc = scourXmlFile('unittests/remove-duplicate-gradients.svg') rects = svgdoc.getElementsByTagNameNS(SVGNS, 'rect') self.assertEqual(rects[0].getAttribute('fill') + ' #fff', rects[5].getAttribute('fill'), 'Reference (with fallback) not updated after removing duplicate linear gradient') @@ -1216,7 +1215,7 @@ class RereferenceForGradientWithFallback(unittest.TestCase): class CollapseSamePathPoints(unittest.TestCase): def runTest(self): - p = scour.scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + p = scourXmlFile('unittests/collapse-same-path-points.svg').getElementsByTagNameNS(SVGNS, 'path')[0] self.assertEqual(p.getAttribute('d'), "m100 100l100.12 100.12c14.877 4.8766-15.123-5.1234 0 0z", 'Did not collapse same path points') @@ -1224,7 +1223,7 @@ class CollapseSamePathPoints(unittest.TestCase): class ScourUnitlessLengths(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/scour-lengths.svg') + doc = scourXmlFile('unittests/scour-lengths.svg') r = doc.getElementsByTagNameNS(SVGNS, 'rect')[0] svg = doc.documentElement self.assertEqual(svg.getAttribute('x'), '1', @@ -1242,7 +1241,7 @@ class ScourUnitlessLengths(unittest.TestCase): class ScourLengthsWithUnits(unittest.TestCase): def runTest(self): - r = scour.scourXmlFile('unittests/scour-lengths.svg').getElementsByTagNameNS(SVGNS, 'rect')[1] + r = scourXmlFile('unittests/scour-lengths.svg').getElementsByTagNameNS(SVGNS, 'rect')[1] self.assertEqual(r.getAttribute('x'), '123.46px', 'Did not scour x attribute with unit') self.assertEqual(r.getAttribute('y'), '35ex', @@ -1256,7 +1255,7 @@ class ScourLengthsWithUnits(unittest.TestCase): class RemoveRedundantSvgNamespaceDeclaration(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement + doc = scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement self.assertNotEqual(doc.getAttribute('xmlns:svg'), 'http://www.w3.org/2000/svg', 'Redundant svg namespace declaration not removed') @@ -1264,7 +1263,7 @@ class RemoveRedundantSvgNamespaceDeclaration(unittest.TestCase): class RemoveRedundantSvgNamespacePrefix(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement + doc = scourXmlFile('unittests/redundant-svg-namespace.svg').documentElement r = doc.getElementsByTagNameNS(SVGNS, 'rect')[1] self.assertEqual(r.tagName, 'rect', 'Redundant svg: prefix not removed') @@ -1273,7 +1272,7 @@ class RemoveRedundantSvgNamespacePrefix(unittest.TestCase): class RemoveDefaultGradX1Value(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') self.assertEqual(g.getAttribute('x1'), '', 'x1="0" not removed') @@ -1281,7 +1280,7 @@ class RemoveDefaultGradX1Value(unittest.TestCase): class RemoveDefaultGradY1Value(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') self.assertEqual(g.getAttribute('y1'), '', 'y1="0" not removed') @@ -1289,7 +1288,7 @@ class RemoveDefaultGradY1Value(unittest.TestCase): class RemoveDefaultGradX2Value(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/gradient-default-attrs.svg') + doc = scourXmlFile('unittests/gradient-default-attrs.svg') self.assertEqual(doc.getElementById('grad1').getAttribute('x2'), '', 'x2="100%" not removed') self.assertEqual(doc.getElementById('grad1b').getAttribute('x2'), '', @@ -1303,7 +1302,7 @@ class RemoveDefaultGradX2Value(unittest.TestCase): class RemoveDefaultGradY2Value(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') self.assertEqual(g.getAttribute('y2'), '', 'y2="0" not removed') @@ -1311,7 +1310,7 @@ class RemoveDefaultGradY2Value(unittest.TestCase): class RemoveDefaultGradGradientUnitsValue(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') self.assertEqual(g.getAttribute('gradientUnits'), '', 'gradientUnits="objectBoundingBox" not removed') @@ -1319,7 +1318,7 @@ class RemoveDefaultGradGradientUnitsValue(unittest.TestCase): class RemoveDefaultGradSpreadMethodValue(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad1') self.assertEqual(g.getAttribute('spreadMethod'), '', 'spreadMethod="pad" not removed') @@ -1327,7 +1326,7 @@ class RemoveDefaultGradSpreadMethodValue(unittest.TestCase): class RemoveDefaultGradCXValue(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') self.assertEqual(g.getAttribute('cx'), '', 'cx="50%" not removed') @@ -1335,7 +1334,7 @@ class RemoveDefaultGradCXValue(unittest.TestCase): class RemoveDefaultGradCYValue(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') self.assertEqual(g.getAttribute('cy'), '', 'cy="50%" not removed') @@ -1343,7 +1342,7 @@ class RemoveDefaultGradCYValue(unittest.TestCase): class RemoveDefaultGradRValue(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') self.assertEqual(g.getAttribute('r'), '', 'r="50%" not removed') @@ -1351,7 +1350,7 @@ class RemoveDefaultGradRValue(unittest.TestCase): class RemoveDefaultGradFXValue(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') self.assertEqual(g.getAttribute('fx'), '', 'fx matching cx not removed') @@ -1359,7 +1358,7 @@ class RemoveDefaultGradFXValue(unittest.TestCase): class RemoveDefaultGradFYValue(unittest.TestCase): def runTest(self): - g = scour.scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') + g = scourXmlFile('unittests/gradient-default-attrs.svg').getElementById('grad2') self.assertEqual(g.getAttribute('fy'), '', 'fy matching cy not removed') @@ -1368,7 +1367,7 @@ class CDATAInXml(unittest.TestCase): def runTest(self): with open('unittests/cdata.svg') as f: - lines = scour.scourString(f.read()).splitlines() + lines = scourString(f.read()).splitlines() self.assertEqual(lines[3], " alert('pb&j');", 'CDATA did not come out correctly') @@ -1378,7 +1377,7 @@ class WellFormedXMLLesserThanInAttrValue(unittest.TestCase): def runTest(self): with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) + wellformed = scourString(f.read()) self.assertTrue(wellformed.find('unicode="<"') != -1, "Improperly serialized < in attribute value") @@ -1387,7 +1386,7 @@ class WellFormedXMLAmpersandInAttrValue(unittest.TestCase): def runTest(self): with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) + wellformed = scourString(f.read()) self.assertTrue(wellformed.find('unicode="&"') != -1, 'Improperly serialized & in attribute value') @@ -1396,7 +1395,7 @@ class WellFormedXMLLesserThanInTextContent(unittest.TestCase): def runTest(self): with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) + wellformed = scourString(f.read()) self.assertTrue(wellformed.find('<title>2 < 5') != -1, 'Improperly serialized < in text content') @@ -1405,7 +1404,7 @@ class WellFormedXMLAmpersandInTextContent(unittest.TestCase): def runTest(self): with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) + wellformed = scourString(f.read()) self.assertTrue(wellformed.find('Peanut Butter & Jelly') != -1, 'Improperly serialized & in text content') @@ -1414,7 +1413,7 @@ class WellFormedXMLNamespacePrefixRemoveUnused(unittest.TestCase): def runTest(self): with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) + wellformed = scourString(f.read()) self.assertTrue(wellformed.find('xmlns:foo=') == -1, 'Improperly serialized namespace prefix declarations: Unused namespace decaration not removed') @@ -1423,7 +1422,7 @@ class WellFormedXMLNamespacePrefixKeepUsedElementPrefix(unittest.TestCase): def runTest(self): with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) + wellformed = scourString(f.read()) self.assertTrue(wellformed.find('xmlns:bar=') != -1, 'Improperly serialized namespace prefix declarations: Used element prefix removed') @@ -1432,7 +1431,7 @@ class WellFormedXMLNamespacePrefixKeepUsedAttributePrefix(unittest.TestCase): def runTest(self): with open('unittests/xml-well-formed.svg') as f: - wellformed = scour.scourString(f.read()) + wellformed = scourString(f.read()) self.assertTrue(wellformed.find('xmlns:baz=') != -1, 'Improperly serialized namespace prefix declarations: Used attribute prefix removed') @@ -1441,7 +1440,7 @@ class NamespaceDeclPrefixesInXMLWhenNotInDefaultNamespace(unittest.TestCase): def runTest(self): with open('unittests/xml-ns-decl.svg') as f: - xmlstring = scour.scourString(f.read()) + xmlstring = scourString(f.read()) self.assertTrue(xmlstring.find('xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"') != -1, 'Improperly serialized namespace prefix declarations when not in default namespace') @@ -1450,7 +1449,7 @@ class MoveSVGElementsToDefaultNamespace(unittest.TestCase): def runTest(self): with open('unittests/xml-ns-decl.svg') as f: - xmlstring = scour.scourString(f.read()) + xmlstring = scourString(f.read()) self.assertTrue(xmlstring.find(' This is some messed-up markup @@ -1546,7 +1545,7 @@ class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): def runTest(self): with open('unittests/whitespace-nested.svg') as f: - s = scour.scourString(f.read()).splitlines() + s = scourString(f.read()).splitlines() c = ''' Use bold text @@ -1560,7 +1559,7 @@ class DoNotPrettyPrintWhenNestedWhitespacePreserved(unittest.TestCase): class GetAttrPrefixRight(unittest.TestCase): def runTest(self): - grad = scour.scourXmlFile('unittests/xml-namespace-attrs.svg') \ + grad = scourXmlFile('unittests/xml-namespace-attrs.svg') \ .getElementsByTagNameNS(SVGNS, 'linearGradient')[1] self.assertEqual(grad.getAttributeNS('http://www.w3.org/1999/xlink', 'href'), '#linearGradient841', 'Did not get xlink:href prefix right') @@ -1570,7 +1569,7 @@ class EnsurePreserveWhitespaceOnNonTextElements(unittest.TestCase): def runTest(self): with open('unittests/no-collapse-lines.svg') as f: - s = scour.scourString(f.read()) + s = scourString(f.read()) self.assertEqual(len(s.splitlines()), 6, 'Did not properly preserve whitespace on elements even if they were not textual') @@ -1579,7 +1578,7 @@ class HandleEmptyStyleElement(unittest.TestCase): def runTest(self): try: - styles = scour.scourXmlFile('unittests/empty-style.svg').getElementsByTagNameNS(SVGNS, 'style') + styles = scourXmlFile('unittests/empty-style.svg').getElementsByTagNameNS(SVGNS, 'style') fail = len(styles) != 1 except AttributeError: fail = True @@ -1591,7 +1590,7 @@ class EnsureLineEndings(unittest.TestCase): def runTest(self): with open('unittests/whitespace-important.svg') as f: - s = scour.scourString(f.read()) + s = scourString(f.read()) self.assertEqual(len(s.splitlines()), 4, 'Did not output line ending character correctly') @@ -1599,14 +1598,14 @@ class EnsureLineEndings(unittest.TestCase): class XmlEntities(unittest.TestCase): def runTest(self): - self.assertEqual(scour.makeWellFormed('<>&'), '<>&', + self.assertEqual(makeWellFormed('<>&'), '<>&', 'Incorrectly translated XML entities') class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/comments.svg') + doc = scourXmlFile('unittests/comments.svg') self.assertEqual(doc.childNodes.length, 4, 'Did not include all comment children outside of root') self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') @@ -1617,7 +1616,7 @@ class DoNotStripCommentsOutsideOfRoot(unittest.TestCase): class DoNotStripDoctype(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/doctype.svg') + doc = scourXmlFile('unittests/doctype.svg') self.assertEqual(doc.childNodes.length, 3, 'Did not include the DOCROOT') self.assertEqual(doc.childNodes[0].nodeType, 8, 'First node not a comment') @@ -1628,7 +1627,7 @@ class DoNotStripDoctype(unittest.TestCase): class PathImplicitLineWithMoveCommands(unittest.TestCase): def runTest(self): - path = scour.scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0] + path = scourXmlFile('unittests/path-implicit-line.svg').getElementsByTagNameNS(SVGNS, 'path')[0] self.assertEqual(path.getAttribute('d'), "m100 100v100m200-100h-200m200 100v-100", "Implicit line segments after move not preserved") @@ -1636,8 +1635,8 @@ class PathImplicitLineWithMoveCommands(unittest.TestCase): class RemoveTitlesOption(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', - scour.parse_args(['--remove-titles'])) + doc = scourXmlFile('unittests/full-descriptive-elements.svg', + parse_args(['--remove-titles'])) self.assertEqual(doc.childNodes.length, 1, 'Did not remove tag with --remove-titles') @@ -1645,8 +1644,8 @@ class RemoveTitlesOption(unittest.TestCase): class RemoveDescriptionsOption(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', - scour.parse_args(['--remove-descriptions'])) + doc = scourXmlFile('unittests/full-descriptive-elements.svg', + parse_args(['--remove-descriptions'])) self.assertEqual(doc.childNodes.length, 1, 'Did not remove tag with --remove-descriptions') @@ -1654,8 +1653,8 @@ class RemoveDescriptionsOption(unittest.TestCase): class RemoveMetadataOption(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', - scour.parse_args(['--remove-metadata'])) + doc = scourXmlFile('unittests/full-descriptive-elements.svg', + parse_args(['--remove-metadata'])) self.assertEqual(doc.childNodes.length, 1, 'Did not remove tag with --remove-metadata') @@ -1663,8 +1662,8 @@ class RemoveMetadataOption(unittest.TestCase): class RemoveDescriptiveElementsOption(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/full-descriptive-elements.svg', - scour.parse_args(['--remove-descriptive-elements'])) + doc = scourXmlFile('unittests/full-descriptive-elements.svg', + parse_args(['--remove-descriptive-elements'])) self.assertEqual(doc.childNodes.length, 1, 'Did not remove , <desc> and <metadata> tags with --remove-descriptive-elements') @@ -1674,8 +1673,8 @@ class EnableCommentStrippingOption(unittest.TestCase): def runTest(self): with open('unittests/comment-beside-xml-decl.svg') as f: docStr = f.read() - docStr = scour.scourString(docStr, - scour.parse_args(['--enable-comment-stripping'])) + docStr = scourString(docStr, + parse_args(['--enable-comment-stripping'])) self.assertEqual(docStr.find('<!--'), -1, 'Did not remove document-level comment with --enable-comment-stripping') @@ -1685,8 +1684,8 @@ class StripXmlPrologOption(unittest.TestCase): def runTest(self): with open('unittests/comment-beside-xml-decl.svg') as f: docStr = f.read() - docStr = scour.scourString(docStr, - scour.parse_args(['--strip-xml-prolog'])) + docStr = scourString(docStr, + parse_args(['--strip-xml-prolog'])) self.assertEqual(docStr.find('<?xml'), -1, 'Did not remove <?xml?> with --strip-xml-prolog') @@ -1694,8 +1693,8 @@ class StripXmlPrologOption(unittest.TestCase): class ShortenIDsOption(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/shorten-ids.svg', - scour.parse_args(['--shorten-ids'])) + doc = scourXmlFile('unittests/shorten-ids.svg', + parse_args(['--shorten-ids'])) gradientTag = doc.getElementsByTagName('linearGradient')[0] self.assertEqual(gradientTag.getAttribute('id'), 'a', "Did not shorten a linear gradient's ID with --shorten-ids") @@ -1707,7 +1706,7 @@ class ShortenIDsOption(unittest.TestCase): class MustKeepGInSwitch(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/groups-in-switch.svg') + doc = scourXmlFile('unittests/groups-in-switch.svg') self.assertEqual(doc.getElementsByTagName('g').length, 1, 'Erroneously removed a <g> in a <switch>') @@ -1715,8 +1714,8 @@ class MustKeepGInSwitch(unittest.TestCase): class MustKeepGInSwitch2(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/groups-in-switch-with-id.svg', - scour.parse_args(['--enable-id-stripping'])) + doc = scourXmlFile('unittests/groups-in-switch-with-id.svg', + parse_args(['--enable-id-stripping'])) self.assertEqual(doc.getElementsByTagName('g').length, 1, 'Erroneously removed a <g> in a <switch>') @@ -1724,8 +1723,8 @@ class MustKeepGInSwitch2(unittest.TestCase): class GroupCreation(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/group-creation.svg', - scour.parse_args(['--create-groups'])) + doc = scourXmlFile('unittests/group-creation.svg', + parse_args(['--create-groups'])) self.assertEqual(doc.getElementsByTagName('g').length, 1, 'Did not create a <g> for a run of elements having similar attributes') @@ -1733,8 +1732,8 @@ class GroupCreation(unittest.TestCase): class GroupCreationForInheritableAttributesOnly(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/group-creation.svg', - scour.parse_args(['--create-groups'])) + doc = scourXmlFile('unittests/group-creation.svg', + parse_args(['--create-groups'])) self.assertEqual(doc.getElementsByTagName('g').item(0).getAttribute('y'), '', 'Promoted the uninheritable attribute y to a <g>') @@ -1742,8 +1741,8 @@ class GroupCreationForInheritableAttributesOnly(unittest.TestCase): class GroupNoCreation(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/group-no-creation.svg', - scour.parse_args(['--create-groups'])) + doc = scourXmlFile('unittests/group-no-creation.svg', + parse_args(['--create-groups'])) self.assertEqual(doc.getElementsByTagName('g').length, 0, 'Created a <g> for a run of elements having dissimilar attributes') @@ -1751,8 +1750,8 @@ class GroupNoCreation(unittest.TestCase): class GroupNoCreationForTspan(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/group-no-creation-tspan.svg', - scour.parse_args(['--create-groups'])) + doc = scourXmlFile('unittests/group-no-creation-tspan.svg', + parse_args(['--create-groups'])) self.assertEqual(doc.getElementsByTagName('g').length, 0, 'Created a <g> for a run of <tspan>s ' 'that are not allowed as children according to content model') @@ -1761,7 +1760,7 @@ class GroupNoCreationForTspan(unittest.TestCase): class DoNotCommonizeAttributesOnReferencedElements(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/commonized-referenced-elements.svg') + doc = scourXmlFile('unittests/commonized-referenced-elements.svg') self.assertEqual(doc.getElementsByTagName('circle')[0].getAttribute('fill'), '#0f0', 'Grouped an element referenced elsewhere into a <g>') @@ -1769,7 +1768,7 @@ class DoNotCommonizeAttributesOnReferencedElements(unittest.TestCase): class DoNotRemoveOverflowVisibleOnMarker(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/overflow-marker.svg') + doc = scourXmlFile('unittests/overflow-marker.svg') self.assertEqual(doc.getElementById('m1').getAttribute('overflow'), 'visible', 'Removed the overflow attribute when it was not using the default value') self.assertEqual(doc.getElementById('m2').getAttribute('overflow'), '', @@ -1779,7 +1778,7 @@ class DoNotRemoveOverflowVisibleOnMarker(unittest.TestCase): class DoNotRemoveOrientAutoOnMarker(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/orient-marker.svg') + doc = scourXmlFile('unittests/orient-marker.svg') self.assertEqual(doc.getElementById('m1').getAttribute('orient'), 'auto', 'Removed the orient attribute when it was not using the default value') self.assertEqual(doc.getElementById('m2').getAttribute('orient'), '', @@ -1789,7 +1788,7 @@ class DoNotRemoveOrientAutoOnMarker(unittest.TestCase): class MarkerOnSvgElements(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/overflow-svg.svg') + doc = scourXmlFile('unittests/overflow-svg.svg') self.assertEqual(doc.getElementsByTagName('svg')[0].getAttribute('overflow'), '', 'Did not remove the overflow attribute when it was using the default value') self.assertEqual(doc.getElementsByTagName('svg')[1].getAttribute('overflow'), '', @@ -1801,7 +1800,7 @@ class MarkerOnSvgElements(unittest.TestCase): class GradientReferencedByStyleCDATA(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/style-cdata.svg') + doc = scourXmlFile('unittests/style-cdata.svg') self.assertEqual(len(doc.getElementsByTagName('linearGradient')), 1, 'Removed a gradient referenced by an internal stylesheet') @@ -1811,8 +1810,8 @@ class ShortenIDsInStyleCDATA(unittest.TestCase): def runTest(self): with open('unittests/style-cdata.svg') as f: docStr = f.read() - docStr = scour.scourString(docStr, - scour.parse_args(['--shorten-ids'])) + docStr = scourString(docStr, + parse_args(['--shorten-ids'])) self.assertEqual(docStr.find('somethingreallylong'), -1, 'Did not shorten IDs in the internal stylesheet') @@ -1820,7 +1819,7 @@ class ShortenIDsInStyleCDATA(unittest.TestCase): class StyleToAttr(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/style-to-attr.svg') + doc = scourXmlFile('unittests/style-to-attr.svg') line = doc.getElementsByTagName('line')[0] self.assertEqual(line.getAttribute('stroke'), '#000') self.assertEqual(line.getAttribute('marker-start'), 'url(#m)') @@ -1831,7 +1830,7 @@ class StyleToAttr(unittest.TestCase): class PathEmptyMove(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/path-empty-move.svg') + doc = scourXmlFile('unittests/path-empty-move.svg') self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('d'), 'm100 100l200 100z') self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('d'), 'm100 100v200l100 100z') @@ -1839,7 +1838,7 @@ class PathEmptyMove(unittest.TestCase): class DefaultsRemovalToplevel(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[1].getAttribute('fill-rule'), '', 'Default attribute fill-rule:nonzero not removed') @@ -1847,7 +1846,7 @@ class DefaultsRemovalToplevel(unittest.TestCase): class DefaultsRemovalToplevelInverse(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('fill-rule'), 'evenodd', 'Non-Default attribute fill-rule:evenodd removed') @@ -1855,7 +1854,7 @@ class DefaultsRemovalToplevelInverse(unittest.TestCase): class DefaultsRemovalToplevelFormat(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[0].getAttribute('stroke-width'), '', 'Default attribute stroke-width:1.00 not removed') @@ -1863,7 +1862,7 @@ class DefaultsRemovalToplevelFormat(unittest.TestCase): class DefaultsRemovalInherited(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[3].getAttribute('fill-rule'), '', 'Default attribute fill-rule:nonzero not removed in child') @@ -1871,7 +1870,7 @@ class DefaultsRemovalInherited(unittest.TestCase): class DefaultsRemovalInheritedInverse(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('fill-rule'), 'evenodd', 'Non-Default attribute fill-rule:evenodd removed in child') @@ -1879,7 +1878,7 @@ class DefaultsRemovalInheritedInverse(unittest.TestCase): class DefaultsRemovalInheritedFormat(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[2].getAttribute('stroke-width'), '', 'Default attribute stroke-width:1.00 not removed in child') @@ -1887,7 +1886,7 @@ class DefaultsRemovalInheritedFormat(unittest.TestCase): class DefaultsRemovalOverwrite(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[5].getAttribute('fill-rule'), 'nonzero', 'Default attribute removed, although it overwrites parent element') @@ -1895,7 +1894,7 @@ class DefaultsRemovalOverwrite(unittest.TestCase): class DefaultsRemovalOverwriteMarker(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[4].getAttribute('marker-start'), 'none', 'Default marker attribute removed, although it overwrites parent element') @@ -1903,7 +1902,7 @@ class DefaultsRemovalOverwriteMarker(unittest.TestCase): class DefaultsRemovalNonOverwrite(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/cascading-default-attribute-removal.svg') + doc = scourXmlFile('unittests/cascading-default-attribute-removal.svg') self.assertEqual(doc.getElementsByTagName('path')[10].getAttribute('fill-rule'), '', 'Default attribute not removed, although its parent used default') @@ -1911,7 +1910,7 @@ class DefaultsRemovalNonOverwrite(unittest.TestCase): class RemoveDefsWithUnreferencedElements(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/useless-defs.svg') + doc = scourXmlFile('unittests/useless-defs.svg') self.assertEqual(doc.getElementsByTagName('defs').length, 0, 'Kept defs, although it contains only unreferenced elements') @@ -1919,7 +1918,7 @@ class RemoveDefsWithUnreferencedElements(unittest.TestCase): class RemoveDefsWithWhitespace(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/whitespace-defs.svg') + doc = scourXmlFile('unittests/whitespace-defs.svg') self.assertEqual(doc.getElementsByTagName('defs').length, 0, 'Kept defs, although it contains only whitespace or is <defs/>') @@ -1927,7 +1926,7 @@ class RemoveDefsWithWhitespace(unittest.TestCase): class TransformIdentityMatrix(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-identity.svg') + doc = scourXmlFile('unittests/transform-matrix-is-identity.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', 'Transform containing identity matrix not removed') @@ -1935,7 +1934,7 @@ class TransformIdentityMatrix(unittest.TestCase): class TransformRotate135(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-135.svg') + doc = scourXmlFile('unittests/transform-matrix-is-rotate-135.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(135)', 'Rotation matrix not converted to rotate(135)') @@ -1943,7 +1942,7 @@ class TransformRotate135(unittest.TestCase): class TransformRotate45(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-45.svg') + doc = scourXmlFile('unittests/transform-matrix-is-rotate-45.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(45)', 'Rotation matrix not converted to rotate(45)') @@ -1951,7 +1950,7 @@ class TransformRotate45(unittest.TestCase): class TransformRotate90(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-90.svg') + doc = scourXmlFile('unittests/transform-matrix-is-rotate-90.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', 'Rotation matrix not converted to rotate(90)') @@ -1959,7 +1958,7 @@ class TransformRotate90(unittest.TestCase): class TransformRotateCCW135(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-225.svg') + doc = scourXmlFile('unittests/transform-matrix-is-rotate-225.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(225)', 'Counter-clockwise rotation matrix not converted to rotate(225)') @@ -1967,7 +1966,7 @@ class TransformRotateCCW135(unittest.TestCase): class TransformRotateCCW45(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-neg-45.svg') + doc = scourXmlFile('unittests/transform-matrix-is-rotate-neg-45.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-45)', 'Counter-clockwise rotation matrix not converted to rotate(-45)') @@ -1975,7 +1974,7 @@ class TransformRotateCCW45(unittest.TestCase): class TransformRotateCCW90(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-rotate-neg-90.svg') + doc = scourXmlFile('unittests/transform-matrix-is-rotate-neg-90.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-90)', 'Counter-clockwise rotation matrix not converted to rotate(-90)') @@ -1983,7 +1982,7 @@ class TransformRotateCCW90(unittest.TestCase): class TransformScale2by3(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-scale-2-3.svg') + doc = scourXmlFile('unittests/transform-matrix-is-scale-2-3.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(2 3)', 'Scaling matrix not converted to scale(2 3)') @@ -1991,7 +1990,7 @@ class TransformScale2by3(unittest.TestCase): class TransformScaleMinus1(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-scale-neg-1.svg') + doc = scourXmlFile('unittests/transform-matrix-is-scale-neg-1.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'scale(-1)', 'Scaling matrix not converted to scale(-1)') @@ -1999,7 +1998,7 @@ class TransformScaleMinus1(unittest.TestCase): class TransformTranslate(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-matrix-is-translate.svg') + doc = scourXmlFile('unittests/transform-matrix-is-translate.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'translate(2 3)', 'Translation matrix not converted to translate(2 3)') @@ -2007,7 +2006,7 @@ class TransformTranslate(unittest.TestCase): class TransformRotationRange719_5(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-rotate-trim-range-719.5.svg') + doc = scourXmlFile('unittests/transform-rotate-trim-range-719.5.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(-.5)', 'Transform containing rotate(719.5) not shortened to rotate(-.5)') @@ -2015,7 +2014,7 @@ class TransformRotationRange719_5(unittest.TestCase): class TransformRotationRangeCCW540_0(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-rotate-trim-range-neg-540.0.svg') + doc = scourXmlFile('unittests/transform-rotate-trim-range-neg-540.0.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(180)', 'Transform containing rotate(-540.0) not shortened to rotate(180)') @@ -2023,7 +2022,7 @@ class TransformRotationRangeCCW540_0(unittest.TestCase): class TransformRotation3Args(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-rotate-fold-3args.svg') + doc = scourXmlFile('unittests/transform-rotate-fold-3args.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), 'rotate(90)', 'Optional zeroes in rotate(angle 0 0) not removed') @@ -2031,7 +2030,7 @@ class TransformRotation3Args(unittest.TestCase): class TransformIdentityRotation(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-rotate-is-identity.svg') + doc = scourXmlFile('unittests/transform-rotate-is-identity.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', 'Transform containing identity rotation not removed') @@ -2039,7 +2038,7 @@ class TransformIdentityRotation(unittest.TestCase): class TransformIdentitySkewX(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-skewX-is-identity.svg') + doc = scourXmlFile('unittests/transform-skewX-is-identity.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', 'Transform containing identity X-axis skew not removed') @@ -2047,7 +2046,7 @@ class TransformIdentitySkewX(unittest.TestCase): class TransformIdentitySkewY(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-skewY-is-identity.svg') + doc = scourXmlFile('unittests/transform-skewY-is-identity.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', 'Transform containing identity Y-axis skew not removed') @@ -2055,7 +2054,7 @@ class TransformIdentitySkewY(unittest.TestCase): class TransformIdentityTranslate(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/transform-translate-is-identity.svg') + doc = scourXmlFile('unittests/transform-translate-is-identity.svg') self.assertEqual(doc.getElementsByTagName('line')[0].getAttribute('transform'), '', 'Transform containing identity translation not removed') @@ -2063,8 +2062,8 @@ class TransformIdentityTranslate(unittest.TestCase): class DuplicateGradientsUpdateStyle(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/duplicate-gradients-update-style.svg', - scour.parse_args(['--disable-style-to-xml'])) + doc = scourXmlFile('unittests/duplicate-gradients-update-style.svg', + parse_args(['--disable-style-to-xml'])) gradient = doc.getElementsByTagName('linearGradient')[0] rects = doc.getElementsByTagName('rect') self.assertEqual('fill:url(#' + gradient.getAttribute('id') + ')', rects[0].getAttribute('style'), @@ -2082,16 +2081,16 @@ class DocWithFlowtext(unittest.TestCase): def runTest(self): with self.assertRaises(Exception): - scour.scourXmlFile('unittests/flowtext.svg', - scour.parse_args(['--error-on-flowtext'])) + scourXmlFile('unittests/flowtext.svg', + parse_args(['--error-on-flowtext'])) class DocWithNoFlowtext(unittest.TestCase): def runTest(self): try: - scour.scourXmlFile('unittests/flowtext-less.svg', - scour.parse_args(['--error-on-flowtext'])) + scourXmlFile('unittests/flowtext-less.svg', + parse_args(['--error-on-flowtext'])) except Exception as e: self.fail("exception '{}' was raised, and we didn't expect that!".format(e)) @@ -2099,7 +2098,7 @@ class DocWithNoFlowtext(unittest.TestCase): class ParseStyleAttribute(unittest.TestCase): def runTest(self): - doc = scour.scourXmlFile('unittests/style.svg') + doc = scourXmlFile('unittests/style.svg') self.assertEqual(doc.documentElement.getAttribute('style'), 'property1:value1;property2:value2;property3:value3', 'Style attribute not properly parsed and/or serialized') From d9198d7872c750e7ccc540d39a788d38ec74b6b7 Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Thu, 15 Sep 2016 21:56:36 +0200 Subject: [PATCH 5/6] Order imports as suggested by PEP 8 --- scour/scour.py | 31 +++++++++++++++---------------- scour/svg_regex.py | 1 + scour/svg_transform.py | 3 ++- setup.py | 3 ++- testscour.py | 9 ++++----- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/scour/scour.py b/scour/scour.py index a531795..8f32367 100644 --- a/scour/scour.py +++ b/scour/scour.py @@ -44,31 +44,30 @@ # - parse transform attribute # - if a <g> has only one element in it, collapse the <g> (ensure transform, etc are carried down) -# necessary to get true division -from __future__ import division -# Needed for Python 2/3 compatible print function. -from __future__ import print_function -from __future__ import absolute_import +from __future__ import division # use "true" division instead of integer division in Python 2 (see PEP 238) +from __future__ import print_function # use print() as a function in Python 2 (see PEP 3105) +from __future__ import absolute_import # use absolute imports by default in Python 2 (see PEP 328) -import os -import sys -import xml.dom.minidom -import re import math -import time -from collections import namedtuple -from scour.svg_regex import svg_parser -from scour.svg_transform import svg_transform_parser import optparse -from scour.yocto_css import parseCssString -import six -from six.moves import range +import os +import re +import sys +import time +import xml.dom.minidom +from collections import namedtuple from decimal import Context, Decimal, InvalidOperation, getcontext +import six +from six.moves import range +from scour.svg_regex import svg_parser +from scour.svg_transform import svg_transform_parser +from scour.yocto_css import parseCssString from scour import __version__ + APP = u'scour' VER = __version__ COPYRIGHT = u'Copyright Jeff Schiller, Louis Simard, 2010' diff --git a/scour/svg_regex.py b/scour/svg_regex.py index 4cba554..0926363 100644 --- a/scour/svg_regex.py +++ b/scour/svg_regex.py @@ -47,6 +47,7 @@ import re from decimal import Decimal, getcontext from functools import partial + # Sentinel. diff --git a/scour/svg_transform.py b/scour/svg_transform.py index 85bea88..944d34c 100644 --- a/scour/svg_transform.py +++ b/scour/svg_transform.py @@ -60,9 +60,10 @@ from __future__ import absolute_import import re from decimal import Decimal -from six.moves import range from functools import partial +from six.moves import range + # Sentinel. class _EOF(object): diff --git a/setup.py b/setup.py index 3fe0831..5149e88 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,8 @@ import os import re -from setuptools import setup, find_packages + +from setuptools import find_packages, setup LONGDESC = """ Scour is a SVG optimizer/sanitizer that can be used to produce SVGs for Web deployment. diff --git a/testscour.py b/testscour.py index 1f5bbf9..cd0c59e 100755 --- a/testscour.py +++ b/testscour.py @@ -22,24 +22,23 @@ from __future__ import absolute_import +import unittest + import six from six.moves import map, range -import unittest - +from scour.scour import makeWellFormed, parse_args, scourString, scourXmlFile from scour.svg_regex import svg_parser -from scour.scour import scourXmlFile, scourString, parse_args, makeWellFormed SVGNS = 'http://www.w3.org/2000/svg' + # I couldn't figure out how to get ElementTree to work with the following XPath # "//*[namespace-uri()='http://example.com']" # so I decided to use minidom and this helper function that performs a test on a given node # and all its children # func must return either True (if pass) or False (if fail) - - def walkTree(elem, func): if func(elem) is False: return False From f27ad1e416df999500e06a1f3013609238e6ba0b Mon Sep 17 00:00:00 2001 From: Eduard Braun <Eduard.Braun2@gmx.de> Date: Thu, 15 Sep 2016 22:15:10 +0200 Subject: [PATCH 6/6] add flake8 to Makefile --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index e0365bb..cb4ab46 100644 --- a/Makefile +++ b/Makefile @@ -23,3 +23,6 @@ test_error_on_flowtext: PYTHONPATH=. scour --error-on-flowtext unittests/flowtext-less.svg /dev/null # .. and this should bail out! PYTHONPATH=. scour --error-on-flowtext unittests/flowtext.svg /dev/null + +flake8: + flake8 --max-line-length=119 \ No newline at end of file